@herbertgao/pi-extensions 2026.8.3 → 2026.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +45 -4
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  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 +5 -8
  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 +8 -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 +15 -2
  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/schedule-menu.ts +9 -8
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  31. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  32. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  33. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  34. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  35. package/node_modules/pi-footer/LICENSE +21 -0
  36. package/node_modules/pi-footer/README.md +354 -0
  37. package/node_modules/pi-footer/package.json +68 -0
  38. package/node_modules/pi-footer/src/cache.ts +106 -0
  39. package/node_modules/pi-footer/src/colors.ts +237 -0
  40. package/node_modules/pi-footer/src/config.ts +202 -0
  41. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  42. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  43. package/node_modules/pi-footer/src/git.ts +135 -0
  44. package/node_modules/pi-footer/src/index.ts +268 -0
  45. package/node_modules/pi-footer/src/metrics.ts +141 -0
  46. package/node_modules/pi-footer/src/presets.ts +383 -0
  47. package/node_modules/pi-footer/src/render.ts +114 -0
  48. package/node_modules/pi-footer/src/separators.ts +52 -0
  49. package/node_modules/pi-footer/src/types.ts +122 -0
  50. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  51. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  52. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  53. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  54. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  55. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  56. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  57. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  58. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  59. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  60. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  61. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  62. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  63. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  64. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  65. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  66. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  67. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  68. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  69. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  70. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  71. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  72. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  73. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  74. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  75. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  76. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  77. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  78. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  79. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  80. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  81. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  82. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  83. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  84. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  85. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  86. package/node_modules/pi-footer/src/ui.ts +66 -0
  87. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  88. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  89. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  90. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  91. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  92. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  93. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  94. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  95. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  96. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  97. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  99. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  100. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  102. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  103. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  104. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  105. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  106. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  107. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  108. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  109. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  110. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  111. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  112. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  113. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  114. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  115. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  116. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  117. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  118. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  119. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  120. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  121. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  122. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  123. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  124. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  125. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  126. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  127. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  128. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  129. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  130. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  131. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  132. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  133. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  134. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  135. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  136. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  137. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  138. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  139. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  140. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  141. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  142. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  143. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  144. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  145. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  146. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  147. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  148. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  149. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  150. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  151. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  152. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  153. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  154. package/node_modules/pi-lens/README.md +13 -0
  155. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  156. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  157. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  158. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  160. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  161. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  162. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  163. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  164. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  165. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  166. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  167. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  168. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  169. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  170. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  171. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  172. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  173. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  174. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  175. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  177. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  178. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  179. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  180. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  181. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  182. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  183. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  184. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  185. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  186. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  187. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  188. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  189. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  190. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  191. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  192. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  193. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  194. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  195. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  196. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  197. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  198. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  199. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  201. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  202. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  203. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  204. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  205. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  206. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  207. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  208. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  209. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  210. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  212. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  213. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  214. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  215. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  216. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  217. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  218. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  219. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  220. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  221. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  222. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  223. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  224. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  225. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  226. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  227. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  228. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  229. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  230. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  231. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  232. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  233. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  234. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  235. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  236. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  237. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  238. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  239. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  240. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  241. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  242. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  243. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  244. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  245. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  246. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  247. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  248. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  249. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  250. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  251. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  252. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  253. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  254. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  255. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  256. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  257. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  258. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  259. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  260. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  261. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  262. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  263. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  264. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  265. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  266. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  267. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  268. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  269. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  270. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  271. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  272. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  273. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  274. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  275. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  277. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  278. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  279. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  280. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  281. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  282. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  283. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  284. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  285. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  286. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  287. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  288. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  289. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  290. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  291. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  292. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  293. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  294. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  295. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  296. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  297. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  298. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  299. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  300. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  301. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  302. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  303. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  304. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  305. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  306. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  307. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  308. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  309. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  310. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  311. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  312. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  313. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  314. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  315. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  316. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  317. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  318. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  319. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  320. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  321. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  322. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  323. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  324. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  325. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  326. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  327. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  328. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  329. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  330. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  331. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  332. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  333. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  334. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  335. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  336. package/node_modules/pi-lens/dist/index.js +49361 -36888
  337. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  338. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  339. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  340. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  341. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  342. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  343. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  344. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  345. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  346. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  347. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  348. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  349. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  350. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  351. package/node_modules/pi-lens/docs/fable.md +27 -0
  352. package/node_modules/pi-lens/docs/features.md +31 -2
  353. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  354. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  355. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  356. package/node_modules/pi-lens/docs/mcp.md +37 -4
  357. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  358. package/node_modules/pi-lens/docs/settings.md +12 -1
  359. package/node_modules/pi-lens/docs/tools.md +4 -1
  360. package/node_modules/pi-lens/docs/usage.md +18 -0
  361. package/node_modules/pi-lens/package.json +10 -10
  362. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  363. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  364. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  365. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  366. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  367. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  368. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  369. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  370. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  371. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  372. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  373. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  374. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  375. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  376. package/node_modules/pi-mcp-adapter/README.md +34 -3
  377. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  378. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  379. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  380. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  381. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  382. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  383. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  384. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  385. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  386. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  387. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  388. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  389. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  390. package/node_modules/pi-mcp-adapter/package.json +3 -3
  391. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  392. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  393. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  394. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  395. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  396. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  397. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  398. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  399. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  400. package/package.json +17 -13
  401. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  402. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  403. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  404. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  405. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  406. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  407. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  408. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -57,6 +57,8 @@
57
57
  * patterns apply to files inside that package, in addition to (and with
58
58
  * higher precedence than) the root config's `ignore` patterns.
59
59
  */
60
+ import { logExtension } from "./extension-log.js";
61
+ import { notifyUserDegradation } from "./user-notify.js";
60
62
  import * as fs from "node:fs";
61
63
  import * as os from "node:os";
62
64
  import * as path from "node:path";
@@ -107,11 +109,17 @@ export function loadPiLensProjectConfig(startDir, preloadedInfo = findPiLensProj
107
109
  if (!configInfo)
108
110
  return EMPTY_PROJECT_CONFIG;
109
111
  const cached = configCache.get(configInfo.path);
110
- if (cached && cached.mtimeMs === configInfo.mtimeMs) {
112
+ if (cached &&
113
+ cached.mtimeMs === configInfo.mtimeMs &&
114
+ cached.size === configInfo.size) {
111
115
  return cached.config;
112
116
  }
113
117
  const config = parseConfigFile(configInfo.path);
114
- configCache.set(configInfo.path, { mtimeMs: configInfo.mtimeMs, config });
118
+ configCache.set(configInfo.path, {
119
+ mtimeMs: configInfo.mtimeMs,
120
+ size: configInfo.size,
121
+ config,
122
+ });
115
123
  return config;
116
124
  }
117
125
  /** For tests + callers that need to force a re-read (e.g. config-watcher hooks). */
@@ -137,7 +145,12 @@ export function findPiLensConfigInDir(dir) {
137
145
  const marker = findPiLensConfigMarkerInDir(dir);
138
146
  if (!marker)
139
147
  return undefined;
140
- return { path: marker.path, dir: marker.dir, mtimeMs: marker.mtimeMs };
148
+ return {
149
+ path: marker.path,
150
+ dir: marker.dir,
151
+ mtimeMs: marker.mtimeMs,
152
+ size: marker.size,
153
+ };
141
154
  }
142
155
  /**
143
156
  * Find the closest config, between an edited file and the project root, that
@@ -174,11 +187,11 @@ export function loadPiLensConfigInDir(dir) {
174
187
  if (!info)
175
188
  return EMPTY_PROJECT_CONFIG;
176
189
  const cached = configCache.get(info.path);
177
- if (cached && cached.mtimeMs === info.mtimeMs) {
190
+ if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size) {
178
191
  return cached.config;
179
192
  }
180
193
  const config = parseConfigFile(info.path);
181
- configCache.set(info.path, { mtimeMs: info.mtimeMs, config });
194
+ configCache.set(info.path, { mtimeMs: info.mtimeMs, size: info.size, config });
182
195
  return config;
183
196
  }
184
197
  export function findPiLensProjectConfig(startDir) {
@@ -189,7 +202,7 @@ export function findPiLensProjectConfig(startDir) {
189
202
  return undefined;
190
203
  const stat = safeFileStat(cached.info.path);
191
204
  if (stat?.isFile())
192
- return { ...cached.info, mtimeMs: stat.mtimeMs };
205
+ return { ...cached.info, mtimeMs: stat.mtimeMs, size: stat.size };
193
206
  }
194
207
  const discovered = discoverPiLensProjectConfig(cacheKey);
195
208
  discoveryCache.set(cacheKey, discovered);
@@ -223,7 +236,7 @@ function discoverPiLensProjectConfig(startDir) {
223
236
  const stat = safeFileStat(candidate);
224
237
  if (stat?.isFile()) {
225
238
  return {
226
- info: { path: candidate, dir, mtimeMs: stat.mtimeMs },
239
+ info: { path: candidate, dir, mtimeMs: stat.mtimeMs, size: stat.size },
227
240
  dirMtimes,
228
241
  };
229
242
  }
@@ -236,7 +249,41 @@ function warnInvalidConfigOnce(configPath, reason) {
236
249
  if (warnedInvalidConfigs.has(key))
237
250
  return;
238
251
  warnedInvalidConfigs.add(key);
239
- console.error(`[pi-lens] ignoring invalid project config ${configPath}: ${reason}`);
252
+ const message = `ignoring invalid project config ${configPath}: ${reason}`;
253
+ logExtension({
254
+ subsystem: "project-lens-config",
255
+ level: "warn",
256
+ message,
257
+ metadata: { configPath, reason },
258
+ });
259
+ // HUMAN-audience too: the user's own `.pi-lens.json` is being ignored.
260
+ notifyUserDegradation(`pi-lens: ${message}`);
261
+ }
262
+ function parseRulePolicyList(configPath, ruleId, key, value) {
263
+ if (!Array.isArray(value)) {
264
+ warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must be an array of strings`);
265
+ return { list: [], invalid: true };
266
+ }
267
+ const list = [];
268
+ for (const entry of value) {
269
+ if (typeof entry !== "string")
270
+ continue;
271
+ const trimmed = entry.trim();
272
+ if (trimmed.length > 0)
273
+ list.push(trimmed);
274
+ }
275
+ if (list.length === 0) {
276
+ // #1087: an explicitly empty array (`"disable": []`) is a well-formed
277
+ // no-op, not an error — don't warn. Only warn when the array HAD entries
278
+ // but none were usable strings (all blank / non-string), which is a real
279
+ // authoring mistake that must not fail silently.
280
+ if (value.length > 0) {
281
+ warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must contain at least one non-empty string`);
282
+ return { list: [], invalid: true };
283
+ }
284
+ return { list: [], invalid: false };
285
+ }
286
+ return { list, invalid: false };
240
287
  }
241
288
  function parseConfigFile(configPath) {
242
289
  let raw;
@@ -264,18 +311,48 @@ function parseConfigFile(configPath) {
264
311
  if (obj.rules && typeof obj.rules === "object" && !Array.isArray(obj.rules)) {
265
312
  const rawRules = obj.rules;
266
313
  for (const [ruleId, ruleCfg] of Object.entries(rawRules)) {
314
+ // #444's own example writes the lists directly under `rules` (`rules.
315
+ // disable`), which lands here as an array and would otherwise be
316
+ // dropped without a word — the one shape a user is most likely to try.
267
317
  if (!ruleCfg || typeof ruleCfg !== "object" || Array.isArray(ruleCfg)) {
318
+ warnInvalidConfigOnce(configPath, `rules.${ruleId} must be an object with threshold, disable, or select; ignored`);
268
319
  continue;
269
320
  }
270
321
  const r = ruleCfg;
322
+ const entry = {};
271
323
  if (typeof r.threshold === "number" &&
272
324
  Number.isFinite(r.threshold) &&
273
325
  r.threshold > 0) {
274
- rules[ruleId] = { threshold: r.threshold };
326
+ entry.threshold = r.threshold;
275
327
  }
276
328
  else if ("threshold" in r) {
277
329
  warnInvalidConfigOnce(configPath, `rules.${ruleId}.threshold must be a positive finite number`);
278
330
  }
331
+ if ("disable" in r) {
332
+ const parsed = parseRulePolicyList(configPath, ruleId, "disable", r.disable);
333
+ // #1087: an explicitly empty list is valid-but-empty (no warning);
334
+ // don't store a pointless no-op entry for it.
335
+ if (!parsed.invalid && parsed.list.length > 0)
336
+ entry.disable = parsed.list;
337
+ }
338
+ if ("select" in r) {
339
+ const parsed = parseRulePolicyList(configPath, ruleId, "select", r.select);
340
+ if (!parsed.invalid && parsed.list.length > 0)
341
+ entry.select = parsed.list;
342
+ }
343
+ // Honor both threshold-only and policy-only entries; only drop if
344
+ // the entry had no recognized fields at all (e.g. { unrelated: true }).
345
+ // A recognized-but-malformed field already warned above, so only warn
346
+ // here when nothing recognized was spelled at all — #444 proposed
347
+ // `only` rather than `select`, and that typo must not fail silent.
348
+ if (entry.threshold !== undefined || entry.disable || entry.select) {
349
+ rules[ruleId] = entry;
350
+ }
351
+ else if (!("threshold" in r) &&
352
+ !("disable" in r) &&
353
+ !("select" in r)) {
354
+ warnInvalidConfigOnce(configPath, `rules.${ruleId} has no recognized setting (threshold, disable, select); ignored`);
355
+ }
279
356
  }
280
357
  }
281
358
  let maxProjectFiles;
@@ -34,8 +34,8 @@
34
34
  * into symbol-node metadata so this path can read it for free.
35
35
  */
36
36
  import * as path from "node:path";
37
- import { normalizeMapKey } from "./path-utils.js";
38
- import { loadProjectSnapshot } from "./project-snapshot.js";
37
+ import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
38
+ import { loadProjectSnapshotWithoutWordIndex } from "./project-snapshot.js";
39
39
  const DEFAULT_LIMIT = 10;
40
40
  const STALE_THRESHOLD_MS = 15 * 60_000; // 15 minutes
41
41
  const LOW_COVERAGE_THRESHOLD = 0.8;
@@ -47,14 +47,19 @@ function clampLimit(limit) {
47
47
  }
48
48
  // Same display-path convention as module-report.ts's toDisplayPath: cwd-relative
49
49
  // + forward-slashed under the project root, else the absolute (slash-normalized)
50
- // path.
50
+ // path. Delegates to the shape-aware toProjectRelativePath (#1163/#1194) instead
51
+ // of hand-rolling with host-default path.isAbsolute/path.relative, which
52
+ // mis-parses a Windows-shaped path (e.g. a persisted graph symbol-key path) on
53
+ // Linux CI: path.isAbsolute("C:\\repo\\x.ts") is false there, short-circuiting
54
+ // to the raw absolute path instead of relativizing (#1024 divergence class).
51
55
  function toDisplayPath(p, projectRoot) {
52
- if (!path.isAbsolute(p))
53
- return p.replace(/\\/g, "/");
54
- const rel = path.relative(projectRoot, p);
55
- return rel && !rel.startsWith("..")
56
- ? rel.replace(/\\/g, "/")
57
- : p.replace(/\\/g, "/");
56
+ return toProjectRelativePath(p, projectRoot);
57
+ }
58
+ /** Test-only export (refs #1194) so the shape-aware delegation can be verified
59
+ * directly without standing up a full review graph — mirrors the
60
+ * `_reset*ForTests` underscore convention used elsewhere in this file. */
61
+ export function _toDisplayPathForTests(p, projectRoot) {
62
+ return toDisplayPath(p, projectRoot);
58
63
  }
59
64
  function suggestedNext(displayPath) {
60
65
  return { tool: "module_report", path: displayPath };
@@ -110,9 +115,10 @@ function buildFileDegrees(graph) {
110
115
  // --- section 1: trust header --------------------------------------------------
111
116
  function computeTrust(graph, cwd) {
112
117
  const filesCovered = graph.fileNodes.size;
113
- const snapshot = loadProjectSnapshot(cwd);
118
+ const snapshot = loadProjectSnapshotWithoutWordIndex(cwd);
114
119
  const snapshotFileCount = snapshot ? Object.keys(snapshot.files).length : 0;
115
- const filesTotal = Math.max(filesCovered, snapshotFileCount);
120
+ const persistedTotal = graph.persistCoverage?.totalFiles ?? 0;
121
+ const filesTotal = Math.max(filesCovered, snapshotFileCount, persistedTotal);
116
122
  const coverage = filesTotal > 0 ? filesCovered / filesTotal : 1;
117
123
  let exact = 0;
118
124
  let imp = 0;
@@ -142,7 +148,8 @@ function computeTrust(graph, cwd) {
142
148
  const frac = (n) => (sampleSize > 0 ? n / sampleSize : 0);
143
149
  const ageMs = Date.now() - Date.parse(graph.builtAt);
144
150
  const stale = Number.isFinite(ageMs) && ageMs > STALE_THRESHOLD_MS;
145
- const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD || graph.persistCoverage?.partial === true;
151
+ const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD ||
152
+ graph.persistCoverage?.partial === true;
146
153
  const notes = [];
147
154
  if (stale) {
148
155
  const ageMin = Math.round(ageMs / 60_000);
@@ -151,7 +158,14 @@ function computeTrust(graph, cwd) {
151
158
  if (lowCoverage) {
152
159
  if (graph.persistCoverage?.partial) {
153
160
  const p = graph.persistCoverage;
154
- notes.push(`Partial persisted graph: ${p.persistedNodes}/${p.totalNodes} nodes and ${p.persistedEdges}/${p.totalEdges} edges were retained under the ${p.cap}-element cap — whole subsystems may be invisible below.`);
161
+ const totalFiles = p.totalFiles ?? filesCovered;
162
+ const persistedFiles = p.persistedFiles ?? filesCovered;
163
+ if (p.sourceFilesTruncated) {
164
+ notes.push(`Partial source walk: at least ${totalFiles} source files were represented before the visited-entry budget stopped enumeration; ${persistedFiles} files are in this graph.`);
165
+ }
166
+ else {
167
+ notes.push(`Partial persisted graph: ${p.persistedNodes}/${p.totalNodes} nodes, ${p.persistedEdges}/${p.totalEdges} edges, and ${persistedFiles}/${totalFiles} files were retained under the ${p.cap}-element cap — whole subsystems may be invisible below.`);
168
+ }
155
169
  }
156
170
  else {
157
171
  notes.push(`Low coverage: only ${filesCovered}/${filesTotal} project files (${Math.round(coverage * 100)}%) are in the graph — whole subsystems may be invisible below.`);
@@ -127,6 +127,12 @@ const SNAPSHOT_PARSE_CACHE_MAX = 4;
127
127
  // against the UNCOMPRESSED body size, never the (much smaller) gz file size.
128
128
  const SNAPSHOT_PARSE_CACHE_MAX_BYTES = 24 * 1024 * 1024;
129
129
  const snapshotParseCache = new Map();
130
+ function withoutWordIndex(snapshot) {
131
+ if (!snapshot?.wordIndex)
132
+ return snapshot;
133
+ const { wordIndex: _releasedPostings, ...stripped } = snapshot;
134
+ return stripped;
135
+ }
130
136
  function cacheParsedSnapshot(snapshotPath, entry) {
131
137
  // Refresh recency (Map preserves insertion order).
132
138
  snapshotParseCache.delete(snapshotPath);
@@ -139,11 +145,64 @@ function cacheParsedSnapshot(snapshotPath, entry) {
139
145
  }
140
146
  }
141
147
  const authoritativeSnapshots = new Map();
148
+ const PROJECT_SNAPSHOT_MAX_WARM_ROOTS = 8;
149
+ const PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
150
+ function projectSnapshotIdleEvictMs() {
151
+ const value = Number.parseInt(process.env.PI_LENS_PROJECT_SNAPSHOT_IDLE_EVICT_MS ?? "", 10);
152
+ return Number.isSafeInteger(value) && value > 0 ? value : PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT;
153
+ }
154
+ function clearAuthoritativeSnapshotTimer(entry) {
155
+ if (entry.idleTimer)
156
+ clearTimeout(entry.idleTimer);
157
+ entry.idleTimer = undefined;
158
+ }
159
+ function deleteAuthoritativeSnapshot(key) {
160
+ const entry = authoritativeSnapshots.get(key);
161
+ if (entry)
162
+ clearAuthoritativeSnapshotTimer(entry);
163
+ authoritativeSnapshots.delete(key);
164
+ }
165
+ function scheduleAuthoritativeSnapshotEviction(key, entry) {
166
+ clearAuthoritativeSnapshotTimer(entry);
167
+ const generation = entry.lastUsedAt;
168
+ entry.idleTimer = setTimeout(() => {
169
+ entry.idleTimer = undefined;
170
+ if (authoritativeSnapshots.get(key) !== entry || entry.lastUsedAt !== generation)
171
+ return;
172
+ deleteAuthoritativeSnapshot(key);
173
+ }, projectSnapshotIdleEvictMs());
174
+ entry.idleTimer.unref?.();
175
+ }
176
+ function touchAuthoritativeSnapshot(key, entry) {
177
+ entry.lastUsedAt = Date.now();
178
+ scheduleAuthoritativeSnapshotEviction(key, entry);
179
+ }
180
+ function enforceAuthoritativeSnapshotCap() {
181
+ while (authoritativeSnapshots.size > PROJECT_SNAPSHOT_MAX_WARM_ROOTS) {
182
+ const victim = [...authoritativeSnapshots.entries()].sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)[0];
183
+ if (!victim)
184
+ return;
185
+ clearAuthoritativeSnapshotTimer(victim[1]);
186
+ deleteAuthoritativeSnapshot(victim[0]);
187
+ }
188
+ }
189
+ /** Test-only cache keys, in LRU order from oldest to newest. */
190
+ export function _getAuthoritativeSnapshotCacheKeysForTests() {
191
+ return [...authoritativeSnapshots.entries()]
192
+ .sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)
193
+ .map(([key]) => key);
194
+ }
142
195
  /** Test hook: drop all cached parses + authoritative writes (per-worker isolation). */
143
196
  export function _resetProjectSnapshotParseCacheForTests() {
144
197
  snapshotParseCache.clear();
198
+ for (const entry of authoritativeSnapshots.values())
199
+ clearAuthoritativeSnapshotTimer(entry);
145
200
  authoritativeSnapshots.clear();
146
201
  }
202
+ /** Test hook: prove the parse-cache tier never owns serialized postings. */
203
+ export function _projectSnapshotParseCacheRetainsWordIndexForTests() {
204
+ return [...snapshotParseCache.values()].some((entry) => entry.snapshot?.wordIndex !== undefined);
205
+ }
147
206
  /** Resolve which body file is currently on disk: gz canonical, else legacy. */
148
207
  function resolveSnapshotBodyPath(cwd) {
149
208
  const gzPath = getProjectSnapshotPath(cwd);
@@ -219,7 +278,7 @@ function readSnapshotBody(bodyPath, gz) {
219
278
  return { snapshot: null, rawBytes: 0 };
220
279
  }
221
280
  }
222
- export function loadProjectSnapshot(cwd) {
281
+ function loadProjectSnapshotInternal(cwd, requireWordIndex) {
223
282
  const key = normalizeMapKey(cwd);
224
283
  const body = resolveSnapshotBodyPath(cwd);
225
284
  // Authoritative in-process write wins while our own (possibly still
@@ -230,11 +289,12 @@ export function loadProjectSnapshot(cwd) {
230
289
  if (authoritative) {
231
290
  const diskMtime = body ? body.mtimeMs : Number.NEGATIVE_INFINITY;
232
291
  if (diskMtime <= authoritative.knownMtime) {
292
+ touchAuthoritativeSnapshot(key, authoritative);
233
293
  return authoritative.snapshot;
234
294
  }
235
295
  // An external writer moved past our write — honor disk and stop
236
296
  // serving the now-stale in-memory object.
237
- authoritativeSnapshots.delete(key);
297
+ deleteAuthoritativeSnapshot(key);
238
298
  }
239
299
  if (!body) {
240
300
  snapshotParseCache.delete(getProjectSnapshotPath(cwd));
@@ -246,14 +306,21 @@ export function loadProjectSnapshot(cwd) {
246
306
  // SnapshotParseCacheEntry for why: coarse FAT/exFAT mtime resolution can
247
307
  // otherwise alias a just-rewritten file onto a stale cache entry).
248
308
  if (cached && cached.mtimeMs === body.mtimeMs && cached.size === body.size) {
249
- return cached.snapshot;
309
+ if (!requireWordIndex || !cached.snapshot || cached.snapshot.wordIndex) {
310
+ return cached.snapshot;
311
+ }
250
312
  }
251
313
  const { snapshot, rawBytes } = readSnapshotBody(body.path, body.gz);
252
- if (rawBytes > 0 && rawBytes <= SNAPSHOT_PARSE_CACHE_MAX_BYTES) {
314
+ // Serialized postings expand into a much larger object graph. Cache only a
315
+ // shallow postings-stripped body: metadata/report consumers stay warm while
316
+ // the live warm WordIndex remains the sole retained postings graph (#1370).
317
+ const cacheSnapshot = withoutWordIndex(snapshot);
318
+ if (rawBytes > 0 &&
319
+ (rawBytes <= SNAPSHOT_PARSE_CACHE_MAX_BYTES || snapshot?.wordIndex)) {
253
320
  cacheParsedSnapshot(cacheKey, {
254
321
  mtimeMs: body.mtimeMs,
255
322
  size: body.size,
256
- snapshot,
323
+ snapshot: cacheSnapshot,
257
324
  });
258
325
  }
259
326
  else {
@@ -261,11 +328,24 @@ export function loadProjectSnapshot(cwd) {
261
328
  }
262
329
  return snapshot;
263
330
  }
331
+ /** Load the canonical body, including serialized postings when present. */
332
+ export function loadProjectSnapshot(cwd) {
333
+ return loadProjectSnapshotInternal(cwd, true);
334
+ }
335
+ /**
336
+ * Load snapshot metadata without retaining or re-reading serialized postings.
337
+ * After publication this is served by the postings-stripped parse cache.
338
+ */
339
+ export function loadProjectSnapshotWithoutWordIndex(cwd) {
340
+ return loadProjectSnapshotInternal(cwd, false);
341
+ }
264
342
  const _snapshotGenerations = new Map();
265
343
  const _snapshotWorkerRequests = new Map();
266
344
  let _snapshotPersistWorker;
267
345
  let _snapshotWorkerRequestId = 0;
268
346
  let _snapshotWorkerDisabled = false;
347
+ let _snapshotGenerationGateEnabledForTests = true;
348
+ let _snapshotPromotionSeamForTests;
269
349
  let _lastSnapshotPersistErrorForTests;
270
350
  function snapshotWorkerEnabled() {
271
351
  // The synchronous fallback writer is a legitimate degraded mode (hosts that
@@ -281,7 +361,7 @@ function recordSnapshotPersistFailure(cwd, error) {
281
361
  // seq), and dropping the authoritative entry means the next load reflects
282
362
  // what is ACTUALLY on disk rather than the object we failed to persist.
283
363
  _lastSnapshotPersistErrorForTests = error;
284
- authoritativeSnapshots.delete(normalizeMapKey(cwd));
364
+ deleteAuthoritativeSnapshot(normalizeMapKey(cwd));
285
365
  logLatency({
286
366
  type: "phase",
287
367
  phase: "project_snapshot_persist_failed",
@@ -289,7 +369,8 @@ function recordSnapshotPersistFailure(cwd, error) {
289
369
  durationMs: 0,
290
370
  metadata: { error },
291
371
  });
292
- console.error("[project-snapshot] body persist failed:", error);
372
+ // #1333: the logLatency call above already carries this failure to
373
+ // latency.log — the console.error was a duplicate RAW write into pi's frame.
293
374
  }
294
375
  function logSnapshotPersistSuccess(pending, stats) {
295
376
  logLatency({
@@ -312,6 +393,33 @@ function reconcileAuthoritativeAfterWrite(pending, rawBytes) {
312
393
  // a superseding save already replaced it with a newer object.
313
394
  if (!entry || entry.snapshot !== pending.snapshot)
314
395
  return;
396
+ // The worker needs the serialized snapshot until promotion completes, but
397
+ // retaining it afterward duplicates the mutable warm index's postings. A
398
+ // shared reference is unsafe: ProjectSnapshot stores serialized arrays while
399
+ // WordIndex owns mutable Map/PathKeyedMap state. Drop the authoritative copy
400
+ // after publication; later merge-writers rehydrate the canonical disk body.
401
+ if (pending.snapshot.wordIndex) {
402
+ try {
403
+ const stat = fs.statSync(pending.gzPath);
404
+ cacheParsedSnapshot(pending.gzPath, {
405
+ mtimeMs: stat.mtimeMs,
406
+ size: stat.size,
407
+ snapshot: withoutWordIndex(pending.snapshot),
408
+ });
409
+ }
410
+ catch {
411
+ // A cache miss is safe: the first metadata consumer reconstructs it.
412
+ }
413
+ deleteAuthoritativeSnapshot(pending.key);
414
+ logLatency({
415
+ type: "phase",
416
+ phase: "project_snapshot_word_index_released",
417
+ filePath: pending.gzPath,
418
+ durationMs: 0,
419
+ metadata: { rawBytes },
420
+ });
421
+ return;
422
+ }
315
423
  if (rawBytes > SNAPSHOT_PARSE_CACHE_MAX_BYTES) {
316
424
  // Benign but invisible otherwise: the next load will re-parse this body
317
425
  // from disk instead of serving the in-process object.
@@ -322,7 +430,7 @@ function reconcileAuthoritativeAfterWrite(pending, rawBytes) {
322
430
  durationMs: 0,
323
431
  metadata: { rawBytes, maxBytes: SNAPSHOT_PARSE_CACHE_MAX_BYTES },
324
432
  });
325
- authoritativeSnapshots.delete(pending.key);
433
+ deleteAuthoritativeSnapshot(pending.key);
326
434
  return;
327
435
  }
328
436
  try {
@@ -378,6 +486,19 @@ function writeSnapshotBodyOnMainThread(pending, reason) {
378
486
  recordSnapshotPersistFailure(pending.cwd, err instanceof Error ? err.message : String(err));
379
487
  }
380
488
  }
489
+ /**
490
+ * The ForTests promotion seam covers worker-message and every main-thread
491
+ * fallback promotion. It stays sync and seam-free in production, identical to
492
+ * calling the writer directly. The process-exit hook is deliberately the sole
493
+ * direct write because an exit handler cannot await this asynchronous seam.
494
+ */
495
+ function dispatchMainThreadWriteThroughSeam(pending, reason) {
496
+ if (_snapshotPromotionSeamForTests) {
497
+ void _snapshotPromotionSeamForTests().then(() => writeSnapshotBodyOnMainThread(pending, reason));
498
+ return;
499
+ }
500
+ writeSnapshotBodyOnMainThread(pending, reason);
501
+ }
381
502
  function handleSnapshotWorkerResult(result) {
382
503
  const pending = _snapshotWorkerRequests.get(result.id);
383
504
  if (!pending) {
@@ -391,12 +512,15 @@ function handleSnapshotWorkerResult(result) {
391
512
  result.serializeMs === undefined ||
392
513
  result.writeMs === undefined) {
393
514
  fs.rm(result.stagePath, { force: true }, () => { });
394
- writeSnapshotBodyOnMainThread(pending, result.error ?? "invalid worker result");
515
+ dispatchMainThreadWriteThroughSeam(pending, result.error ?? "invalid worker result");
395
516
  return;
396
517
  }
397
518
  // Generation gate: a newer save already superseded this one — discard the
398
519
  // stale stage file rather than promote it over the fresher body.
399
- if (_snapshotGenerations.get(pending.key) !== result.generation) {
520
+ if (_snapshotGenerationGateEnabledForTests &&
521
+ _snapshotGenerations.get(pending.key) !== result.generation) {
522
+ // The stale stage is part of the promotion transaction: remove it before
523
+ // returning so a superseded save cannot leave an orphan behind.
400
524
  fs.rm(result.stagePath, { force: true }, () => { });
401
525
  return;
402
526
  }
@@ -414,7 +538,7 @@ function handleSnapshotWorkerResult(result) {
414
538
  }
415
539
  catch (err) {
416
540
  fs.rm(result.stagePath, { force: true }, () => { });
417
- writeSnapshotBodyOnMainThread(pending, err instanceof Error ? err.message : String(err));
541
+ dispatchMainThreadWriteThroughSeam(pending, err instanceof Error ? err.message : String(err));
418
542
  }
419
543
  }
420
544
  function handleSnapshotWorkerDeath(reason) {
@@ -423,7 +547,7 @@ function handleSnapshotWorkerDeath(reason) {
423
547
  const requests = [..._snapshotWorkerRequests.values()];
424
548
  _snapshotWorkerRequests.clear();
425
549
  for (const pending of requests)
426
- writeSnapshotBodyOnMainThread(pending, reason);
550
+ dispatchMainThreadWriteThroughSeam(pending, reason);
427
551
  }
428
552
  function resolveSnapshotPersistWorkerPath() {
429
553
  // esbuild does NOT rewrite new URL(...) asset refs, so from the bundled
@@ -460,8 +584,17 @@ function getSnapshotPersistWorker() {
460
584
  return undefined;
461
585
  }
462
586
  const worker = new Worker(workerPath);
463
- worker.unref();
464
- worker.on("message", handleSnapshotWorkerResult);
587
+ // The ForTests promotion seam wraps ONLY when set — the production path
588
+ // binds the sync handler directly, so scheduling is byte-identical when
589
+ // no test seam is installed (the async-handler variant of this shifted
590
+ // promotion timing under full-suite load and flaked the round-trip test).
591
+ worker.on("message", (result) => {
592
+ if (_snapshotPromotionSeamForTests) {
593
+ void _snapshotPromotionSeamForTests().then(() => handleSnapshotWorkerResult(result));
594
+ return;
595
+ }
596
+ handleSnapshotWorkerResult(result);
597
+ });
465
598
  worker.on("error", (err) => handleSnapshotWorkerDeath(err.message));
466
599
  worker.on("exit", (code) => {
467
600
  if (_snapshotPersistWorker === worker)
@@ -475,7 +608,7 @@ function getSnapshotPersistWorker() {
475
608
  if (stranded.length > 0) {
476
609
  _snapshotWorkerRequests.clear();
477
610
  for (const pending of stranded) {
478
- writeSnapshotBodyOnMainThread(pending, `persist worker exited with code ${code}`);
611
+ dispatchMainThreadWriteThroughSeam(pending, `persist worker exited with code ${code}`);
479
612
  }
480
613
  }
481
614
  // Only an ABNORMAL exit disables respawning; a clean idle exit (nothing
@@ -483,6 +616,9 @@ function getSnapshotPersistWorker() {
483
616
  if (code !== 0)
484
617
  _snapshotWorkerDisabled = true;
485
618
  });
619
+ // #1148: adding a message listener refs the Worker's public MessagePort.
620
+ // Unref only after every listener is installed so it stays background-only.
621
+ worker.unref();
486
622
  _snapshotPersistWorker = worker;
487
623
  return worker;
488
624
  }
@@ -585,7 +721,14 @@ export function saveProjectSnapshot(cwd, snapshot) {
585
721
  // legacy body to a merge-consumer, silently dropping this snapshot's fields.
586
722
  const priorBody = resolveSnapshotBodyPath(cwd);
587
723
  const knownMtime = priorBody ? priorBody.mtimeMs : Number.NEGATIVE_INFINITY;
588
- authoritativeSnapshots.set(key, { snapshot, knownMtime });
724
+ const authoritativeEntry = {
725
+ snapshot,
726
+ knownMtime,
727
+ lastUsedAt: Date.now(),
728
+ };
729
+ authoritativeSnapshots.set(key, authoritativeEntry);
730
+ scheduleAuthoritativeSnapshotEviction(key, authoritativeEntry);
731
+ enforceAuthoritativeSnapshotCap();
589
732
  // A stale disk-parse-cache entry for this path must not out-vote the fresh
590
733
  // authoritative write once the latter is dropped (oversized bodies).
591
734
  snapshotParseCache.delete(gzPath);
@@ -604,12 +747,12 @@ export function saveProjectSnapshot(cwd, snapshot) {
604
747
  sweepStaleSnapshotStageFiles(cacheDir);
605
748
  ensureSnapshotPersistExitHook();
606
749
  if (!snapshotWorkerEnabled()) {
607
- writeSnapshotBodyOnMainThread(pending);
750
+ dispatchMainThreadWriteThroughSeam(pending, undefined);
608
751
  return;
609
752
  }
610
753
  const worker = getSnapshotPersistWorker();
611
754
  if (!worker) {
612
- writeSnapshotBodyOnMainThread(pending, "persist worker unavailable");
755
+ dispatchMainThreadWriteThroughSeam(pending, "persist worker unavailable");
613
756
  return;
614
757
  }
615
758
  const id = ++_snapshotWorkerRequestId;
@@ -640,7 +783,7 @@ export function flushProjectSnapshotPersistsForTests() {
640
783
  for (const pending of requests) {
641
784
  if (_snapshotGenerations.get(pending.key) !== pending.generation)
642
785
  continue;
643
- writeSnapshotBodyOnMainThread(pending);
786
+ dispatchMainThreadWriteThroughSeam(pending, undefined);
644
787
  }
645
788
  }
646
789
  /** Test-only: exercise the degraded worker-death path. */
@@ -652,11 +795,21 @@ export async function terminateProjectSnapshotPersistWorkerForTests() {
652
795
  /** Test-only: restore worker creation + clear generation state after a deliberate death. */
653
796
  export function resetProjectSnapshotPersistWorkerForTests() {
654
797
  _snapshotWorkerDisabled = false;
798
+ _snapshotGenerationGateEnabledForTests = true;
799
+ _snapshotPromotionSeamForTests = undefined;
655
800
  _snapshotPersistWorker = undefined;
656
801
  _snapshotWorkerRequests.clear();
657
802
  _snapshotGenerations.clear();
658
803
  _lastSnapshotPersistErrorForTests = undefined;
659
804
  }
805
+ /** Test-only mutation switch for proving the supersession invariant. */
806
+ export function setProjectSnapshotGenerationGateForTests(enabled) {
807
+ _snapshotGenerationGateEnabledForTests = enabled;
808
+ }
809
+ /** Test-only seam immediately before generation-gated promotion. */
810
+ export function setProjectSnapshotPromotionSeamForTests(seam) {
811
+ _snapshotPromotionSeamForTests = seam;
812
+ }
660
813
  export function getProjectSnapshotPersistErrorForTests() {
661
814
  return _lastSnapshotPersistErrorForTests;
662
815
  }
@@ -730,7 +883,9 @@ export function saveRuntimeProjectSnapshot(args) {
730
883
  // isProjectSnapshotFresh on load, seq mismatch) would get silently
731
884
  // re-stamped with the CURRENT seq by this save, "laundering" a stale
732
885
  // index into looking fresh before the word-index task even runs.
733
- if (!snapshot.wordIndex && existing.wordIndex && existing.seq === snapshot.seq) {
886
+ if (!snapshot.wordIndex &&
887
+ existing.wordIndex &&
888
+ existing.seq === snapshot.seq) {
734
889
  snapshot.wordIndex = existing.wordIndex;
735
890
  }
736
891
  }