@herbertgao/pi-extensions 2026.8.4 → 2026.8.6

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 (360) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
  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/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  26. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  36. package/node_modules/@narumitw/pi-btw/README.md +32 -24
  37. package/node_modules/@narumitw/pi-btw/package.json +6 -6
  38. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  39. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
  40. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  41. package/node_modules/pi-lens/CHANGELOG.md +2535 -954
  42. package/node_modules/pi-lens/README.md +13 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  44. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  45. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  46. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  47. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  48. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  50. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  51. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  52. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  53. package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
  54. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  55. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  56. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  57. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  58. package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
  59. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  60. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  61. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  62. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  63. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  64. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  65. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  66. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  67. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
  69. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  70. package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
  71. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  72. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  73. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  74. package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
  75. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
  76. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
  77. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
  78. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  79. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  80. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  81. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  83. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
  84. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  87. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  114. package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
  115. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  116. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  117. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  118. package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
  119. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  120. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  121. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  122. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  123. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
  125. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  126. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  127. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  128. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  129. package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
  130. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  131. package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  134. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  135. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  136. package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
  137. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  138. package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
  139. package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
  140. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  141. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  142. package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
  143. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  144. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  145. package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
  146. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  147. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  148. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
  149. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  150. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  151. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
  152. package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
  153. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  154. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
  155. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  156. package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
  157. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  158. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
  161. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  162. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  163. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  164. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  165. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  167. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
  168. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  171. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  172. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  173. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  174. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  175. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  177. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  179. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  180. package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
  181. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  182. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  183. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  184. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  185. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  186. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  187. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  188. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  189. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  190. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  191. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  192. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  193. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  194. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  195. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  196. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  197. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  198. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  199. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  201. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  202. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  203. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  204. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  205. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  206. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  207. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  208. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  217. package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
  218. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  219. package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
  220. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  221. package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
  222. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  223. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  224. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  225. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  226. package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
  227. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  228. package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
  229. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  230. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  233. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  234. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  237. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  238. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  239. package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
  240. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  241. package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
  242. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  243. package/node_modules/pi-lens/dist/index.js +51785 -36118
  244. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  245. package/node_modules/pi-lens/dist/mcp/server.js +219 -46
  246. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  247. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  248. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  249. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  250. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
  251. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
  252. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  253. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  254. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  255. package/node_modules/pi-lens/docs/agent-guide.md +40 -15
  256. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  257. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  258. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  259. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
  260. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  261. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  262. package/node_modules/pi-lens/docs/fable.md +27 -0
  263. package/node_modules/pi-lens/docs/features.md +45 -3
  264. package/node_modules/pi-lens/docs/globalconfig.md +56 -1
  265. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  266. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  267. package/node_modules/pi-lens/docs/mcp.md +37 -4
  268. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  269. package/node_modules/pi-lens/docs/settings.md +18 -1
  270. package/node_modules/pi-lens/docs/tools.md +4 -1
  271. package/node_modules/pi-lens/docs/usage.md +41 -5
  272. package/node_modules/pi-lens/docs/word-index.md +35 -0
  273. package/node_modules/pi-lens/package.json +10 -10
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  300. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  301. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  302. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
  303. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  304. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  305. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  306. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  307. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  308. package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
  309. package/node_modules/pi-mcp-adapter/README.md +38 -4
  310. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  311. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  312. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  313. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  314. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  315. package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
  316. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  317. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  318. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  319. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  320. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  321. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  322. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  323. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  324. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  325. package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
  326. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  327. package/node_modules/pi-mcp-adapter/package.json +4 -3
  328. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  329. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  330. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  331. package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
  332. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  333. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  334. package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
  335. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  336. package/node_modules/pi-mcp-adapter/types.ts +176 -27
  337. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  338. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  339. package/node_modules/pi-web-access/README.md +18 -12
  340. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  341. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  342. package/node_modules/pi-web-access/curator-page.ts +5 -3
  343. package/node_modules/pi-web-access/curator-server.ts +2 -1
  344. package/node_modules/pi-web-access/extract.ts +106 -34
  345. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  346. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  347. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  348. package/node_modules/pi-web-access/index.ts +120 -48
  349. package/node_modules/pi-web-access/package.json +2 -2
  350. package/node_modules/pi-web-access/summary-review.ts +11 -5
  351. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  352. package/package.json +12 -13
  353. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  354. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  355. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  356. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  357. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  358. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  359. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  360. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -2,7 +2,7 @@ import { stat } from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { visibleWidth } from "./deps/pi-tui.js";
5
- import { normalizeEphemeralMapKey } from "./path-utils.js";
5
+ import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
6
6
  import { fitLine } from "./tui-fit.js";
7
7
  import { WriteOrderingGuard } from "./write-ordering-guard.js";
8
8
  /**
@@ -57,7 +57,42 @@ let requestRenderFn = null;
57
57
  * `clients/runtime-tool-result.ts:nextWriteIndex`).
58
58
  */
59
59
  const diagnosticsWriteGuard = new WriteOrderingGuard();
60
+ /**
61
+ * Runner completions also mutate the shared file record: they mark the
62
+ * diagnostics snapshot pending. Keep that mutation in the same per-file order
63
+ * as the final diagnostic replacement, or an older pipeline can set a newer
64
+ * confirmed-clean record back to `(pending)` (#1198).
65
+ */
66
+ const runnerWriteGuard = new WriteOrderingGuard();
60
67
  const MAX_STORED_DIAGNOSTICS_PER_FILE = 12;
68
+ const MAX_INACTIVE_FILE_RECORDS = 1024;
69
+ const ACTIVE_FILE_IDLE_MS = 30 * 60_000;
70
+ const MAX_LSP_SERVER_RECORDS = 128;
71
+ // Pruning is a cold-size-boundary operation. Do not walk the whole file map
72
+ // for every record in a large diagnostics reconciliation; the full-scan path
73
+ // can legitimately create thousands of records in one synchronous batch.
74
+ let nextInactivePruneSize = MAX_INACTIVE_FILE_RECORDS + 1;
75
+ function pruneInactiveFileRecords(now = Date.now()) {
76
+ if (files.size <= MAX_INACTIVE_FILE_RECORDS)
77
+ return;
78
+ const victims = [...files.entries()]
79
+ .filter(([, rec]) => now - rec.touchedAt > ACTIVE_FILE_IDLE_MS &&
80
+ !hasLiveDiagnostic(rec))
81
+ .sort(([, a], [, b]) => a.touchedAt - b.touchedAt);
82
+ for (const [key] of victims) {
83
+ if (files.size <= MAX_INACTIVE_FILE_RECORDS)
84
+ break;
85
+ files.delete(key);
86
+ }
87
+ }
88
+ function maybePruneInactiveFileRecords() {
89
+ if (files.size < nextInactivePruneSize)
90
+ return;
91
+ pruneInactiveFileRecords();
92
+ // A live-heavy map may remain above the soft bound. Do not rescan it for
93
+ // every subsequent file; the next lifecycle starts with a fresh state map.
94
+ nextInactivePruneSize = Number.POSITIVE_INFINITY;
95
+ }
61
96
  // ── Public API ────────────────────────────────────────────────────────────────
62
97
  export function setRenderCallback(fn) {
63
98
  requestRenderFn = fn;
@@ -68,8 +103,15 @@ export function clearWidgetState() {
68
103
  sessionLanguages = [];
69
104
  requestRenderFn = null;
70
105
  diagnosticsWriteGuard.clear();
71
- }
72
- const WIDGET_STATE_VERSION = 1;
106
+ runnerWriteGuard.clear();
107
+ nextInactivePruneSize = MAX_INACTIVE_FILE_RECORDS + 1;
108
+ }
109
+ // v1 → v2 (#1186): per-entry `WidgetDiagnostic.observedAt`. v2 is a SUPERSET of
110
+ // v1 (the field is additive/optional), so `importWidgetState` accepts either and
111
+ // migrates a v1 record by inheriting each entry's `observedAt` from the record's
112
+ // `touchedAt`. A v1 file must never be rejected (that would silently drop resume
113
+ // diagnostics) nor crash.
114
+ export const WIDGET_STATE_VERSION = 2;
73
115
  /**
74
116
  * Snapshot the per-file widget diagnostics for persistence (#190). Excludes
75
117
  * `lspServers` — those are process-bound (servers re-spawn fresh on the next
@@ -96,9 +138,29 @@ export function exportWidgetState() {
96
138
  * Replaces the in-memory `files` map; ignores snapshots from a different
97
139
  * version. Triggers a re-render if a callback is registered.
98
140
  */
141
+ /**
142
+ * #1186 v1→v2 migration: stamp each entry that lacks a per-entry `observedAt`
143
+ * with the record's `touchedAt` (the single stamp the whole record shared under
144
+ * v1). Non-mutating; entries that already carry a stamp (a v2 record) pass
145
+ * through untouched.
146
+ */
147
+ function migrateEntryStamps(entries, recordTouchedAt) {
148
+ return (entries ?? []).map((d) => d.observedAt == null ? { ...d, observedAt: recordTouchedAt } : d);
149
+ }
99
150
  export function importWidgetState(state) {
100
- if (!state || state.version !== WIDGET_STATE_VERSION)
151
+ // Accept any known-or-older version and migrate (#1186): reject a missing
152
+ // snapshot, a missing/non-numeric `version` (NaN/undefined/null — the
153
+ // pre-#1186 guard `version !== WIDGET_STATE_VERSION` rejected these, and
154
+ // loosening that would silently admit a malformed/foreign snapshot), or a
155
+ // FUTURE version this build can't understand. Rejecting a v1
156
+ // (pre-per-entry-stamp) file, by contrast, would silently drop all resume
157
+ // diagnostics — so v1..current are accepted and migrated.
158
+ if (!state ||
159
+ typeof state.version !== "number" ||
160
+ state.version < 1 ||
161
+ state.version > WIDGET_STATE_VERSION) {
101
162
  return false;
163
+ }
102
164
  files.clear();
103
165
  // A resumed session's writeIndex counter starts fresh (#190 rehydration is
104
166
  // process-bound like lspServers, see the export above) — any ordering
@@ -106,26 +168,39 @@ export function importWidgetState(state) {
106
168
  // drop them rather than risk a legitimate post-resume write being read as
107
169
  // "superseded" against a stale token.
108
170
  diagnosticsWriteGuard.clear();
171
+ runnerWriteGuard.clear();
109
172
  for (const f of state.files ?? []) {
110
173
  // Fold persisted keys through the same normalizer as live writes (#1020),
111
174
  // or a persisted forward-slash key stays split from a fresh backslash key
112
175
  // across a resumed session — a primary repro condition. Keep a readable
113
176
  // display path on the record.
177
+ // #1186 migration: a v1 record's entries have no per-entry `observedAt`.
178
+ // Inherit the record's `touchedAt` (a safe, over-conservative default —
179
+ // the whole record shared that one stamp before), so the per-entry stale
180
+ // gate has a concrete observation time and never treats `undefined` as
181
+ // epoch-0 (which would drop every migrated entry on the first sweep).
182
+ const recordTouchedAt = f.touchedAt ?? Date.now();
114
183
  files.set(fileMapKey(f.filePath), {
115
184
  filePath: f.filePath,
116
185
  runners: new Map(f.runners ?? []),
117
- formatters: new Map(f.formatters ?? []),
118
- diagnostics: f.diagnostics ?? [],
119
- allDiagnostics: f.allDiagnostics ?? [],
186
+ // Failure entries do NOT survive a session restore (#1348 review):
187
+ // a fmt-failed marker is live advice about THIS session's last
188
+ // attempt; rehydrating one from a snapshot shows a stale failure the
189
+ // current session never observed (and same-mtime fixes would never
190
+ // clear it). Successes rehydrate as before.
191
+ formatters: new Map((f.formatters ?? []).filter(([, outcome]) => outcome?.success !== false)),
192
+ diagnostics: migrateEntryStamps(f.diagnostics, recordTouchedAt),
193
+ allDiagnostics: migrateEntryStamps(f.allDiagnostics, recordTouchedAt),
120
194
  diagnosticCounts: f.diagnosticCounts ?? {
121
195
  blocking: 0,
122
196
  errors: 0,
123
197
  warnings: 0,
124
198
  },
125
199
  hasFinalDiagnosticsSnapshot: f.hasFinalDiagnosticsSnapshot ?? false,
126
- touchedAt: f.touchedAt ?? Date.now(),
200
+ touchedAt: recordTouchedAt,
127
201
  });
128
202
  }
203
+ pruneInactiveFileRecords();
129
204
  sessionLanguages = state.sessionLanguages ?? [];
130
205
  requestRenderFn?.();
131
206
  return true;
@@ -161,7 +236,15 @@ export function recordFormatter(filePath, formatter, changed, success) {
161
236
  files.set(fileMapKey(filePath), rec);
162
237
  requestRender();
163
238
  }
164
- export function recordRunner(filePath, runnerId, status, diagnosticCount, durationMs) {
239
+ export function recordRunner(filePath, runnerId, status, diagnosticCount, durationMs, writeIndex) {
240
+ const key = fileMapKey(filePath);
241
+ if (!runnerWriteGuard.shouldWrite(key, writeIndex))
242
+ return;
243
+ // Advance the final-diagnostics guard too. A runner completion is part of
244
+ // this pipeline's ordered write, even though its immediate effect is only to
245
+ // mark the record pending. This prevents an older final replacement from
246
+ // landing after a newer runner state (and vice versa).
247
+ diagnosticsWriteGuard.shouldWrite(key, writeIndex);
165
248
  const rec = getOrCreate(filePath);
166
249
  rec.runners.set(runnerId, { status, count: diagnosticCount, durationMs });
167
250
  rec.hasFinalDiagnosticsSnapshot = false;
@@ -179,7 +262,16 @@ export function recordRunner(filePath, runnerId, status, diagnosticCount, durati
179
262
  function toSingleLineMessage(message) {
180
263
  return (message ?? "").replace(/\s+/g, " ").trim();
181
264
  }
182
- export function recordDiagnostics(filePath, diagnostics, writeIndex) {
265
+ export function recordDiagnostics(filePath, diagnostics, writeIndex,
266
+ // #1093: when the truth was OBSERVED, not when it's being written. Defaults
267
+ // to `Date.now()` for the per-edit/live path (observed now). A reconcile
268
+ // replaying a CACHED view (e.g. the workspace-diagnostics cache-hit branch in
269
+ // `tools/lsp-diagnostics.ts`) must pass the cache entry's own scan timestamp
270
+ // here — otherwise a repeat "fresh check" that merely re-serves a stale
271
+ // cached view keeps bumping `touchedAt` to now(), permanently disarming
272
+ // `reconcileStaleWidgetFiles`'s `mtimeMs > touchedAt` gate so a resolved
273
+ // finding renders forever (the #1092 touchedAt-re-arming defect).
274
+ observedAt) {
183
275
  // Drop a write that's superseded by a later same-turn edit to this file
184
276
  // whose pipeline finished first (same race class as #555). No cache write,
185
277
  // no count/timestamp update, no render trigger — the recorded state must
@@ -187,11 +279,27 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
187
279
  // the `clients/mcp/analyze.ts` on-demand call site, which has no per-edit
188
280
  // ordering token) always proceeds, same as version-less LSP servers in the
189
281
  // #555 guard.
190
- if (!diagnosticsWriteGuard.shouldWrite(fileMapKey(filePath), writeIndex))
282
+ const key = fileMapKey(filePath);
283
+ if (!diagnosticsWriteGuard.shouldWrite(key, writeIndex))
191
284
  return;
192
- const rec = getOrCreate(filePath);
285
+ // Keep runner state ordered with the final diagnostic replacement. The
286
+ // guards are deliberately advanced in both directions because either verb
287
+ // may be the first completion from a pipeline.
288
+ runnerWriteGuard.shouldWrite(key, writeIndex);
289
+ // Resolve the observation time ONCE (#1186): every incoming entry is stamped
290
+ // with it, and it also seeds the record's `touchedAt`. A fresh write (no
291
+ // `observedAt`) is observed now.
292
+ const observedTs = observedAt ?? Date.now();
293
+ const rec = getOrCreate(filePath, key);
294
+ commitDiagnostics(rec, filePath, normalizeDiagnostics(filePath, diagnostics, observedTs), observedTs, key);
295
+ }
296
+ /** Map the raw diagnostic shape callers pass into stored {@link WidgetDiagnostic}s.
297
+ * Every produced entry is stamped with `observedTs` (#1186) — the time THIS batch
298
+ * of diagnostics was observed — so the per-entry stale gate can later drop just
299
+ * the entries older than the file's mtime rather than the whole record. */
300
+ function normalizeDiagnostics(filePath, diagnostics, observedTs) {
193
301
  const base = pathToFileURL(filePath).href;
194
- const normalized = diagnostics.map((d) => {
302
+ return diagnostics.map((d) => {
195
303
  const rule = d.rule ?? d.id;
196
304
  const uri = d.line != null
197
305
  ? `${base}#L${d.line}${d.column != null ? `:${d.column}` : ""}`
@@ -205,12 +313,35 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
205
313
  rule,
206
314
  tool: d.tool,
207
315
  uri,
316
+ observedAt: observedTs,
208
317
  };
209
318
  });
319
+ }
320
+ /** Store `normalized` as the record's complete diagnostic set: recompute counts,
321
+ * cap the display list, stamp `touchedAt` (at `observedAt` when given, else now
322
+ * — #1093), persist, and re-render. The caller decides what `normalized`
323
+ * contains (a full replace, or a merge — see `reconcileCascadeNeighborLspErrors`). */
324
+ function commitDiagnostics(rec, filePath, normalized, observedAt, key = fileMapKey(filePath)) {
325
+ rec.diagnosticCounts = countDiagnostics(normalized);
326
+ rec.diagnostics = capStoredDiagnostics(normalized);
327
+ rec.allDiagnostics = normalized;
328
+ rec.hasFinalDiagnosticsSnapshot = true;
329
+ // Record-level `touchedAt` is the FRESHEST per-entry observation in the merged
330
+ // set (#1186) — drives render recency and the empty-record stale gate. On a
331
+ // merge (`reconcileCascadeNeighborLspErrors`) this is the newest of the
332
+ // preserved + incoming entries, not the (possibly aging) incoming stamp, so a
333
+ // record holding a fresh preserved entry doesn't sort/gate as stale. Empty set
334
+ // falls back to the passed `observedAt` (or now).
335
+ rec.touchedAt = freshestObservation(normalized, observedAt ?? Date.now());
336
+ files.set(key, rec);
337
+ requestRender();
338
+ }
339
+ /** Recompute the {blocking, errors, warnings} tally for a diagnostic set. */
340
+ function countDiagnostics(diags) {
210
341
  let blocking = 0;
211
342
  let errors = 0;
212
343
  let warnings = 0;
213
- for (const diagnostic of normalized) {
344
+ for (const diagnostic of diags) {
214
345
  if (isBlocking(diagnostic))
215
346
  blocking++;
216
347
  if (diagnostic.severity === "error")
@@ -218,13 +349,69 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
218
349
  else if (diagnostic.severity === "warning")
219
350
  warnings++;
220
351
  }
221
- rec.diagnosticCounts = { blocking, errors, warnings };
222
- rec.diagnostics = capStoredDiagnostics(normalized);
223
- rec.allDiagnostics = normalized;
224
- rec.hasFinalDiagnosticsSnapshot = true;
225
- rec.touchedAt = Date.now();
226
- files.set(fileMapKey(filePath), rec);
227
- requestRender();
352
+ return { blocking, errors, warnings };
353
+ }
354
+ /** The newest per-entry `observedAt` in `diags`, or `fallback` when empty (or no
355
+ * entry carries a stamp). Used as the record-level `touchedAt` (#1186). */
356
+ function freshestObservation(diags, fallback) {
357
+ let newest;
358
+ for (const d of diags) {
359
+ if (d.observedAt != null && (newest === undefined || d.observedAt > newest)) {
360
+ newest = d.observedAt;
361
+ }
362
+ }
363
+ return newest ?? fallback;
364
+ }
365
+ /**
366
+ * A stored diagnostic that came from a language SERVER (tsserver, pyright, …)
367
+ * AND is an error/blocking finding. `convertLspDiagnostics` tags every
368
+ * language-server diagnostic with `tool: "lsp"` and `retagAuxiliaryDiagnostics`
369
+ * re-tags auxiliary-LSP findings (opengrep/ast-grep/zizmor/typos) to their real
370
+ * tool id, so `tool === "lsp"` uniquely identifies a genuine language-server
371
+ * entry. Used by the cascade merge below to decide which existing entries an
372
+ * errors-only LSP re-check is entitled to replace.
373
+ */
374
+ function isLspErrorEntry(d) {
375
+ return d.tool === "lsp" && (d.semantic === "blocking" || d.severity === "error");
376
+ }
377
+ /**
378
+ * Reconcile a CONFIRMED cascade neighbor re-check (#1093) into the footer,
379
+ * MERGING rather than whole-replacing (unlike `recordDiagnostics`).
380
+ *
381
+ * The cascade only re-checks a neighbor through its LANGUAGE SERVER, and only
382
+ * for ERRORS (`severity === 1`; see `clients/dispatch/integration.ts`). A plain
383
+ * full-replace would therefore erase the neighbor's live findings from OTHER
384
+ * sources that the cascade never re-examined — biome/ruff/ast-grep runner
385
+ * findings, and even the language server's own WARNINGS — turning a
386
+ * cross-file-error re-check into a silent false-clean for everything else
387
+ * (#533). So we replace ONLY the existing LSP-error entries
388
+ * (`isLspErrorEntry`) with the cascade's fresh LSP errors and preserve
389
+ * everything else verbatim. A stale LSP warning or biome finding therefore
390
+ * survives an errors-only cascade — correct, because this check never looked at
391
+ * it; it self-corrects on the next per-edit dispatch or a `lens_diagnostics`
392
+ * scan (which DO re-examine every source).
393
+ *
394
+ * Only ever call this for a CONFIRMED result (a valid passive snapshot or a
395
+ * completed, NON-inconclusive active touch — #571). `writeIndex` and
396
+ * `observedAt` behave exactly as in `recordDiagnostics`.
397
+ */
398
+ export function reconcileCascadeNeighborLspErrors(filePath, lspErrorDiagnostics, writeIndex, observedAt) {
399
+ const key = fileMapKey(filePath);
400
+ if (!diagnosticsWriteGuard.shouldWrite(key, writeIndex))
401
+ return;
402
+ runnerWriteGuard.shouldWrite(key, writeIndex);
403
+ // #1186: the INCOMING LSP errors are stamped at THIS observation time
404
+ // (`observedAt`, e.g. an aging passive snapshot's `entry.ts`, or now for a
405
+ // fresh active touch). The PRESERVED entries keep their OWN prior per-entry
406
+ // `observedAt` — a fresh per-edit finding preserved through this errors-only
407
+ // merge is NOT re-aged to the incoming stamp. That per-entry split is exactly
408
+ // what lets `reconcileStaleWidgetFiles` drop the stale incoming entry while
409
+ // keeping the newer preserved one, instead of dropping the whole record.
410
+ const observedTs = observedAt ?? Date.now();
411
+ const rec = getOrCreate(filePath);
412
+ const incoming = normalizeDiagnostics(filePath, lspErrorDiagnostics, observedTs);
413
+ const preserved = rec.allDiagnostics.filter((d) => !isLspErrorEntry(d));
414
+ commitDiagnostics(rec, filePath, [...incoming, ...preserved], observedTs);
228
415
  }
229
416
  /**
230
417
  * Reconcile a diagnostics result obtained OUTSIDE the per-edit dispatch
@@ -253,11 +440,18 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
253
440
  * write for the same file — an omitted `writeIndex` always proceeds (same
254
441
  * version-less fallback `recordDiagnostics` already documents), which is
255
442
  * only safe for callers with no ordering token to give (e.g. tests).
443
+ *
444
+ * `observedAt` (#1093) is the wall-clock time the diagnostics were actually
445
+ * OBSERVED — pass it whenever the reconciled result is a replay of an older
446
+ * CACHED observation (the workspace-diagnostics cache-hit branch), so
447
+ * `touchedAt` records when the truth was seen, not when it was written. Omit
448
+ * it for genuinely fresh observations (a just-completed touch/scan), which are
449
+ * observed now.
256
450
  */
257
- export function reconcileScanDiagnostics(filePath, diagnostics, confirmed, writeIndex) {
451
+ export function reconcileScanDiagnostics(filePath, diagnostics, confirmed, writeIndex, observedAt) {
258
452
  if (!confirmed)
259
453
  return;
260
- recordDiagnostics(filePath, diagnostics, writeIndex);
454
+ recordDiagnostics(filePath, diagnostics, writeIndex, observedAt);
261
455
  }
262
456
  /**
263
457
  * Drop widget entries whose file changed on disk after pi-lens last recorded
@@ -271,25 +465,60 @@ export function reconcileScanDiagnostics(filePath, diagnostics, confirmed, write
271
465
  */
272
466
  export async function reconcileStaleWidgetFiles() {
273
467
  const entries = [...files.entries()];
274
- const staleKeys = await Promise.all(
468
+ const verdicts = await Promise.all(
275
469
  // `mapKey` is the normalized `files` key (used for deletion); stat the
276
470
  // record's real display path, not the lowercased key (#1020).
277
471
  entries.map(async ([mapKey, rec]) => {
472
+ let mtimeMs;
278
473
  try {
279
- const st = await stat(rec.filePath);
280
- // +1ms tolerance: a freshly-recorded file has touchedAt >= mtime.
281
- return st.mtimeMs > rec.touchedAt + 1 ? mapKey : undefined;
474
+ mtimeMs = (await stat(rec.filePath)).mtimeMs;
282
475
  }
283
476
  catch {
284
- return mapKey; // deleted / unreadable → drop
477
+ return { mapKey, action: "drop" }; // deleted / unreadable → drop
285
478
  }
479
+ // A clean record (no findings) has no per-entry stamps to consult —
480
+ // gate it on the record's own `touchedAt` exactly as before, so a ✓
481
+ // entry for a file that changed on disk still drops.
482
+ if (rec.allDiagnostics.length === 0) {
483
+ return mtimeMs > rec.touchedAt + 1
484
+ ? { mapKey, action: "drop" }
485
+ : { mapKey, action: "keep" };
486
+ }
487
+ // #1186 per-ENTRY gate: drop only the entries observed BEFORE the file's
488
+ // current mtime; keep the rest. A merged record can hold a fresh
489
+ // preserved entry beside an entry replayed from an aging snapshot, so a
490
+ // per-RECORD gate over-cleared the whole record (the residual documented
491
+ // at dispatch/integration.ts). A missing per-entry stamp (a migrated
492
+ // pre-#1186 record) inherits the record's `touchedAt`. +1ms tolerance:
493
+ // a freshly-recorded file has observedAt >= mtime.
494
+ const survivors = rec.allDiagnostics.filter((d) => !(mtimeMs > (d.observedAt ?? rec.touchedAt) + 1));
495
+ if (survivors.length === rec.allDiagnostics.length) {
496
+ return { mapKey, action: "keep" }; // nothing stale
497
+ }
498
+ if (survivors.length === 0) {
499
+ return { mapKey, action: "drop" }; // every entry stale → drop record
500
+ }
501
+ return { mapKey, action: "prune", survivors };
286
502
  }));
287
503
  let dropped = 0;
288
- for (const key of staleKeys) {
289
- if (key !== undefined) {
290
- files.delete(key);
504
+ for (const v of verdicts) {
505
+ if (v.action === "keep")
506
+ continue;
507
+ if (v.action === "drop") {
508
+ files.delete(v.mapKey);
291
509
  dropped += 1;
510
+ continue;
511
+ }
512
+ // prune: the file changed and shed its stale entries but retains fresher
513
+ // ones — keep the record, recompute counts/cap from the survivors, and
514
+ // still count it as a changed file so the agent is told to rescan.
515
+ const rec = files.get(v.mapKey);
516
+ if (rec) {
517
+ rec.allDiagnostics = v.survivors;
518
+ rec.diagnostics = capStoredDiagnostics(v.survivors);
519
+ rec.diagnosticCounts = countDiagnostics(v.survivors);
292
520
  }
521
+ dropped += 1;
293
522
  }
294
523
  if (dropped > 0)
295
524
  requestRenderFn?.();
@@ -372,13 +601,20 @@ export const __testing = {
372
601
  },
373
602
  };
374
603
  export function recordLsp(serverId, root, status, durationMs) {
375
- const key = `${serverId}@${root}`;
604
+ const normalizedRoot = normalizeMapKey(root);
605
+ const key = `${serverId}@${normalizedRoot}`;
376
606
  const mapped = status === "spawn_start"
377
607
  ? "spawning"
378
608
  : status === "spawn_success"
379
609
  ? "ready"
380
610
  : "failed";
381
611
  lspServers.set(key, { serverId, root, status: mapped, durationMs });
612
+ while (lspServers.size > MAX_LSP_SERVER_RECORDS) {
613
+ const oldest = lspServers.keys().next().value;
614
+ if (oldest === undefined)
615
+ break;
616
+ lspServers.delete(oldest);
617
+ }
382
618
  requestRender();
383
619
  }
384
620
  // ── Render ────────────────────────────────────────────────────────────────────
@@ -488,11 +724,17 @@ function formatFileRowVertical(rec, theme) {
488
724
  const blocking = rec.diagnosticCounts.blocking;
489
725
  const errors = rec.diagnosticCounts.errors;
490
726
  const warnings = rec.diagnosticCounts.warnings;
727
+ const formatterFailed = hasFailedFormatter(rec);
728
+ // Diagnostic severity outranks formatter failure (#1348 review): a file
729
+ // with blocking diagnostics shows the blocking dot even if a format also
730
+ // failed -- same precedence as the horizontal renderer.
491
731
  const dot = blocking > 0
492
732
  ? red("●")
493
- : warnings > 0 || errors > 0
494
- ? yellow("!")
495
- : green("✓");
733
+ : formatterFailed
734
+ ? red("x")
735
+ : warnings > 0 || errors > 0
736
+ ? yellow("!")
737
+ : green("✓");
496
738
  const runnerNames = [...rec.runners.entries()]
497
739
  .filter(([, r]) => r.status !== "skipped")
498
740
  .map(([id]) => id)
@@ -505,11 +747,15 @@ function formatFileRowVertical(rec, theme) {
505
747
  ? " " + yellow(`${warnings}W`)
506
748
  : " " + dim("clean");
507
749
  const changedFormatters = [...rec.formatters.entries()]
508
- .filter(([, f]) => f.changed)
750
+ .filter(([, f]) => f.changed && f.success)
509
751
  .map(([name]) => name);
510
- const formatMark = changedFormatters.length > 0
511
- ? dim(` fmt:${changedFormatters.join(",")}`)
512
- : "";
752
+ const failedFormatters = [...rec.formatters.entries()]
753
+ .filter(([, f]) => !f.success)
754
+ .map(([name]) => name);
755
+ const formatMark = (failedFormatters.length > 0
756
+ ? red(` fmt-failed:${failedFormatters.join(",")}`)
757
+ : "") +
758
+ (changedFormatters.length > 0 ? dim(` fmt:${changedFormatters.join(",")}`) : "");
513
759
  return ` ${dot} ${base} ${dim(runnerNames)}${formatMark}${counts}`;
514
760
  }
515
761
  function packHorizontalRow(recs, totalWidth, theme) {
@@ -577,6 +823,7 @@ function formatFileTokenHorizontal(rec, remainingWidth, theme) {
577
823
  const errors = rec.diagnosticCounts.errors;
578
824
  const warnings = rec.diagnosticCounts.warnings;
579
825
  const formatterChanged = hasChangedFormatter(rec);
826
+ const formatterFailed = hasFailedFormatter(rec);
580
827
  let dotChar;
581
828
  if (blocking > 0)
582
829
  dotChar = red("●");
@@ -586,6 +833,9 @@ function formatFileTokenHorizontal(rec, remainingWidth, theme) {
586
833
  dotChar = dim("✎");
587
834
  else
588
835
  dotChar = dim("·");
836
+ if (formatterFailed && blocking === 0 && errors === 0 && warnings === 0) {
837
+ dotChar = red("x");
838
+ }
589
839
  let countsStyled = "";
590
840
  if (errors > 0 && warnings > 0) {
591
841
  const eColor = blocking > 0 ? red : yellow;
@@ -622,10 +872,11 @@ function truncateBasename(name, maxWidth) {
622
872
  return stem.slice(0, keep) + "…" + ext;
623
873
  }
624
874
  // ── Helpers ──────────────────────────────────────────────────────────────────
625
- function getOrCreate(filePath) {
875
+ function getOrCreate(filePath, key = fileMapKey(filePath)) {
876
+ maybePruneInactiveFileRecords();
626
877
  // Look up by the normalized key so mixed path forms of the same file share
627
878
  // ONE record (#1020); keep the caller's verbatim path as the display path.
628
- return (files.get(fileMapKey(filePath)) ?? {
879
+ return (files.get(key) ?? {
629
880
  filePath,
630
881
  runners: new Map(),
631
882
  formatters: new Map(),
@@ -637,10 +888,16 @@ function getOrCreate(filePath) {
637
888
  });
638
889
  }
639
890
  function hasChangedFormatter(rec) {
640
- return [...rec.formatters.values()].some((f) => f.changed);
891
+ return [...rec.formatters.values()].some((f) => f.changed && f.success);
892
+ }
893
+ function hasFailedFormatter(rec) {
894
+ return [...rec.formatters.values()].some((f) => !f.success);
641
895
  }
642
896
  function shouldRenderFile(rec) {
643
- return rec.hasFinalDiagnosticsSnapshot || hasChangedFormatter(rec);
897
+ return rec.hasFinalDiagnosticsSnapshot || hasChangedFormatter(rec) || hasFailedFormatter(rec);
898
+ }
899
+ function hasLiveDiagnostic(rec) {
900
+ return rec.hasFinalDiagnosticsSnapshot && rec.diagnostics.length > 0;
644
901
  }
645
902
  function isPendingAnalysis(rec) {
646
903
  return rec.runners.size > 0 && !rec.hasFinalDiagnosticsSnapshot;