@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
@@ -8,10 +8,14 @@
8
8
  * Requires: npm install -D madge
9
9
  * Docs: https://github.com/pahen/madge
10
10
  */
11
+ import { createSubsystemLogger } from "./extension-log.js";
11
12
  import * as fs from "node:fs";
12
13
  import * as path from "node:path";
13
14
  import { findNodeToolBinary } from "./package-manager.js";
15
+ import { isFullyQualified } from "./path-utils.js";
14
16
  import { safeSpawnAsync } from "./safe-spawn.js";
17
+ import { createAvailabilityChecker, discoverManagedTool, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
18
+ import { createAvailabilityLatch } from "./dispatch/runners/utils/availability-policy.js";
15
19
  /**
16
20
  * Build madge's argv for a circular-dependency scan.
17
21
  *
@@ -67,25 +71,96 @@ export function parseMadgeSkips(stderr) {
67
71
  * fork-bombing subprocesses.
68
72
  */
69
73
  const MADGE_BATCH_CONCURRENCY = 6;
74
+ /**
75
+ * Cap on per-target timings kept in `MadgeBatchStats`. A bulk rename can hand
76
+ * the batch hundreds of files; the latency log is a diagnostic breadcrumb, not
77
+ * a transcript, and the aggregate counts stay exact either way.
78
+ */
79
+ const MADGE_STATS_TARGET_CAP = 12;
80
+ /**
81
+ * Fast madge targets are not useful enough to justify widening the shared
82
+ * latency.log records. Aggregate counts remain unconditional; retain a
83
+ * target breadcrumb only when the spawn itself was slow enough to explain a
84
+ * turn-end tail.
85
+ */
86
+ const MADGE_STATS_TARGET_MIN_DURATION_MS = 100;
87
+ /** Is `target` inside `dir`? */
88
+ function isWithin(dir, target) {
89
+ const rel = path.relative(dir, target);
90
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
91
+ }
92
+ /**
93
+ * Classify a resolved madge command for `MadgeCommandKind`. Both resolution
94
+ * steps feed this: `findNodeToolBinary` walks up to the project's
95
+ * `node_modules/.bin` but ALSO probes the npm/pnpm/yarn/bun global bins, so
96
+ * "which step answered" says nothing about where the binary actually lives.
97
+ */
98
+ function classifyMadgeKind(resolved, managedToolsDir, projectRoot) {
99
+ if (!isFullyQualified(resolved))
100
+ return "path";
101
+ if (isWithin(managedToolsDir, resolved))
102
+ return "managed";
103
+ if (isWithin(projectRoot, resolved))
104
+ return "local";
105
+ return "global";
106
+ }
107
+ /**
108
+ * Has the memoized resolution stopped being reachable? An absolute path is
109
+ * checked with a plain `existsSync` (madge uninstalled, the managed tree
110
+ * wiped). `"npx"` is the deliberate no-memo fallback (already dropped from
111
+ * the cache the moment it's produced — see `resolveUnlessNpx`) and is never
112
+ * itself stale. Everything else is a bare PATH name (`"madge"`, a shell
113
+ * alias resolved via `findNodeToolBinary`'s global-bin probes, …) — #1276:
114
+ * `existsSync` can't check those, so without this they were never
115
+ * revalidated and a PATH change or removed global install kept serving the
116
+ * old answer for the rest of the process.
117
+ *
118
+ * `spawnableCache` memoizes the bare-command branch per checker instance
119
+ * (P2 fix, #1276 review): `isSpawnableCommand` does a synchronous PATH walk,
120
+ * and without this it re-ran on every single cached `madgeCommand` hit —
121
+ * once per file when callers resolve per-file instead of via
122
+ * `checkFilesBatch`'s single per-batch resolution. The cache is cleared by
123
+ * `resetMadgeMemo()` alongside `madgeCommand`, so it stays exactly as fresh
124
+ * as the memo it's revalidating.
125
+ */
126
+ async function resolvedCommandIsStale(resolved, spawnableCache) {
127
+ if (resolved.kind === "npx")
128
+ return false;
129
+ if (isFullyQualified(resolved.cmd))
130
+ return !fs.existsSync(resolved.cmd);
131
+ const cached = spawnableCache.get(resolved.cmd);
132
+ if (cached !== undefined)
133
+ return !cached;
134
+ const { isSpawnableCommand } = await import("./installer/index.js");
135
+ const spawnable = await isSpawnableCommand(resolved.cmd);
136
+ spawnableCache.set(resolved.cmd, spawnable);
137
+ return !spawnable;
138
+ }
70
139
  /** Run `mapper` over `items` with at most `concurrency` in flight at once. */
71
140
  async function mapWithConcurrency(items, concurrency, mapper) {
72
141
  if (items.length === 0)
73
142
  return;
74
143
  let nextIndex = 0;
75
144
  const workerCount = Math.max(1, Math.min(concurrency, items.length));
76
- const workers = Array.from({ length: workerCount }, async () => {
145
+ const worker = async () => {
77
146
  while (true) {
78
147
  const index = nextIndex++;
79
148
  if (index >= items.length)
80
149
  return;
81
150
  await mapper(items[index]);
82
151
  }
83
- });
152
+ };
153
+ const workers = Array.from({ length: workerCount }, () => worker());
84
154
  await Promise.all(workers);
85
155
  }
86
156
  // --- Client ---
87
157
  export class DependencyChecker {
88
- available = null;
158
+ madgeAvailability = createAvailabilityChecker("madge", ".cmd", ["--version"], {
159
+ environment: (cwd) => getManagedToolEnvironment("madge", cwd),
160
+ unclassifiedFailureOutcome: "missing",
161
+ });
162
+ /** Transient-aware availability memo — see `ensureAvailable` (#1467). */
163
+ availabilityLatch = createAvailabilityLatch();
89
164
  ensureInFlight = null;
90
165
  checkInFlight = new Map();
91
166
  scanInFlight = new Map();
@@ -96,31 +171,171 @@ export class DependencyChecker {
96
171
  lastCircular = [];
97
172
  // Files that are part of a circular dependency
98
173
  circularFiles = new Set();
174
+ // Newest-classification-wins guard over the two fields above — see the
175
+ // cross-operation contract on `checkFilesBatch`.
176
+ opGeneration = 0;
177
+ stateGeneration = 0;
178
+ // projectRoot -> resolved madge command
179
+ madgeCommand = new Map();
180
+ // Bare command -> last-known spawnability (#1276 revalidation, P2 fix). A
181
+ // cache hit on `madgeCommand` used to re-run `isSpawnableCommand`'s
182
+ // synchronous PATH walk on EVERY resolution — unbounded FS work on a path
183
+ // that can run once per file in a batch. Memoized here instead, and
184
+ // invalidated by the exact same `resetMadgeMemo()` call the madge-command
185
+ // memo uses, so a madge removed/reinstalled mid-session is still detected
186
+ // on the next resolution after an install completes.
187
+ spawnableCache = new Map();
188
+ // #1276: every live checker registers itself so `resetMadgeManagedPathMemo`
189
+ // (called from installer's `finishInstallAttempt`, mirroring
190
+ // `resetSafeSpawnWindowsCommandCache`) can drop every instance's memo, not
191
+ // just whichever one happened to run the install. Held via `WeakRef` (not a
192
+ // strong `Set<DependencyChecker>`) so a checker that's no longer referenced
193
+ // anywhere else (test/reinit instances; production only ever constructs one
194
+ // in `bootstrap.ts`) can still be garbage-collected instead of being
195
+ // retained for the rest of the process — dead refs are pruned lazily the
196
+ // next time the registry is walked.
197
+ static instances = new Set();
99
198
  constructor(verbose = false) {
100
199
  this.log = verbose
101
- ? (msg) => console.error(`[deps] ${msg}`)
200
+ ? createSubsystemLogger("deps")
102
201
  : () => { };
202
+ DependencyChecker.instances.add(new WeakRef(this));
203
+ }
204
+ /** Drop this instance's memoized madge resolution for every project root. */
205
+ resetMadgeMemo() {
206
+ this.madgeCommand.clear();
207
+ this.spawnableCache.clear();
208
+ }
209
+ /**
210
+ * Reset hook for #1276: the madge managed-path memo is keyed only by
211
+ * `projectRoot`, but the memoized resolution reads PATH/PATHEXT, local/
212
+ * global tool discovery, and managed-install state — all of which a
213
+ * completed install can change. Call this from `finishInstallAttempt()`
214
+ * right alongside `resetSafeSpawnWindowsCommandCache()` so a mid-session
215
+ * install is picked up instead of serving the pre-install answer for the
216
+ * rest of the process.
217
+ */
218
+ static resetMadgeManagedPathMemo() {
219
+ for (const ref of DependencyChecker.instances) {
220
+ const checker = ref.deref();
221
+ if (!checker) {
222
+ // Garbage-collected since registration — prune the dead ref instead
223
+ // of leaving it around forever.
224
+ DependencyChecker.instances.delete(ref);
225
+ continue;
226
+ }
227
+ checker.resetMadgeMemo();
228
+ }
229
+ }
230
+ /**
231
+ * Resolve how to invoke madge for `projectRoot`: a project-local/global
232
+ * binary (npm/pnpm/yarn/bun) if found, else whatever the installer already
233
+ * has on disk, else `npx madge` (#375). `prefix` is prepended to the madge
234
+ * args (empty for a resolved binary, `["madge"]` for the npx fallback).
235
+ *
236
+ * Memoized per project root: `findNodeToolBinary` falls through to
237
+ * `npm config get prefix` / `pnpm bin -g` / `yarn global bin` spawns that
238
+ * nothing caches, and this used to run once per file inside the batch
239
+ * mapper (#766). The memo must not become one-way, though: resolution used
240
+ * to re-probe on every spawn and so healed itself, and both escapes below
241
+ * buy that back.
242
+ */
243
+ async resolveMadge(projectRoot) {
244
+ const cached = this.madgeCommand.get(projectRoot);
245
+ if (cached) {
246
+ // One stat per resolution: a binary that has since been uninstalled or
247
+ // moved must re-resolve, not fail every spawn for the rest of the
248
+ // session.
249
+ const resolved = await cached;
250
+ if (!(await resolvedCommandIsStale(resolved, this.spawnableCache)))
251
+ return resolved;
252
+ // Several callers may have observed the same stale promise before any
253
+ // continuation ran. Only remove the entry if it is still the promise
254
+ // this caller observed; otherwise join the newer resolution already in
255
+ // flight instead of deleting it and starting a duplicate probe.
256
+ const current = this.madgeCommand.get(projectRoot);
257
+ if (current === cached) {
258
+ this.madgeCommand.delete(projectRoot);
259
+ }
260
+ else {
261
+ // An npx result may have resolved and removed itself already. In that
262
+ // case falling through starts the intended fresh probe.
263
+ if (current)
264
+ return current;
265
+ }
266
+ }
267
+ const resolving = this.resolveUnlessNpx(projectRoot);
268
+ this.madgeCommand.set(projectRoot, resolving);
269
+ return resolving;
103
270
  }
104
271
  /**
105
- * Resolve how to invoke madge for `cwd`: a local/global-installed binary
106
- * (npm/pnpm/yarn/bun) if found, else `npx madge` (#375). `prefix` is prepended
107
- * to the madge args (empty for a resolved binary, `["madge"]` for the npx
108
- * fallback).
272
+ * Resolve, and drop the memo entry again if all we found was `npx` — pinning
273
+ * it would pin precisely the slow path #766 removes, and a transient
274
+ * installer failure would disable managed resolution for the whole session.
275
+ * Callers already in flight still coalesce on this promise; only the next
276
+ * one re-probes.
109
277
  */
110
- async resolveMadge(cwd) {
111
- const bin = await findNodeToolBinary("madge", cwd);
112
- return bin ? { cmd: bin, prefix: [] } : { cmd: "npx", prefix: ["madge"] };
278
+ async resolveUnlessNpx(projectRoot) {
279
+ const resolved = await this.doResolveMadge(projectRoot);
280
+ if (resolved.kind === "npx")
281
+ this.madgeCommand.delete(projectRoot);
282
+ return resolved;
113
283
  }
114
284
  /**
115
- * Check if madge is available, auto-install if not
285
+ * `allowInstall: false` is load-bearing: discovery here must never trigger a
286
+ * download, because installation is `ensureAvailable()`'s job and this runs
287
+ * on the spawn path. Resolution also never rejects — a throwing probe would
288
+ * otherwise poison the memo with a rejected promise for the whole session.
116
289
  */
117
- async ensureAvailable() {
118
- // Fast path: already checked
119
- if (this.available !== null)
120
- return this.available;
290
+ async doResolveMadge(projectRoot) {
291
+ try {
292
+ const { getManagedToolsDir } = await import("./installer/index.js");
293
+ const classify = (cmd) => ({
294
+ cmd,
295
+ prefix: [],
296
+ kind: classifyMadgeKind(cmd, getManagedToolsDir(), projectRoot),
297
+ });
298
+ const bin = await findNodeToolBinary("madge", projectRoot);
299
+ if (bin)
300
+ return classify(bin);
301
+ const discovered = await discoverManagedTool("madge");
302
+ if (discovered)
303
+ return classify(discovered);
304
+ }
305
+ catch (err) {
306
+ this.log(`Madge resolution failed, falling back to npx: ${String(err)}`);
307
+ }
308
+ return { cmd: "npx", prefix: ["madge"], kind: "npx" };
309
+ }
310
+ /**
311
+ * Apply a circular-dep state update on behalf of the operation that took
312
+ * generation `gen`. See the cross-operation contract on `checkFilesBatch`:
313
+ * an operation that classified EARLIER but finished later carries a smaller
314
+ * generation, and its write is dropped rather than resurrecting its stale
315
+ * view over newer state.
316
+ */
317
+ publishState(gen, circular, circularFiles) {
318
+ if (gen < this.stateGeneration)
319
+ return;
320
+ this.stateGeneration = gen;
321
+ this.lastCircular = circular;
322
+ this.circularFiles = circularFiles;
323
+ }
324
+ /**
325
+ * Check if madge is available, auto-install if not.
326
+ *
327
+ * Shares knip's latch policy (#1467): only a durable verdict is memoized, so
328
+ * a probe that timed out — madge's `--version` is ~0.7 s today, but a host
329
+ * event-loop stall can expire any budget — does not disable madge for the
330
+ * life of the process.
331
+ */
332
+ async ensureAvailable(cwd = process.cwd()) {
333
+ const memo = this.availabilityLatch.read();
334
+ if (memo !== null)
335
+ return memo;
121
336
  if (this.ensureInFlight)
122
337
  return this.ensureInFlight;
123
- this.ensureInFlight = this.doEnsureAvailable();
338
+ this.ensureInFlight = this.doEnsureAvailable(cwd);
124
339
  try {
125
340
  return await this.ensureInFlight;
126
341
  }
@@ -128,27 +343,14 @@ export class DependencyChecker {
128
343
  this.ensureInFlight = null;
129
344
  }
130
345
  }
131
- async doEnsureAvailable() {
132
- // Check if available in PATH
133
- const result = await safeSpawnAsync("madge", ["--version"], {
134
- timeout: 5000,
135
- });
136
- this.available = !result.error && result.status === 0;
137
- if (this.available) {
138
- this.log(`Madge found: ${result.stdout?.trim()}`);
139
- return true;
140
- }
141
- // Auto-install via pi-lens installer
142
- this.log("Madge not found, attempting auto-install...");
143
- const { ensureTool } = await import("./installer/index.js");
144
- const installedPath = await ensureTool("madge");
145
- if (installedPath) {
146
- this.log(`Madge auto-installed: ${installedPath}`);
147
- this.available = true;
346
+ async doEnsureAvailable(cwd) {
347
+ const resolved = await resolveAvailableOrInstall(this.madgeAvailability, "madge", cwd);
348
+ if (resolved !== null) {
349
+ this.availabilityLatch.noteAvailable();
148
350
  return true;
149
351
  }
150
- this.available = false;
151
- this.log("Madge auto-install failed");
352
+ const verdict = this.madgeAvailability.getVerdict(cwd);
353
+ this.availabilityLatch.noteUnavailable(verdict.outcome ?? "missing", verdict.cause ?? "not-found");
152
354
  return false;
153
355
  }
154
356
  /**
@@ -209,8 +411,9 @@ export class DependencyChecker {
209
411
  }
210
412
  const stat = fs.statSync(normalized);
211
413
  const cached = this.importCache.get(normalized);
212
- // Fast path: timestamp hasn't changed
213
- if (cached && cached.timestamp >= stat.mtimeMs) {
414
+ // Fast path: neither mtime NOR size moved (#1105 — size guards the
415
+ // mtime-preserving content change that mtime alone would miss).
416
+ if (cached && cached.timestamp >= stat.mtimeMs && cached.size === stat.size) {
214
417
  return false;
215
418
  }
216
419
  // Compare actual imports
@@ -220,6 +423,7 @@ export class DependencyChecker {
220
423
  this.importCache.set(normalized, {
221
424
  imports: newImports,
222
425
  timestamp: stat.mtimeMs,
426
+ size: stat.size,
223
427
  });
224
428
  return hasChanged;
225
429
  }
@@ -262,7 +466,9 @@ export class DependencyChecker {
262
466
  cacheHit: true,
263
467
  };
264
468
  }
265
- if (!(await this.ensureAvailable())) {
469
+ // Taken at classification time, not at publish time (see checkFilesBatch).
470
+ const gen = ++this.opGeneration;
471
+ if (!(await this.ensureAvailable(projectRoot))) {
266
472
  return {
267
473
  hasCircular: false,
268
474
  circular: [],
@@ -274,14 +480,15 @@ export class DependencyChecker {
274
480
  const existing = this.checkInFlight.get(key);
275
481
  if (existing)
276
482
  return existing;
277
- const promise = this.runCheckFile(normalized, projectRoot).finally(() => {
483
+ const promise = this.runCheckFile(normalized, projectRoot, gen).finally(() => {
278
484
  this.checkInFlight.delete(key);
279
485
  });
280
486
  this.checkInFlight.set(key, promise);
281
487
  return promise;
282
488
  }
283
- async runCheckFile(normalized, projectRoot) {
284
- const spawnResult = await this.runMadgeSpawn(normalized, projectRoot);
489
+ async runCheckFile(normalized, projectRoot, gen) {
490
+ const resolved = await this.resolveMadge(projectRoot);
491
+ const spawnResult = await this.runMadgeSpawn(normalized, projectRoot, resolved);
285
492
  if (!spawnResult.ok) {
286
493
  return {
287
494
  hasCircular: false,
@@ -290,8 +497,7 @@ export class DependencyChecker {
290
497
  cacheHit: false,
291
498
  };
292
499
  }
293
- this.lastCircular = spawnResult.circular;
294
- this.circularFiles = spawnResult.circularFiles;
500
+ this.publishState(gen, spawnResult.circular, spawnResult.circularFiles);
295
501
  return {
296
502
  hasCircular: spawnResult.circular.length > 0,
297
503
  circular: spawnResult.circular.filter((d) => d.file === normalized || d.path.includes(normalized)),
@@ -307,11 +513,10 @@ export class DependencyChecker {
307
513
  * themselves (`runCheckFile` does so immediately; `checkFilesBatch` defers
308
514
  * it so concurrent spawns can't clobber each other's writes, see #766).
309
515
  */
310
- async runMadgeSpawn(normalized, projectRoot) {
516
+ async runMadgeSpawn(normalized, projectRoot, { cmd, prefix }) {
311
517
  this.log(`Imports changed for ${path.basename(normalized)}, checking dependencies...`);
312
518
  // Run madge on the specific file (fast)
313
519
  try {
314
- const { cmd, prefix } = await this.resolveMadge(projectRoot);
315
520
  const result = await safeSpawnAsync(cmd, [...prefix, ...buildMadgeArgs(normalized, projectRoot)], {
316
521
  timeout: 15000,
317
522
  cwd: projectRoot,
@@ -347,11 +552,11 @@ export class DependencyChecker {
347
552
  return { ok: false };
348
553
  }
349
554
  }
350
- /** Build the cache-hit `DepCheckResult` for `normalized` from current shared state. */
351
- buildCachedResult(normalized) {
555
+ /** Build the cache-hit `DepCheckResult` for `normalized` from a state snapshot. */
556
+ buildCachedResult(normalized, circular, circularFiles) {
352
557
  return {
353
- hasCircular: this.circularFiles.has(normalized),
354
- circular: this.lastCircular.filter((d) => d.file === normalized || d.path.includes(normalized)),
558
+ hasCircular: circularFiles.has(normalized),
559
+ circular: circular.filter((d) => d.file === normalized || d.path.includes(normalized)),
355
560
  checked: true,
356
561
  cacheHit: true,
357
562
  };
@@ -361,25 +566,45 @@ export class DependencyChecker {
361
566
  * running the madge subprocess spawns concurrently (bounded by
362
567
  * `MADGE_BATCH_CONCURRENCY`) instead of one at a time (#766).
363
568
  *
364
- * Equivalence with the sequential `for…await checkFile(file)` loop it
365
- * replaces:
569
+ * `lastCircular`/`circularFiles` are shared instance state that every madge
570
+ * run OVERWRITES wholesale, so ordering has to be pinned on two axes.
571
+ *
572
+ * WITHIN a batch — equivalence with the sequential `for…await
573
+ * checkFile(file)` loop this replaces:
366
574
  * - Classification (existence + `importsChanged`) runs synchronously in
367
575
  * original array order first — identical to what each sequential
368
576
  * `checkFile()` call would do, including the `importCache` side effect.
369
577
  * - Only the actual madge spawns for import-changed ("miss") files run
370
578
  * concurrently; each one's parsed result stays LOCAL (no shared-state
371
579
  * write) until every spawn has settled.
372
- * - The shared `lastCircular`/`circularFiles` state is then folded by
373
- * replaying the miss results in ORIGINAL array order (not completion
374
- * order), so the final state — and each cache-hit file's result, which
375
- * reads the state as of its position in the array — is byte-for-byte
376
- * what the sequential last-write-wins loop would have produced. No
377
- * concurrent write can clobber another's, because none are applied
378
- * until after `Promise.all` resolves.
580
+ * - The miss results are then folded in ORIGINAL array order (not
581
+ * completion order) into a BATCH-LOCAL copy of the state, so each
582
+ * cache-hit file's result reads the state as of its own position in the
583
+ * array — byte-for-byte what the sequential last-write-wins loop
584
+ * returned. The fold is synchronous, so the instance-state write is one
585
+ * atomic assignment at the end; no concurrent spawn can interleave into
586
+ * it, and the returned map is unaffected by whether that write survives
587
+ * the rule below.
588
+ *
589
+ * ACROSS operations — newest classification wins:
590
+ * - `checkFile`, `checkFilesBatch` and `scanProject` all write the same
591
+ * shared state and genuinely overlap (turn-end vs. the background scans
592
+ * in runtime-session and project-diagnostics/fresh-fetch). Each takes a
593
+ * generation when it classifies, and `publishState` drops a write whose
594
+ * generation is no longer the newest — so an operation that started
595
+ * against OLDER file content cannot resurrect that view over a newer
596
+ * operation that already published, whatever order the spawns finish in.
597
+ * - ONLY the shared-state write is gated. Every per-file result handed back
598
+ * to the caller is built from that file's own spawn output, so a dropped
599
+ * publish never fabricates or hides a cycle for the caller.
600
+ * - No result cache is introduced (#533, "when in doubt, re-run"): changed
601
+ * content still re-runs madge every time, and the guard only skips writes
602
+ * that a newest-wins sequential interleaving would also have discarded.
379
603
  */
380
604
  async checkFilesBatch(filePaths, cwd) {
381
605
  const projectRoot = path.resolve(cwd || process.cwd());
382
606
  const results = new Map();
607
+ const gen = ++this.opGeneration;
383
608
  const entries = [];
384
609
  for (const file of filePaths) {
385
610
  const normalized = path.resolve(projectRoot, file);
@@ -398,44 +623,85 @@ export class DependencyChecker {
398
623
  checked: false,
399
624
  cacheHit: false,
400
625
  };
401
- if (missEntries.length > 0 && !(await this.ensureAvailable())) {
626
+ const stats = {
627
+ requested: filePaths.length,
628
+ missing: entries.filter((e) => e.kind === "missing").length,
629
+ cacheHits: entries.filter((e) => e.kind === "hit").length,
630
+ spawned: 0,
631
+ failed: 0,
632
+ resolveMs: 0,
633
+ targets: [],
634
+ targetsTruncated: false,
635
+ };
636
+ if (missEntries.length > 0 && !(await this.ensureAvailable(projectRoot))) {
402
637
  // madge unavailable: mirrors checkFile()'s "not available" branch for
403
638
  // every miss; hits still read whatever shared state already exists.
639
+ // The miss set counts as failed — reporting it as zero-of-everything
640
+ // would read as a clean turn that simply had nothing to do.
641
+ stats.failed = missEntries.length;
404
642
  for (const entry of entries) {
405
643
  if (entry.kind === "hit") {
406
- results.set(entry.file, this.buildCachedResult(entry.normalized));
644
+ results.set(entry.file, this.buildCachedResult(entry.normalized, this.lastCircular, this.circularFiles));
407
645
  }
408
646
  else {
409
647
  results.set(entry.file, notAvailableResult);
410
648
  }
411
649
  }
412
- return results;
650
+ return { results, stats };
413
651
  }
414
652
  // Run the concurrent (bounded) madge spawns for the miss set only. Each
415
653
  // result stays local — keyed by normalized path — until folded below.
416
654
  const spawnResults = new Map();
417
- await mapWithConcurrency(missEntries, MADGE_BATCH_CONCURRENCY, async (entry) => {
418
- spawnResults.set(entry.normalized, await this.runMadgeSpawn(entry.normalized, projectRoot));
419
- });
420
- // Fold in original order: each miss overwrites the shared state exactly
421
- // as the sequential loop would, and each hit is resolved against the
422
- // state as folded up to (but not past) its own position.
655
+ const spawnDurations = new Map();
656
+ if (missEntries.length > 0) {
657
+ const resolveStart = Date.now();
658
+ const resolved = await this.resolveMadge(projectRoot);
659
+ stats.resolveMs = Date.now() - resolveStart;
660
+ stats.commandKind = resolved.kind;
661
+ await mapWithConcurrency(missEntries, MADGE_BATCH_CONCURRENCY, async (entry) => {
662
+ stats.spawned++;
663
+ const startedAt = Date.now();
664
+ spawnResults.set(entry.normalized, await this.runMadgeSpawn(entry.normalized, projectRoot, resolved));
665
+ spawnDurations.set(entry.normalized, Date.now() - startedAt);
666
+ });
667
+ }
668
+ // Fold in original order into a batch-local view: each miss overwrites it
669
+ // exactly as the sequential loop would overwrite the shared state, and
670
+ // each hit is resolved against the view as folded up to (but not past)
671
+ // its own position.
672
+ let foldedCircular = this.lastCircular;
673
+ let foldedCircularFiles = this.circularFiles;
674
+ let folded = false;
423
675
  for (const entry of entries) {
424
676
  if (entry.kind === "missing") {
425
677
  results.set(entry.file, notAvailableResult);
426
678
  continue;
427
679
  }
428
680
  if (entry.kind === "hit") {
429
- results.set(entry.file, this.buildCachedResult(entry.normalized));
681
+ results.set(entry.file, this.buildCachedResult(entry.normalized, foldedCircular, foldedCircularFiles));
430
682
  continue;
431
683
  }
432
684
  const spawnResult = spawnResults.get(entry.normalized);
685
+ const durationMs = spawnDurations.get(entry.normalized) ?? 0;
686
+ if (durationMs >= MADGE_STATS_TARGET_MIN_DURATION_MS &&
687
+ stats.targets.length < MADGE_STATS_TARGET_CAP) {
688
+ stats.targets.push({
689
+ file: path.relative(projectRoot, entry.normalized),
690
+ durationMs,
691
+ ok: spawnResult?.ok === true,
692
+ });
693
+ }
694
+ else if (durationMs >= MADGE_STATS_TARGET_MIN_DURATION_MS) {
695
+ stats.targetsTruncated = true;
696
+ }
433
697
  if (!spawnResult || !spawnResult.ok) {
698
+ stats.failed++;
434
699
  results.set(entry.file, notAvailableResult);
435
700
  continue;
436
701
  }
437
- this.lastCircular = spawnResult.circular;
438
- this.circularFiles = spawnResult.circularFiles;
702
+ foldedCircular = spawnResult.circular;
703
+ foldedCircularFiles = spawnResult.circularFiles;
704
+ folded = true;
439
705
  results.set(entry.file, {
440
706
  hasCircular: spawnResult.circular.length > 0,
441
707
  circular: spawnResult.circular.filter((d) => d.file === entry.normalized || d.path.includes(entry.normalized)),
@@ -444,7 +710,13 @@ export class DependencyChecker {
444
710
  localSkips: spawnResult.localSkips,
445
711
  });
446
712
  }
447
- return results;
713
+ // A batch that learned nothing publishes nothing — advancing the
714
+ // generation on an unchanged view would suppress an older operation's
715
+ // still-legitimate write for free.
716
+ if (folded) {
717
+ this.publishState(gen, foldedCircular, foldedCircularFiles);
718
+ }
719
+ return { results, stats };
448
720
  }
449
721
  /**
450
722
  * Format circular dependency warning for LLM
@@ -473,19 +745,23 @@ export class DependencyChecker {
473
745
  if (!hasSourceFiles) {
474
746
  return { circular: [], count: 0 };
475
747
  }
476
- if (!(await this.ensureAvailable())) {
748
+ if (!(await this.ensureAvailable(projectRoot))) {
477
749
  return { circular: [], count: 0 };
478
750
  }
479
751
  const existing = this.scanInFlight.get(projectRoot);
480
752
  if (existing)
481
753
  return existing;
482
- const promise = this.runScanProject(projectRoot).finally(() => {
754
+ // A whole-project scan writes the same shared state a turn-end batch
755
+ // does, and the two overlap (runtime-session, fresh-fetch), so it takes a
756
+ // generation too — claimed before the spawn, as late as a scan can.
757
+ const gen = ++this.opGeneration;
758
+ const promise = this.runScanProject(projectRoot, gen).finally(() => {
483
759
  this.scanInFlight.delete(projectRoot);
484
760
  });
485
761
  this.scanInFlight.set(projectRoot, promise);
486
762
  return promise;
487
763
  }
488
- async runScanProject(projectRoot) {
764
+ async runScanProject(projectRoot, gen) {
489
765
  try {
490
766
  const { cmd, prefix } = await this.resolveMadge(projectRoot);
491
767
  const result = await safeSpawnAsync(cmd, [...prefix, ...buildMadgeArgs(projectRoot, projectRoot)], {
@@ -510,8 +786,7 @@ export class DependencyChecker {
510
786
  });
511
787
  }
512
788
  }
513
- this.lastCircular = circular;
514
- this.circularFiles = circularFiles;
789
+ this.publishState(gen, circular, circularFiles);
515
790
  return { circular, count: circular.length };
516
791
  }
517
792
  catch (err) {
@@ -540,3 +815,12 @@ export class DependencyChecker {
540
815
  return output;
541
816
  }
542
817
  }
818
+ /**
819
+ * Reset the madge managed-path memo on every live `DependencyChecker`.
820
+ * Free-function wrapper (matching `resetSafeSpawnWindowsCommandCache`'s
821
+ * shape) so callers reset without needing a checker instance in hand — see
822
+ * `DependencyChecker.resetMadgeManagedPathMemo` for why this exists (#1276).
823
+ */
824
+ export function resetMadgeManagedPathMemo() {
825
+ DependencyChecker.resetMadgeManagedPathMemo();
826
+ }