@herbertgao/pi-extensions 2026.8.4 → 2026.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +4 -4
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  13. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  14. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  15. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  16. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  20. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  21. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  22. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  30. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  31. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  32. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  33. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  34. package/node_modules/pi-lens/README.md +13 -0
  35. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  36. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  37. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  39. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  40. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  41. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  42. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  43. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  44. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  45. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  46. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  47. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  51. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  52. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  53. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  54. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  56. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  57. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  58. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  59. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  60. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  63. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  64. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  66. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  67. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  68. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  69. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  70. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  71. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  72. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  73. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  75. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  99. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  100. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  101. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  102. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  103. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  104. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  105. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  106. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  107. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  108. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  109. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  110. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  111. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  112. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  113. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  114. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  116. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  117. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  118. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  120. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  121. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  122. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  123. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  124. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  125. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  126. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  127. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  128. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  129. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  130. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  131. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  132. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  134. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  135. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  136. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  137. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  138. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  139. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  140. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  141. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  142. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  143. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  144. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  145. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  146. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  147. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  148. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  149. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  150. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  151. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  152. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  153. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  154. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  155. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  157. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  158. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  159. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  160. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  161. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  162. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  163. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  164. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  165. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  166. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  167. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  168. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  169. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  170. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  171. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  172. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  173. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  174. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  175. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  177. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  178. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  179. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  180. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  181. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  182. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  183. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  184. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  185. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  186. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  187. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  188. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  189. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  190. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  191. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  192. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  193. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  194. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  196. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  197. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  198. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  199. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  200. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  201. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  202. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  203. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  204. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  205. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  210. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  211. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  212. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  213. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  214. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  215. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  216. package/node_modules/pi-lens/dist/index.js +49361 -36888
  217. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  218. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  219. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  220. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  221. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  222. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  223. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  224. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  225. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  226. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  227. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  228. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  229. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  230. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  231. package/node_modules/pi-lens/docs/fable.md +27 -0
  232. package/node_modules/pi-lens/docs/features.md +31 -2
  233. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  234. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  235. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  236. package/node_modules/pi-lens/docs/mcp.md +37 -4
  237. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  238. package/node_modules/pi-lens/docs/settings.md +12 -1
  239. package/node_modules/pi-lens/docs/tools.md +4 -1
  240. package/node_modules/pi-lens/docs/usage.md +18 -0
  241. package/node_modules/pi-lens/package.json +10 -10
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  252. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  253. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  254. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  255. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  256. package/node_modules/pi-mcp-adapter/README.md +34 -3
  257. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  258. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  259. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  260. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  261. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  262. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  263. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  264. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  265. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  266. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  267. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  268. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  269. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  270. package/node_modules/pi-mcp-adapter/package.json +3 -3
  271. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  272. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  273. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  274. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  275. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  276. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  277. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  278. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  279. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  280. package/package.json +11 -12
  281. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  282. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  283. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  284. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  285. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  286. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  287. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  288. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -3,30 +3,36 @@ import * as path from "node:path";
3
3
  import { deadCodeIssueCount } from "./dead-code-client.js";
4
4
  import { logDeadCodeScan } from "./dead-code-logger.js";
5
5
  import { getDiagnosticTracker } from "./diagnostic-tracker.js";
6
+ import { resetDegradationLedger } from "./degradation-ledger.js";
7
+ import { resetDispatchAvailabilityState } from "./dispatch/runners/utils/runner-helpers.js";
6
8
  import { clearAllSessions as clearFileTimeSessions } from "./file-time.js";
7
- import { getKnipIgnorePatterns } from "./file-utils.js";
8
- import { clearTsconfigPathsCache } from "./review-graph/tsconfig-paths.js";
9
- import { resetSafeSpawnWindowsCommandCache } from "./safe-spawn.js";
10
- import { resetWorkspaceTopology } from "./workspace-topology.js";
9
+ import { createDeadline, yieldIfOverBudget } from "./cooperative-budget.js";
10
+ import { getGlobalPiLensDir, getKnipIgnorePatterns, getProjectDataDir, } from "./file-utils.js";
11
11
  import { GitleaksClient } from "./gitleaks-client.js";
12
- import { TrivyClient } from "./trivy-client.js";
13
12
  import { GovulncheckClient, } from "./govulncheck-client.js";
14
13
  import { canRunStartupHeavyScans } from "./language-policy.js";
15
14
  import { detectProjectLanguageProfile, getDefaultStartupTools, } from "./language-profile.js";
16
15
  import { logLatency } from "./latency-logger.js";
17
- import { logWordIndex } from "./word-index-logger.js";
18
16
  import { runLogCleanup } from "./log-cleanup.js";
19
- import { setSessionLanguages } from "./widget-state.js";
20
17
  import { initLSPConfig, loadLSPConfig } from "./lsp/config.js";
21
- import { getLSPService } from "./lsp/index.js";
22
- import { readLatestProjectSequence, } from "./project-changes.js";
18
+ import { loadLspService } from "./lsp-lazy.js";
19
+ import { isAtOrAboveHomeDir } from "./path-utils.js";
20
+ import { isPrintMode } from "./print-mode.js";
21
+ import { readLatestProjectSequence, readLatestProjectSequenceAsync, } from "./project-changes.js";
23
22
  import { getProjectSnapshotPath, hydrateRuntimeFromProjectSnapshot, isProjectSnapshotFresh, isProjectSnapshotMetaStale, loadProjectSnapshot, PROJECT_SNAPSHOT_VERSION, readProjectSnapshotMeta, saveRuntimeProjectSnapshot, } from "./project-snapshot.js";
23
+ import { clearTsconfigPathsCache } from "./review-graph/tsconfig-paths.js";
24
24
  import { scanProjectRules } from "./rules-scanner.js";
25
- import { isAtOrAboveHomeDir } from "./path-utils.js";
25
+ import { resetSafeSpawnWindowsCommandCache } from "./safe-spawn.js";
26
26
  import { getSlowFsVerdict, isSlowFs, slowFsDegradationNotice, } from "./slow-fs.js";
27
- import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
27
+ import { countRecentSmells, formatSmellsSessionStartLine, resetSmellsSessionState, } from "./smells-rollup.js";
28
28
  import { findNearestProjectRoot, getStartupScanMaxEntries, isStartupScanVerdictFresh, resolveStartupScanContext, } from "./startup-scan.js";
29
+ import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
30
+ import { sweepAtomicWriteStages } from "./instance-reaper.js";
31
+ import { TrivyClient } from "./trivy-client.js";
29
32
  import { isWarmAttached } from "./warm-attach.js";
33
+ import { setSessionLanguages } from "./widget-state.js";
34
+ import { logWordIndex } from "./word-index-logger.js";
35
+ import { resetWorkspaceTopology } from "./workspace-topology.js";
30
36
  function resolveSnapshotRoot(cwd) {
31
37
  const resolvedCwd = path.resolve(cwd);
32
38
  const nearest = findNearestProjectRoot(resolvedCwd);
@@ -106,20 +112,135 @@ function resolveStartupMode() {
106
112
  if (envMode === "full" || envMode === "minimal" || envMode === "quick") {
107
113
  return envMode;
108
114
  }
109
- const argv = process.argv;
110
- if (argv.includes("--print") || argv.includes("-p")) {
115
+ if (isPrintMode()) {
111
116
  return "quick";
112
117
  }
113
118
  return "full";
114
119
  }
115
120
  // --- Session-start helpers ---
121
+ // #1162: bound the session_start sequence read to this budget. The default
122
+ // mirrors the issue's ~250ms figure; overridable for tests (mirrors the
123
+ // `PI_LENS_WARMUP_DELAY_MS` precedent above) so a regression can use a tiny
124
+ // budget instead of racing a real 250ms wall-clock wait.
125
+ function sequenceReadBudgetMs() {
126
+ const raw = Number(process.env.PI_LENS_SEQUENCE_READ_BUDGET_MS ?? 250);
127
+ return Number.isFinite(raw) && raw > 0 ? raw : 250;
128
+ }
129
+ // #1162 review follow-up (P3): the sentinel used ONLY for the snapshot
130
+ // freshness check (`isProjectSnapshotFresh`/`isProjectSnapshotMetaStale`,
131
+ // both `=== currentProjectSeq` equality) when a sequence read timed out.
132
+ // Every real `projectSeq`/`snapshot.seq` is >= 0 (both derive from
133
+ // `Math.max(0, ...)`/a monotonic fold starting at 0), so this value can
134
+ // never collide with a legitimate seq — including a project's real
135
+ // first-ever snapshot persisted at `seq === 0`, which the cold-seed's OWN
136
+ // `projectSeq: 0` would otherwise alias. Deliberately kept separate from
137
+ // `runtime.projectSeq` (which a timed-out read still seeds to plain `0`):
138
+ // the reseed-advancement guard on the deferred continuation below needs
139
+ // `runtime.projectSeq` to read exactly 0 immediately after a cold seed so
140
+ // `> 0` reliably means "a bump happened in the stall window", which this
141
+ // sentinel is never fed into.
142
+ const UNKNOWN_PROJECT_SEQ = -1;
143
+ /**
144
+ * Bound the sequence read that gates snapshot freshness (#1162). Normally
145
+ * ~2ms, but the underlying read is a blocking `fs.readFileSync` that can
146
+ * balloon under host I/O pressure (2125ms observed) with NO escape hatch —
147
+ * and a `setTimeout`/`Promise.race` timeout can never preempt a *synchronous*
148
+ * read (the thread only returns to the event loop once the OS call
149
+ * returns). The fix is to read asynchronously (`readLatestProjectSequenceAsync`,
150
+ * which uses `fs.promises.readFile` and genuinely yields) so a timeout CAN
151
+ * win the race.
152
+ *
153
+ * On the healthy path (the overwhelming common case) the async read settles
154
+ * first and this adds ~zero overhead beyond one microtask hop. On a stalled
155
+ * read, the timeout wins: the caller proceeds immediately with a cold
156
+ * sequence (`{ projectSeq: 0, fileSeqByPath: empty }` — byte-identical to
157
+ * what a full replay of a missing/empty log already produces, so this is
158
+ * exactly the existing cold-start case, not a new code path) while the real
159
+ * read keeps running in the background. When it resolves, it re-seeds the
160
+ * runtime so the warm-start optimization is recovered for the NEXT
161
+ * session_start/turn instead of lost for the rest of the process — unless
162
+ * the session has since moved on (`runtime.isCurrentSession`) or this is a
163
+ * one-shot `pi --print` process (`isPrintMode()`), which has no future
164
+ * session in-process to benefit and where letting background work outlive
165
+ * settle is exactly the referenced-handle retention class #1154/#1153 just
166
+ * closed. The timeout timer itself is `.unref()`'d and always cleared on
167
+ * both race outcomes, so it never holds the process open either.
168
+ *
169
+ * Never silent (shape 10): the caller's `session_start_sequence_read`
170
+ * latency line always carries `timedOut`, and a successful deferred reseed
171
+ * logs its own `session_start_sequence_read_deferred_reseed` phase.
172
+ */
173
+ async function readSequenceWithBudget(args) {
174
+ const { snapshotRoot, base, cwd, runtime, sessionGeneration, dbg } = args;
175
+ const readPromise = readLatestProjectSequenceAsync(snapshotRoot, base);
176
+ let timeoutHandle;
177
+ const timeoutPromise = new Promise((resolve) => {
178
+ timeoutHandle = setTimeout(() => resolve({ timedOut: true }), sequenceReadBudgetMs());
179
+ timeoutHandle.unref();
180
+ });
181
+ const raced = await Promise.race([
182
+ readPromise.then((latestSeq) => ({ timedOut: false, latestSeq })),
183
+ timeoutPromise,
184
+ ]);
185
+ if (timeoutHandle)
186
+ clearTimeout(timeoutHandle);
187
+ if (!raced.timedOut) {
188
+ return { latestSeq: raced.latestSeq, timedOut: false };
189
+ }
190
+ dbg(`session_start: sequence read exceeded ${sequenceReadBudgetMs()}ms budget — falling back to cold-start sequence`);
191
+ if (!isPrintMode()) {
192
+ readPromise
193
+ .then((latestSeq) => {
194
+ if (!runtime.isCurrentSession(sessionGeneration))
195
+ return;
196
+ // #1162 review follow-up (P3): `isCurrentSession` alone catches a
197
+ // CROSS-session move-on but not a SAME-session advancement in the
198
+ // stall window. Interleaving: timeout -> cold seed sets
199
+ // projectSeq=0 -> handler returns -> agent edits file X ->
200
+ // `bumpFileSeq(X)` advances `_projectSeq`/`_fileSeq[X]` -> this
201
+ // deferred read completes. Reseeding here would blindly `.clear()`
202
+ // and repopulate `_fileSeq`/`_fileLastProjectSeq` from the STALE
203
+ // pre-edit snapshot, erasing the in-window bump for file X (a
204
+ // transient inconsistency that self-heals into a full sweep per
205
+ // `seedProjectSequence`'s empty-changed-map invariant — safe, but
206
+ // worth not doing). The cold seed always sets `projectSeq` to
207
+ // exactly 0, so `runtime.projectSeq > 0` here can ONLY be true if a
208
+ // `bumpFileSeq` ran since — i.e. an in-window edit — never a false
209
+ // positive from a legitimately-empty log (which leaves it at 0, and
210
+ // the reseed below is then still wanted: it recovers the real,
211
+ // possibly non-empty, result that arrived too late for the budget).
212
+ if (runtime.projectSeq > 0) {
213
+ dbg("session_start: deferred sequence read completed, but the session already advanced (in-window edit) — skipping reseed to avoid clobbering it");
214
+ return;
215
+ }
216
+ const reseedStartedAt = Date.now();
217
+ runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
218
+ logLatency({
219
+ type: "phase",
220
+ phase: "session_start_sequence_read_deferred_reseed",
221
+ filePath: cwd,
222
+ startedAt: new Date(reseedStartedAt).toISOString(),
223
+ durationMs: Date.now() - reseedStartedAt,
224
+ metadata: { entries: latestSeq.fileSeqByPath.size, deferred: true },
225
+ });
226
+ dbg(`session_start: deferred sequence read completed — reseeded projectSeq=${latestSeq.projectSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
227
+ })
228
+ .catch((err) => {
229
+ dbg(`session_start: deferred sequence read failed: ${err}`);
230
+ });
231
+ }
232
+ return {
233
+ latestSeq: { projectSeq: 0, fileSeqByPath: new Map() },
234
+ timedOut: true,
235
+ };
236
+ }
116
237
  async function igniteWarmFiles(cwd, warmFiles, runtime, sessionGeneration, dbg) {
117
238
  try {
118
239
  dbg(`session_start lsp-warm: ${warmFiles.length} warm file(s) configured`);
119
240
  await initLSPConfig(cwd);
120
241
  if (!runtime.isCurrentSession(sessionGeneration))
121
242
  return;
122
- const lspService = getLSPService();
243
+ const lspService = (await loadLspService()).getLSPService();
123
244
  const total = warmFiles.length;
124
245
  let loaded = 0;
125
246
  let errors = 0;
@@ -170,7 +291,7 @@ async function igniteDominantLanguageWarm(cwd, runtime, sessionGeneration, dbg)
170
291
  await initLSPConfig(cwd);
171
292
  if (!runtime.isCurrentSession(sessionGeneration))
172
293
  return;
173
- const lspService = getLSPService();
294
+ const lspService = (await loadLspService()).getLSPService();
174
295
  const { collectSourceFilesAsync } = await import("./source-filter.js");
175
296
  const { CODE_KINDS, detectFileKind } = await import("./file-kinds.js");
176
297
  // Async, event-loop-yielding walk (deferred off the interactive path).
@@ -307,14 +428,14 @@ async function collectTodoBaselineItems(scanner, analysisRoot, stillCurrent) {
307
428
  const files = await getSourceFilesAsync(analysisRoot, true);
308
429
  if (!stillCurrent())
309
430
  return items;
310
- let processedSinceYield = 0;
431
+ const deadline = createDeadline(8);
311
432
  for (const file of files) {
312
433
  if (!stillCurrent())
313
434
  return items;
314
435
  scanOneTodoFile(scanner, file, items);
315
- if (++processedSinceYield % 30 === 0) {
316
- await new Promise((resolve) => setImmediate(resolve));
317
- }
436
+ // scanFile cost scales with the file contents, so a count cadence cannot
437
+ // bound the event-loop block across differently-sized corpora.
438
+ await yieldIfOverBudget(deadline);
318
439
  }
319
440
  }
320
441
  catch {
@@ -328,6 +449,7 @@ async function buildOrRefreshWordIndex(args) {
328
449
  if (!runtime.isCurrentSession(sessionGeneration))
329
450
  return;
330
451
  const startMs = Date.now();
452
+ let rebuildPreflightFiles;
331
453
  const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
332
454
  const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
333
455
  const snapshot = loadProjectSnapshot(snapshotRoot);
@@ -339,35 +461,55 @@ async function buildOrRefreshWordIndex(args) {
339
461
  const result = await refreshWordIndexIncrementally(index, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
340
462
  if (!runtime.isCurrentSession(sessionGeneration))
341
463
  return;
342
- runtime.wordIndex = index;
343
- // A stale project seq must be advanced even when mtimes prove every
344
- // indexed document reusable. Fresh snapshots with no changes avoid
345
- // an unnecessary rewrite of the large shared snapshot.
346
- if (!isProjectSnapshotFresh(snapshot, effectiveSeq) ||
347
- result.refreshed > 0 ||
348
- result.dropped > 0 ||
349
- snapshot.wordIndex.truncated !== index.truncated) {
350
- saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
464
+ if (result.mode === "full-required") {
465
+ rebuildPreflightFiles = result.preflightFiles;
466
+ dbg(`session_start word-index: incremental preflight selected full rebuild (${result.reason})`);
467
+ logWordIndex({
468
+ phase: "incremental_fallback",
469
+ cwd: snapshotRoot,
470
+ trigger: "session_start",
471
+ reason: result.reason,
472
+ });
473
+ }
474
+ else {
475
+ runtime.wordIndex = index;
476
+ // A stale project seq must be advanced even when mtimes prove every
477
+ // indexed document reusable. Fresh snapshots with no changes avoid
478
+ // an unnecessary rewrite of the large shared snapshot.
479
+ if (!isProjectSnapshotFresh(snapshot, effectiveSeq) ||
480
+ result.refreshed > 0 ||
481
+ result.dropped > 0 ||
482
+ snapshot.wordIndex.truncated !== index.truncated) {
483
+ saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
484
+ }
485
+ dbg(`session_start word-index: incremental (seq=${effectiveSeq}, refreshed=${result.refreshed}, dropped=${result.dropped}, skipped=${result.skipped}, reused=${result.reused}, ${Date.now() - startMs}ms)`);
486
+ // M2, #958: the incremental-vs-full decision + honest coverage
487
+ // (indexedFileCount/truncated/skipped), independent of host `dbg`.
488
+ logWordIndex({
489
+ phase: "incremental_refresh",
490
+ cwd: snapshotRoot,
491
+ trigger: "session_start",
492
+ durationMs: Date.now() - startMs,
493
+ indexedFileCount: index.docCount,
494
+ tokens: index.postings.size,
495
+ truncated: index.truncated,
496
+ refreshed: result.refreshed,
497
+ dropped: result.dropped,
498
+ skipped: result.skipped,
499
+ reused: result.reused,
500
+ });
501
+ return result;
351
502
  }
352
- dbg(`session_start word-index: incremental (seq=${effectiveSeq}, refreshed=${result.refreshed}, dropped=${result.dropped}, skipped=${result.skipped}, reused=${result.reused}, ${Date.now() - startMs}ms)`);
353
- // M2, #958: the incremental-vs-full decision + honest coverage
354
- // (indexedFileCount/truncated/skipped), independent of host `dbg`.
355
- logWordIndex({
356
- phase: "incremental_refresh",
357
- cwd: snapshotRoot,
358
- trigger: "session_start",
359
- durationMs: Date.now() - startMs,
360
- indexedFileCount: index.docCount,
361
- tokens: index.postings.size,
362
- truncated: index.truncated,
363
- refreshed: result.refreshed,
364
- dropped: result.dropped,
365
- skipped: result.skipped,
366
- reused: result.reused,
367
- });
368
- return result;
369
503
  }
370
504
  catch (err) {
505
+ // Supersession is an expected transition, not a failure: a newer
506
+ // session_start bumped the generation mid-refresh. Return undefined the
507
+ // way master's synchronous path did instead of reporting a fallback and
508
+ // re-walking for a rebuild whose result this generation may not publish.
509
+ if (!runtime.isCurrentSession(sessionGeneration)) {
510
+ dbg("session_start word-index: incremental refresh superseded");
511
+ return;
512
+ }
371
513
  dbg(`session_start word-index: incremental refresh failed; falling back to full rebuild (${err})`);
372
514
  logWordIndex({
373
515
  phase: "incremental_fallback",
@@ -382,11 +524,30 @@ async function buildOrRefreshWordIndex(args) {
382
524
  // implementation backs this task, the quick-mode warmup call below, AND the
383
525
  // stateless cold-query background trigger in word-index.ts — a bound/skip
384
526
  // -rule change lands once, not in three copies.
385
- const { buildWordIndex, collectWordIndexDocs } = await import("./word-index.js");
386
- const docs = await collectWordIndexDocs(analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
527
+ const { buildWordIndexAsync, collectWordIndexDocs } = await import("./word-index.js");
528
+ const docs = await collectWordIndexDocs(analysisRoot, () => runtime.isCurrentSession(sessionGeneration), rebuildPreflightFiles);
387
529
  if (!runtime.isCurrentSession(sessionGeneration))
388
530
  return;
389
- runtime.wordIndex = buildWordIndex(docs);
531
+ // #1197 review finding 2: `buildWordIndexAsync` THROWS on supersession, and
532
+ // the quick-mode warmup caller (below) awaits this function OUTSIDE any
533
+ // per-step try/catch — an escaping throw there skips the rest of warmup,
534
+ // including the #947 LSP pre-warm, and resets `__piLensWarmupScheduled`.
535
+ // Supersession is an expected transition (master's synchronous path just
536
+ // returned), so absorb it here and let the caller continue.
537
+ let rebuiltIndex;
538
+ try {
539
+ rebuiltIndex = await buildWordIndexAsync(docs, () => runtime.isCurrentSession(sessionGeneration));
540
+ }
541
+ catch (err) {
542
+ if (!runtime.isCurrentSession(sessionGeneration)) {
543
+ dbg("session_start word-index: rebuild superseded");
544
+ return;
545
+ }
546
+ throw err;
547
+ }
548
+ if (!runtime.isCurrentSession(sessionGeneration))
549
+ return;
550
+ runtime.wordIndex = rebuiltIndex;
390
551
  saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
391
552
  dbg(`session_start word-index: rebuilt (absent/stale, seq=${effectiveSeq}) ` +
392
553
  `${runtime.wordIndex.docCount} files, ${runtime.wordIndex.postings.size} tokens (${Date.now() - startMs}ms)`);
@@ -433,29 +594,38 @@ function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, sn
433
594
  const queuedAt = Date.now();
434
595
  dbg(`session_start task ${name}: scheduled`);
435
596
  runtime.markStartupScanInFlight(name, sessionGeneration);
436
- const fire = () => {
437
- const startedAt = Date.now();
438
- dbg(`session_start task ${name}: start queuedMs=${startedAt - queuedAt}`);
439
- void task()
440
- .then(() => {
441
- dbg(`session_start task ${name}: success runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
442
- })
443
- .catch((err) => {
444
- dbg(`session_start: ${name} background scan failed: ${err}`);
445
- dbg(`session_start task ${name}: failed runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
446
- })
447
- .finally(() => {
448
- runtime.clearStartupScanInFlight(name, sessionGeneration);
449
- dbg(`session_start task ${name}: end`);
450
- });
451
- };
452
- const delay = taskDeferMsByName[name] ?? 0;
453
- if (delay > 0) {
454
- setTimeout(fire, delay);
455
- }
456
- else {
457
- setImmediate(fire);
458
- }
597
+ const completion = new Promise((resolve) => {
598
+ const fire = () => {
599
+ const startedAt = Date.now();
600
+ dbg(`session_start task ${name}: start queuedMs=${startedAt - queuedAt}`);
601
+ void task()
602
+ .then(() => {
603
+ dbg(`session_start task ${name}: success runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
604
+ })
605
+ .catch((err) => {
606
+ dbg(`session_start: ${name} background scan failed: ${err}`);
607
+ dbg(`session_start task ${name}: failed runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
608
+ })
609
+ .finally(() => {
610
+ runtime.clearStartupScanInFlight(name, sessionGeneration);
611
+ dbg(`session_start task ${name}: end`);
612
+ resolve();
613
+ });
614
+ };
615
+ const delay = taskDeferMsByName[name] ?? 0;
616
+ if (delay > 0) {
617
+ // #1154: unref to match the repo-wide convention that every
618
+ // background timer is `.unref()`'d. Full-mode deferred scans only
619
+ // run in long-lived/interactive sessions today (never a one-shot),
620
+ // so this is defensive — but a referenced 5s timer would keep a
621
+ // process open were a full-mode one-shot ever introduced.
622
+ setTimeout(fire, delay).unref?.();
623
+ }
624
+ else {
625
+ setImmediate(fire);
626
+ }
627
+ });
628
+ return completion;
459
629
  };
460
630
  const canRunJsTsHeavyScans = canRunStartupHeavyScans(languageProfile, "jsts");
461
631
  const scanNames = ["todo", "dead-code"];
@@ -779,50 +949,65 @@ function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, sn
779
949
  dbg(`session_start trivy: ${result.findings.length} CVE findings (${Date.now() - startMs}ms)`);
780
950
  });
781
951
  // call-graph — build function-level call graph from review graph data
782
- runTask("call-graph", async () => {
952
+ let callGraphIdentity;
953
+ const callGraphTask = runTask("call-graph", async () => {
783
954
  const { FactStore } = await import("./dispatch/fact-store.js");
784
- const { buildOrUpdateGraph, extractSymbolsAndRefsFromGraph, isReviewGraphMigrationNeeded, } = await import("./review-graph/builder.js");
785
- const { buildCallGraph, saveCallGraph, loadCallGraph, staleFiles, readMtimes, } = await import("./call-graph.js");
955
+ const { buildOrUpdateGraph, extractSymbolsAndRefsFromGraph, getReviewGraphCacheIdentity, } = await import("./review-graph/builder.js");
956
+ const { buildCallGraph, saveCallGraph, loadCallGraph } = await import("./call-graph.js");
786
957
  if (!runtime.isCurrentSession(sessionGeneration))
787
958
  return;
788
959
  const startMs = Date.now();
789
- // Try loading from cache first
790
- const cached = loadCallGraph(snapshotRoot);
791
- if (cached) {
792
- const cachedFiles = [...cached.fileMtimes.keys()];
793
- const stale = staleFiles(cached.fileMtimes, cachedFiles);
794
- // #260: a stale REVIEW-graph version must force a rebuild even when the
795
- // (separate) call-graph cache is fresh — otherwise an upgrade that
796
- // invalidated the persisted graph (e.g. v2→v3 test exclusion) leaves
797
- // reads cold until the next edit. The version check is cheap (file head).
798
- if (stale.length === 0 &&
799
- cachedFiles.length > 0 &&
800
- !isReviewGraphMigrationNeeded(analysisRoot)) {
801
- runtime.callGraph = cached.graph;
802
- dbg(`session_start call-graph: loaded from cache (${cached.graph.edges.length} edges, ${Date.now() - startMs}ms)`);
803
- return;
804
- }
805
- }
806
- // Build from the review graph (reuses already-parsed data, no re-parse)
960
+ // Build (or hydrate) the canonical review graph first. The call graph is a
961
+ // derived projection of that graph, so its freshness is the review graph's
962
+ // version/signature—not a second source walk and mtime policy.
807
963
  const sessionFacts = new FactStore();
808
964
  const graph = await buildOrUpdateGraph(analysisRoot, [], sessionFacts);
809
965
  if (!runtime.isCurrentSession(sessionGeneration))
810
966
  return;
811
- const { allSymbols, allRefs } = extractSymbolsAndRefsFromGraph(graph);
812
- const callGraph = buildCallGraph(allSymbols, allRefs);
967
+ const identity = getReviewGraphCacheIdentity(analysisRoot, graph);
968
+ if (!identity) {
969
+ dbg("session_start call-graph: canonical review-graph identity unavailable");
970
+ return;
971
+ }
972
+ callGraphIdentity = {
973
+ reviewGraphVersion: identity.version,
974
+ reviewGraphSignature: identity.signature,
975
+ };
976
+ const cached = loadCallGraph(snapshotRoot, {
977
+ reviewGraphVersion: identity.version,
978
+ reviewGraphSignature: identity.signature,
979
+ });
980
+ if (cached) {
981
+ runtime.callGraph = cached.graph;
982
+ dbg(`session_start call-graph: loaded from cache (${cached.graph.edges.length} edges, ${cached.graph.callers.size} callee entries, ${Date.now() - startMs}ms)`);
983
+ return;
984
+ }
985
+ // Build from the canonical review graph (reuses already-parsed data, no
986
+ // duplicate parser or source walk).
987
+ const { allSymbols, allRefs, coverage } = extractSymbolsAndRefsFromGraph(graph);
988
+ const callGraph = buildCallGraph(allSymbols, allRefs, coverage);
813
989
  runtime.callGraph = callGraph;
814
- const mtimes = readMtimes([...allSymbols.keys()]);
815
- saveCallGraph(snapshotRoot, callGraph, mtimes);
990
+ saveCallGraph(snapshotRoot, callGraph, {
991
+ reviewGraphVersion: identity.version,
992
+ reviewGraphSignature: identity.signature,
993
+ });
816
994
  dbg(`session_start call-graph: built ${callGraph.edges.length} edges, ${callGraph.callers.size} callee entries (${Date.now() - startMs}ms)`);
817
995
  });
818
996
  // codebase-model — build mental model from call graph (internal-only until validated)
997
+ // Keep this deferred, but await the call-graph task's completion rather than
998
+ // racing its independently deferred timer. Otherwise a slow graph build leaves
999
+ // `runtime.callGraph` unset at this task's start and loses the model for the
1000
+ // entire session (#1070).
819
1001
  runTask("codebase-model", async () => {
1002
+ await callGraphTask;
820
1003
  if (!runtime.isCurrentSession(sessionGeneration))
821
1004
  return;
822
1005
  if (!runtime.callGraph)
823
- return; // call-graph task may not have completed yet
824
- const { buildCodebaseModel, saveCodebaseModel } = await import("./codebase-model.js");
825
- const model = buildCodebaseModel(runtime.callGraph, analysisRoot);
1006
+ return;
1007
+ const { buildCodebaseModel, saveCodebaseModel, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET } = await import("./codebase-model.js");
1008
+ if (!callGraphIdentity)
1009
+ return;
1010
+ const model = buildCodebaseModel(runtime.callGraph, analysisRoot, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, callGraphIdentity);
826
1011
  saveCodebaseModel(snapshotRoot, model);
827
1012
  const top3 = model.entries
828
1013
  .slice(0, 3)
@@ -912,6 +1097,7 @@ export const SESSION_START_GUIDANCE = [
912
1097
  "• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace, ast_grep_dump.",
913
1098
  ];
914
1099
  export async function handleSessionStart(deps) {
1100
+ resetDegradationLedger();
915
1101
  const handlerEnteredAt = Date.now();
916
1102
  const sessionStartMs = deps.sessionStartFiredAt ?? handlerEnteredAt;
917
1103
  const cwdForTelemetry = deps.ctxCwd ?? process.cwd();
@@ -935,8 +1121,7 @@ export async function handleSessionStart(deps) {
935
1121
  filePath: cwdForTelemetry,
936
1122
  phase: "session_start_prehandler",
937
1123
  startedAt: new Date(deps.sessionStartFiredAt).toISOString(),
938
- durationMs: (deps.handlerEnteredAt ?? handlerEnteredAt) -
939
- deps.sessionStartFiredAt,
1124
+ durationMs: (deps.handlerEnteredAt ?? handlerEnteredAt) - deps.sessionStartFiredAt,
940
1125
  metadata: { reason: deps.sessionReason },
941
1126
  });
942
1127
  }
@@ -977,14 +1162,31 @@ export async function handleSessionStart(deps) {
977
1162
  startupMode = deps.startupModeOverride ?? "quick";
978
1163
  }
979
1164
  processGlobals.__piLensFirstSessionDone = true;
1165
+ // #1154: quick mode is entered on BOTH `pi -p`/`--print` (a one-shot that
1166
+ // exits right after this turn) and an interactive process's first
1167
+ // session_start (forced quick to protect keystroke latency, then warms
1168
+ // caches for the NEXT /new). The warmup only benefits a *future* session in
1169
+ // the same process — a one-shot print invocation has none. Worse, in a
1170
+ // one-shot the warmup is a referenced-handle keep-alive: the +2s timer and
1171
+ // the LSP-prewarm children / language-profile source walk it launches
1172
+ // outlive settle with no session_shutdown teardown, holding the process
1173
+ // open (the #1097/#1110/#1122-hyp-A class). So skip warmup entirely in
1174
+ // print mode; interactive first-sessions (not print) still warm.
980
1175
  if (startupMode === "quick" &&
1176
+ !isPrintMode() &&
981
1177
  process.env.PI_LENS_COLD_START_QUICK !== "0" &&
982
1178
  !processGlobals.__piLensWarmupScheduled) {
983
1179
  processGlobals.__piLensWarmupScheduled = true;
984
1180
  const warmupDelayMs = Number(process.env.PI_LENS_WARMUP_DELAY_MS ?? 2000);
985
1181
  const warmupCwd = deps.ctxCwd ?? process.cwd();
986
1182
  const warmupDbg = deps.dbg;
987
- setTimeout(() => {
1183
+ // #1154: `.unref()` the warmup timer so — even for a warmup that IS
1184
+ // scheduled (an interactive first-session) — the pending timer alone can
1185
+ // never hold the loop open. Interactive processes stay alive for other
1186
+ // reasons, so the warmup still fires; a one-shot never reaches here (the
1187
+ // isPrintMode gate above), and repo convention is that every background
1188
+ // timer is unref'd (this file previously had none).
1189
+ const warmupTimer = setTimeout(() => {
988
1190
  const warmupStartedAt = Date.now();
989
1191
  void (async () => {
990
1192
  try {
@@ -1115,7 +1317,9 @@ export async function handleSessionStart(deps) {
1115
1317
  // #957 review: honor explicit warmFiles (#203) like the full
1116
1318
  // path does — configured projects warm exactly what they
1117
1319
  // asked for; dominant-language warm is the fallback.
1118
- const lspConfig = await loadLSPConfig(warmupCwd).catch(() => ({ warmFiles: [] }));
1320
+ const lspConfig = await loadLSPConfig(warmupCwd).catch(() => ({
1321
+ warmFiles: [],
1322
+ }));
1119
1323
  const warmFiles = lspConfig.warmFiles ?? [];
1120
1324
  if (warmFiles.length > 0) {
1121
1325
  await igniteWarmFiles(warmupCwd, warmFiles, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
@@ -1150,6 +1354,7 @@ export async function handleSessionStart(deps) {
1150
1354
  }
1151
1355
  })();
1152
1356
  }, warmupDelayMs);
1357
+ warmupTimer.unref?.();
1153
1358
  }
1154
1359
  const allowBootstrapTasks = startupMode === "full";
1155
1360
  const quickMode = startupMode === "quick";
@@ -1174,10 +1379,22 @@ export async function handleSessionStart(deps) {
1174
1379
  // previously session-lived with no reset hook at all).
1175
1380
  resetWorkspaceTopology();
1176
1381
  clearTsconfigPathsCache();
1177
- // #817: PATH/PATHEXT command resolution is cached per (command, PATH, cwd);
1178
- // drop it each session so a PATH change (e.g. a tool installed mid-session
1179
- // in a prior session, or a differently-scoped shell) is picked up fresh.
1382
+ // #817/#1199: Windows command resolution is cached per (command, canonical
1383
+ // effective child PATH/PATHEXT/cwd/per-drive provenance); drop it each
1384
+ // session so environment changes (e.g.
1385
+ // a tool installed mid-session or a differently-scoped managed bin) are
1386
+ // picked up fresh.
1180
1387
  resetSafeSpawnWindowsCommandCache();
1388
+ // #1266: install-failure suppression in dispatch runner availability
1389
+ // checkers (`noteInstallFailure`) is meant to last only until "the next
1390
+ // session", per #1222 — but nothing called the reset helper, so a
1391
+ // transient install failure (npm registry blip, locked file) suppressed
1392
+ // the tool for the rest of the process lifetime. Clear it here, same
1393
+ // boundary as the other per-session caches on this line.
1394
+ resetDispatchAvailabilityState();
1395
+ // #1123 item 3: a fresh session can re-report smells that a prior session
1396
+ // already surfaced once (see `checkSmellsAndNoteOnce`'s once-per-session gate).
1397
+ resetSmellsSessionState();
1181
1398
  runtime.resetForSession(sessionStartMs);
1182
1399
  logLatency({
1183
1400
  type: "phase",
@@ -1226,22 +1443,53 @@ export async function handleSessionStart(deps) {
1226
1443
  }
1227
1444
  const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
1228
1445
  const cwd = ctxCwd ?? process.cwd();
1446
+ // #1228: generic atomic-write stages are shared by several project stores,
1447
+ // so the review-graph-specific sweep cannot own this namespace. Sweep the
1448
+ // project data roots and machine-global registry root once per session start;
1449
+ // this is fire-and-forget and bounded so it never delays startup.
1450
+ const projectDataDir = getProjectDataDir(cwd);
1451
+ void sweepAtomicWriteStages([
1452
+ projectDataDir,
1453
+ path.join(projectDataDir, "cache"),
1454
+ path.join(projectDataDir, "sessions"),
1455
+ getGlobalPiLensDir(),
1456
+ ]).catch(() => {
1457
+ // best-effort lifecycle cleanup — never fail session_start
1458
+ });
1229
1459
  if (quickMode) {
1230
1460
  runtime.projectRoot = cwd;
1231
1461
  const sequenceReadStartedAt = Date.now();
1232
1462
  const snapshotRoot = resolveSnapshotRoot(cwd);
1233
- const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
1463
+ const { latestSeq, timedOut } = await readSequenceWithBudget({
1464
+ snapshotRoot,
1465
+ base: snapshotSequenceBase(snapshotRoot),
1466
+ cwd,
1467
+ runtime,
1468
+ sessionGeneration: runtime.sessionGeneration,
1469
+ dbg,
1470
+ });
1234
1471
  logLatency({
1235
1472
  type: "phase",
1236
1473
  phase: "session_start_sequence_read",
1237
1474
  filePath: cwd,
1238
1475
  startedAt: new Date(sequenceReadStartedAt).toISOString(),
1239
1476
  durationMs: Date.now() - sequenceReadStartedAt,
1240
- metadata: { entries: latestSeq.fileSeqByPath.size },
1477
+ metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
1241
1478
  });
1242
1479
  runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
1243
1480
  const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
1244
1481
  dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
1482
+ // #1162 review follow-up (P3): a timed-out read's cold sentinel is
1483
+ // `projectSeq: 0`, which is indistinguishable from a project's real
1484
+ // first-ever snapshot (persisted with `seq === 0` before any change was
1485
+ // logged). Feeding `effectiveSeq` (0) straight into the freshness check
1486
+ // would let that legit seq-0 snapshot match and hydrate as FRESH even
1487
+ // though the on-disk log has since moved past it. Use a value the
1488
+ // freshness check can never match (`snapshot.seq` is always >= 0) ONLY
1489
+ // for this gate when the read timed out — `runtime.projectSeq` itself
1490
+ // stays 0 (untouched), which is what Fix 1's advancement guard above
1491
+ // relies on to detect an in-window bump.
1492
+ const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
1245
1493
  const snapshotLoadStartedAt = Date.now();
1246
1494
  const snapshotPath = getProjectSnapshotPath(snapshotRoot);
1247
1495
  let snapshotBytes = 0;
@@ -1253,11 +1501,11 @@ export async function handleSessionStart(deps) {
1253
1501
  }
1254
1502
  const snapshotGate = loadSnapshotBodyUnlessStale({
1255
1503
  root: snapshotRoot,
1256
- currentProjectSeq: effectiveSeq,
1504
+ currentProjectSeq: freshnessSeq,
1257
1505
  dbg,
1258
1506
  });
1259
1507
  const snapshot = snapshotGate.snapshot;
1260
- const snapshotFresh = isProjectSnapshotFresh(snapshot, effectiveSeq);
1508
+ const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
1261
1509
  logLatency({
1262
1510
  type: "phase",
1263
1511
  phase: "session_start_snapshot_load",
@@ -1269,12 +1517,13 @@ export async function handleSessionStart(deps) {
1269
1517
  fresh: snapshotFresh,
1270
1518
  seq: snapshot?.seq ?? null,
1271
1519
  ...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
1520
+ ...(timedOut ? { sequenceUnknown: true } : {}),
1272
1521
  },
1273
1522
  });
1274
1523
  logProjectSnapshotProbe({
1275
1524
  dbg,
1276
1525
  root: snapshotRoot,
1277
- currentProjectSeq: effectiveSeq,
1526
+ currentProjectSeq: freshnessSeq,
1278
1527
  snapshot,
1279
1528
  });
1280
1529
  if (snapshotFresh) {
@@ -1297,6 +1546,7 @@ export async function handleSessionStart(deps) {
1297
1546
  durationMs: totalDurationMs,
1298
1547
  metadata: { mode: startupMode, reason: deps.sessionReason },
1299
1548
  });
1549
+ emitSmellsSessionStartLine(dbg, sessionStartMs);
1300
1550
  return;
1301
1551
  }
1302
1552
  const tools = [];
@@ -1309,20 +1559,39 @@ export async function handleSessionStart(deps) {
1309
1559
  dbg(`session_start: cleaned stale tsbuildinfo: ${cleaned.join(", ")}`);
1310
1560
  }
1311
1561
  }
1562
+ // Captured here (not at first use, below) because #1162's bounded read
1563
+ // needs it to gate the deferred reseed against a stale/moved-on session —
1564
+ // it's stable for the rest of this call (no further `resetForSession`
1565
+ // between here and the later uses of the same generation).
1566
+ const sessionGeneration = runtime.sessionGeneration;
1312
1567
  const sequenceReadStartedAt = Date.now();
1313
1568
  const snapshotRoot = resolveSnapshotRoot(cwd);
1314
- const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
1569
+ const { latestSeq, timedOut } = await readSequenceWithBudget({
1570
+ snapshotRoot,
1571
+ base: snapshotSequenceBase(snapshotRoot),
1572
+ cwd,
1573
+ runtime,
1574
+ sessionGeneration,
1575
+ dbg,
1576
+ });
1315
1577
  logLatency({
1316
1578
  type: "phase",
1317
1579
  phase: "session_start_sequence_read",
1318
1580
  filePath: cwd,
1319
1581
  startedAt: new Date(sequenceReadStartedAt).toISOString(),
1320
1582
  durationMs: Date.now() - sequenceReadStartedAt,
1321
- metadata: { entries: latestSeq.fileSeqByPath.size },
1583
+ metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
1322
1584
  });
1323
1585
  runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
1324
1586
  const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
1325
1587
  dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
1588
+ // #1162 review follow-up (P3) — see the matching comment in the quick-mode
1589
+ // path above: a timed-out read's cold sentinel (`projectSeq: 0`) must
1590
+ // never satisfy the freshness check, or a project's real first-ever
1591
+ // snapshot (persisted at `seq === 0`) would hydrate as fresh despite the
1592
+ // on-disk log having moved past it. `runtime.projectSeq` itself is left
1593
+ // at 0, which Fix 1's advancement guard above depends on.
1594
+ const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
1326
1595
  const snapshotLoadStartedAt = Date.now();
1327
1596
  const snapshotPath = getProjectSnapshotPath(snapshotRoot);
1328
1597
  let snapshotBytes = 0;
@@ -1334,11 +1603,11 @@ export async function handleSessionStart(deps) {
1334
1603
  }
1335
1604
  const snapshotGate = loadSnapshotBodyUnlessStale({
1336
1605
  root: snapshotRoot,
1337
- currentProjectSeq: effectiveSeq,
1606
+ currentProjectSeq: freshnessSeq,
1338
1607
  dbg,
1339
1608
  });
1340
1609
  const snapshot = snapshotGate.snapshot;
1341
- const snapshotFresh = isProjectSnapshotFresh(snapshot, effectiveSeq);
1610
+ const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
1342
1611
  logLatency({
1343
1612
  type: "phase",
1344
1613
  phase: "session_start_snapshot_load",
@@ -1350,12 +1619,13 @@ export async function handleSessionStart(deps) {
1350
1619
  fresh: snapshotFresh,
1351
1620
  seq: snapshot?.seq ?? null,
1352
1621
  ...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
1622
+ ...(timedOut ? { sequenceUnknown: true } : {}),
1353
1623
  },
1354
1624
  });
1355
1625
  logProjectSnapshotProbe({
1356
1626
  dbg,
1357
1627
  root: snapshotRoot,
1358
- currentProjectSeq: effectiveSeq,
1628
+ currentProjectSeq: freshnessSeq,
1359
1629
  snapshot,
1360
1630
  });
1361
1631
  const freshSnapshot = snapshotFresh ? snapshot : null;
@@ -1523,7 +1793,8 @@ export async function handleSessionStart(deps) {
1523
1793
  dbg("session_start: no project rules found");
1524
1794
  }
1525
1795
  cacheManager.writeCache("session-start-guidance", { content: agentStartupGuidance.join("\n") }, analysisRoot);
1526
- const sessionGeneration = runtime.sessionGeneration;
1796
+ // `sessionGeneration` was captured earlier (#1162's bounded sequence read
1797
+ // needs it before this point) and is stable across this whole call.
1527
1798
  if (!allowBootstrapTasks) {
1528
1799
  dbg("session_start: skipping startup background scans (startup mode)");
1529
1800
  }
@@ -1595,4 +1866,21 @@ export async function handleSessionStart(deps) {
1595
1866
  durationMs: totalDurationMs,
1596
1867
  metadata: { mode: startupMode, reason: deps.sessionReason },
1597
1868
  });
1869
+ emitSmellsSessionStartLine(dbg, sessionStartMs);
1870
+ }
1871
+ /**
1872
+ * #1123 item 3: one bounded `session_start` line surfacing smells the manual
1873
+ * `npm run logs:smells` analyzer would otherwise only catch on demand — see
1874
+ * `clients/smells-rollup.ts` for the tail-scan cost bound and threshold
1875
+ * gating. Never throws: a rollup miss must not break session_start.
1876
+ */
1877
+ function emitSmellsSessionStartLine(dbg, sessionStartMs) {
1878
+ try {
1879
+ const line = formatSmellsSessionStartLine(countRecentSmells(undefined, sessionStartMs));
1880
+ if (line)
1881
+ dbg(line);
1882
+ }
1883
+ catch {
1884
+ // best-effort — smells rollup must never break session_start
1885
+ }
1598
1886
  }