@herbertgao/pi-extensions 2026.8.4 → 2026.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  26. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  36. package/node_modules/@narumitw/pi-btw/README.md +32 -24
  37. package/node_modules/@narumitw/pi-btw/package.json +6 -6
  38. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  39. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
  40. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  41. package/node_modules/pi-lens/CHANGELOG.md +2535 -954
  42. package/node_modules/pi-lens/README.md +13 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  44. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  45. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  46. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  47. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  48. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  50. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  51. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  52. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  53. package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
  54. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  55. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  56. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  57. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  58. package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
  59. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  60. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  61. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  62. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  63. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  64. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  65. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  66. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  67. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
  69. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  70. package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
  71. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  72. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  73. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  74. package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
  75. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
  76. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
  77. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
  78. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  79. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  80. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  81. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  83. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
  84. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  87. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  114. package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
  115. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  116. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  117. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  118. package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
  119. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  120. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  121. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  122. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  123. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
  125. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  126. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  127. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  128. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  129. package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
  130. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  131. package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  134. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  135. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  136. package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
  137. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  138. package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
  139. package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
  140. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  141. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  142. package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
  143. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  144. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  145. package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
  146. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  147. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  148. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
  149. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  150. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  151. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
  152. package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
  153. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  154. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
  155. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  156. package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
  157. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  158. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
  161. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  162. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  163. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  164. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  165. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  167. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
  168. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  171. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  172. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  173. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  174. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  175. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  177. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  179. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  180. package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
  181. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  182. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  183. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  184. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  185. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  186. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  187. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  188. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  189. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  190. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  191. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  192. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  193. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  194. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  195. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  196. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  197. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  198. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  199. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  201. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  202. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  203. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  204. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  205. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  206. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  207. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  208. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  217. package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
  218. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  219. package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
  220. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  221. package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
  222. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  223. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  224. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  225. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  226. package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
  227. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  228. package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
  229. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  230. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  233. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  234. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  237. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  238. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  239. package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
  240. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  241. package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
  242. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  243. package/node_modules/pi-lens/dist/index.js +51785 -36118
  244. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  245. package/node_modules/pi-lens/dist/mcp/server.js +219 -46
  246. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  247. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  248. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  249. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  250. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
  251. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
  252. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  253. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  254. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  255. package/node_modules/pi-lens/docs/agent-guide.md +40 -15
  256. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  257. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  258. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  259. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
  260. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  261. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  262. package/node_modules/pi-lens/docs/fable.md +27 -0
  263. package/node_modules/pi-lens/docs/features.md +45 -3
  264. package/node_modules/pi-lens/docs/globalconfig.md +56 -1
  265. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  266. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  267. package/node_modules/pi-lens/docs/mcp.md +37 -4
  268. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  269. package/node_modules/pi-lens/docs/settings.md +18 -1
  270. package/node_modules/pi-lens/docs/tools.md +4 -1
  271. package/node_modules/pi-lens/docs/usage.md +41 -5
  272. package/node_modules/pi-lens/docs/word-index.md +35 -0
  273. package/node_modules/pi-lens/package.json +10 -10
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  300. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  301. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  302. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
  303. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  304. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  305. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  306. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  307. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  308. package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
  309. package/node_modules/pi-mcp-adapter/README.md +38 -4
  310. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  311. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  312. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  313. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  314. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  315. package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
  316. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  317. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  318. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  319. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  320. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  321. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  322. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  323. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  324. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  325. package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
  326. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  327. package/node_modules/pi-mcp-adapter/package.json +4 -3
  328. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  329. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  330. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  331. package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
  332. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  333. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  334. package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
  335. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  336. package/node_modules/pi-mcp-adapter/types.ts +176 -27
  337. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  338. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  339. package/node_modules/pi-web-access/README.md +18 -12
  340. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  341. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  342. package/node_modules/pi-web-access/curator-page.ts +5 -3
  343. package/node_modules/pi-web-access/curator-server.ts +2 -1
  344. package/node_modules/pi-web-access/extract.ts +106 -34
  345. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  346. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  347. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  348. package/node_modules/pi-web-access/index.ts +120 -48
  349. package/node_modules/pi-web-access/package.json +2 -2
  350. package/node_modules/pi-web-access/summary-review.ts +11 -5
  351. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  352. package/package.json +12 -13
  353. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  354. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  355. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  356. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  357. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  358. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  359. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  360. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -1,32 +1,38 @@
1
1
  import * as nodeFs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { createDeadline, yieldIfOverBudget } from "./cooperative-budget.js";
3
4
  import { deadCodeIssueCount } from "./dead-code-client.js";
4
5
  import { logDeadCodeScan } from "./dead-code-logger.js";
6
+ import { resetDegradationLedger } from "./degradation-ledger.js";
5
7
  import { getDiagnosticTracker } from "./diagnostic-tracker.js";
8
+ import { resetDispatchAvailabilityState } from "./dispatch/runners/utils/runner-helpers.js";
6
9
  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";
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";
13
+ import { sweepAtomicWriteStages } from "./instance-reaper.js";
14
14
  import { canRunStartupHeavyScans } from "./language-policy.js";
15
15
  import { detectProjectLanguageProfile, getDefaultStartupTools, } from "./language-profile.js";
16
16
  import { logLatency } from "./latency-logger.js";
17
- import { logWordIndex } from "./word-index-logger.js";
18
17
  import { runLogCleanup } from "./log-cleanup.js";
19
- import { setSessionLanguages } from "./widget-state.js";
20
18
  import { initLSPConfig, loadLSPConfig } from "./lsp/config.js";
21
- import { getLSPService } from "./lsp/index.js";
22
- import { readLatestProjectSequence, } from "./project-changes.js";
19
+ import { loadLspService } from "./lsp-lazy.js";
20
+ import { isAtOrAboveHomeDir } from "./path-utils.js";
21
+ import { isPrintMode } from "./print-mode.js";
22
+ import { readLatestProjectSequence, readLatestProjectSequenceAsync, } from "./project-changes.js";
23
23
  import { getProjectSnapshotPath, hydrateRuntimeFromProjectSnapshot, isProjectSnapshotFresh, isProjectSnapshotMetaStale, loadProjectSnapshot, PROJECT_SNAPSHOT_VERSION, readProjectSnapshotMeta, saveRuntimeProjectSnapshot, } from "./project-snapshot.js";
24
+ import { clearTsconfigPathsCache } from "./review-graph/tsconfig-paths.js";
24
25
  import { scanProjectRules } from "./rules-scanner.js";
25
- import { isAtOrAboveHomeDir } from "./path-utils.js";
26
+ import { resetSafeSpawnWindowsCommandCache } from "./safe-spawn.js";
26
27
  import { getSlowFsVerdict, isSlowFs, slowFsDegradationNotice, } from "./slow-fs.js";
27
- import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
28
+ import { countRecentSmells, formatSmellsSessionStartLine, resetSmellsSessionState, } from "./smells-rollup.js";
28
29
  import { findNearestProjectRoot, getStartupScanMaxEntries, isStartupScanVerdictFresh, resolveStartupScanContext, } from "./startup-scan.js";
30
+ import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.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,52 +449,96 @@ 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;
455
+ const snapshotLoadStartMs = Date.now();
333
456
  const snapshot = loadProjectSnapshot(snapshotRoot);
457
+ const snapshotLoadMs = Date.now() - snapshotLoadStartMs;
334
458
  if (snapshot?.wordIndex) {
335
459
  const { deserializeWordIndex, refreshWordIndexIncrementally } = await import("./word-index.js");
460
+ const deserializeStartMs = Date.now();
336
461
  const index = deserializeWordIndex(snapshot.wordIndex);
462
+ const deserializeMs = Date.now() - deserializeStartMs;
337
463
  if (index) {
338
464
  try {
339
465
  const result = await refreshWordIndexIncrementally(index, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
340
466
  if (!runtime.isCurrentSession(sessionGeneration))
341
467
  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 });
468
+ if (result.mode === "full-required") {
469
+ rebuildPreflightFiles = result.preflightFiles;
470
+ dbg(`session_start word-index: incremental preflight selected full rebuild (${result.reason})`);
471
+ logWordIndex({
472
+ phase: "incremental_fallback",
473
+ cwd: snapshotRoot,
474
+ trigger: "session_start",
475
+ reason: result.reason,
476
+ phaseDurationsMs: {
477
+ snapshotLoadMs,
478
+ deserializeMs,
479
+ ...result.timings,
480
+ },
481
+ });
482
+ }
483
+ else {
484
+ runtime.wordIndex = index;
485
+ let snapshotSaveSyncMs = 0;
486
+ // A stale project seq must be advanced even when mtimes prove every
487
+ // indexed document reusable. Fresh snapshots with no changes avoid
488
+ // an unnecessary rewrite of the large shared snapshot.
489
+ if (!isProjectSnapshotFresh(snapshot, effectiveSeq) ||
490
+ result.refreshed > 0 ||
491
+ result.dropped > 0 ||
492
+ snapshot.wordIndex.truncated !== index.truncated) {
493
+ const serializeSaveEnqueueStartMs = Date.now();
494
+ saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
495
+ snapshotSaveSyncMs = Date.now() - serializeSaveEnqueueStartMs;
496
+ }
497
+ const phases = `snapshot-load=${snapshotLoadMs}ms, deserialize=${deserializeMs}ms, ` +
498
+ `source-walk=${result.timings.sourceWalkMs}ms, stat-walk=${result.timings.statWalkMs}ms, ` +
499
+ `refresh-reads=${result.timings.refreshReadsMs}ms, ` +
500
+ `snapshot-save-sync=${snapshotSaveSyncMs}ms`;
501
+ dbg(`session_start word-index: incremental (seq=${effectiveSeq}, refreshed=${result.refreshed}, dropped=${result.dropped}, skipped=${result.skipped}, reused=${result.reused}, ${Date.now() - startMs}ms, phases: ${phases})`);
502
+ // M2, #958: the incremental-vs-full decision + honest coverage
503
+ // (indexedFileCount/truncated/skipped), independent of host `dbg`.
504
+ logWordIndex({
505
+ phase: "incremental_refresh",
506
+ cwd: snapshotRoot,
507
+ trigger: "session_start",
508
+ durationMs: Date.now() - startMs,
509
+ indexedFileCount: index.docCount,
510
+ tokens: index.postings.size,
511
+ truncated: index.truncated,
512
+ phaseDurationsMs: {
513
+ snapshotLoadMs,
514
+ deserializeMs,
515
+ ...result.timings,
516
+ snapshotSaveSyncMs,
517
+ },
518
+ refreshed: result.refreshed,
519
+ dropped: result.dropped,
520
+ skipped: result.skipped,
521
+ reused: result.reused,
522
+ });
523
+ return result;
351
524
  }
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
525
  }
370
526
  catch (err) {
527
+ // Supersession is an expected transition, not a failure: a newer
528
+ // session_start bumped the generation mid-refresh. Return undefined the
529
+ // way master's synchronous path did instead of reporting a fallback and
530
+ // re-walking for a rebuild whose result this generation may not publish.
531
+ if (!runtime.isCurrentSession(sessionGeneration)) {
532
+ dbg("session_start word-index: incremental refresh superseded");
533
+ return;
534
+ }
371
535
  dbg(`session_start word-index: incremental refresh failed; falling back to full rebuild (${err})`);
372
536
  logWordIndex({
373
537
  phase: "incremental_fallback",
374
538
  cwd: snapshotRoot,
375
539
  trigger: "session_start",
376
540
  reason: err instanceof Error ? err.message : String(err),
541
+ phaseDurationsMs: { snapshotLoadMs, deserializeMs },
377
542
  });
378
543
  }
379
544
  }
@@ -382,11 +547,30 @@ async function buildOrRefreshWordIndex(args) {
382
547
  // implementation backs this task, the quick-mode warmup call below, AND the
383
548
  // stateless cold-query background trigger in word-index.ts — a bound/skip
384
549
  // -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));
550
+ const { buildWordIndexAsync, collectWordIndexDocs } = await import("./word-index.js");
551
+ const docs = await collectWordIndexDocs(analysisRoot, () => runtime.isCurrentSession(sessionGeneration), rebuildPreflightFiles);
387
552
  if (!runtime.isCurrentSession(sessionGeneration))
388
553
  return;
389
- runtime.wordIndex = buildWordIndex(docs);
554
+ // #1197 review finding 2: `buildWordIndexAsync` THROWS on supersession, and
555
+ // the quick-mode warmup caller (below) awaits this function OUTSIDE any
556
+ // per-step try/catch — an escaping throw there skips the rest of warmup,
557
+ // including the #947 LSP pre-warm, and resets `__piLensWarmupScheduled`.
558
+ // Supersession is an expected transition (master's synchronous path just
559
+ // returned), so absorb it here and let the caller continue.
560
+ let rebuiltIndex;
561
+ try {
562
+ rebuiltIndex = await buildWordIndexAsync(docs, () => runtime.isCurrentSession(sessionGeneration));
563
+ }
564
+ catch (err) {
565
+ if (!runtime.isCurrentSession(sessionGeneration)) {
566
+ dbg("session_start word-index: rebuild superseded");
567
+ return;
568
+ }
569
+ throw err;
570
+ }
571
+ if (!runtime.isCurrentSession(sessionGeneration))
572
+ return;
573
+ runtime.wordIndex = rebuiltIndex;
390
574
  saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
391
575
  dbg(`session_start word-index: rebuilt (absent/stale, seq=${effectiveSeq}) ` +
392
576
  `${runtime.wordIndex.docCount} files, ${runtime.wordIndex.postings.size} tokens (${Date.now() - startMs}ms)`);
@@ -433,29 +617,38 @@ function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, sn
433
617
  const queuedAt = Date.now();
434
618
  dbg(`session_start task ${name}: scheduled`);
435
619
  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
- }
620
+ const completion = new Promise((resolve) => {
621
+ const fire = () => {
622
+ const startedAt = Date.now();
623
+ dbg(`session_start task ${name}: start queuedMs=${startedAt - queuedAt}`);
624
+ void task()
625
+ .then(() => {
626
+ dbg(`session_start task ${name}: success runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
627
+ })
628
+ .catch((err) => {
629
+ dbg(`session_start: ${name} background scan failed: ${err}`);
630
+ dbg(`session_start task ${name}: failed runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
631
+ })
632
+ .finally(() => {
633
+ runtime.clearStartupScanInFlight(name, sessionGeneration);
634
+ dbg(`session_start task ${name}: end`);
635
+ resolve();
636
+ });
637
+ };
638
+ const delay = taskDeferMsByName[name] ?? 0;
639
+ if (delay > 0) {
640
+ // #1154: unref to match the repo-wide convention that every
641
+ // background timer is `.unref()`'d. Full-mode deferred scans only
642
+ // run in long-lived/interactive sessions today (never a one-shot),
643
+ // so this is defensive — but a referenced 5s timer would keep a
644
+ // process open were a full-mode one-shot ever introduced.
645
+ setTimeout(fire, delay).unref?.();
646
+ }
647
+ else {
648
+ setImmediate(fire);
649
+ }
650
+ });
651
+ return completion;
459
652
  };
460
653
  const canRunJsTsHeavyScans = canRunStartupHeavyScans(languageProfile, "jsts");
461
654
  const scanNames = ["todo", "dead-code"];
@@ -779,50 +972,65 @@ function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, sn
779
972
  dbg(`session_start trivy: ${result.findings.length} CVE findings (${Date.now() - startMs}ms)`);
780
973
  });
781
974
  // call-graph — build function-level call graph from review graph data
782
- runTask("call-graph", async () => {
975
+ let callGraphIdentity;
976
+ const callGraphTask = runTask("call-graph", async () => {
783
977
  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");
978
+ const { buildOrUpdateGraph, extractSymbolsAndRefsFromGraph, getReviewGraphCacheIdentity, } = await import("./review-graph/builder.js");
979
+ const { buildCallGraph, saveCallGraph, loadCallGraph } = await import("./call-graph.js");
786
980
  if (!runtime.isCurrentSession(sessionGeneration))
787
981
  return;
788
982
  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)
983
+ // Build (or hydrate) the canonical review graph first. The call graph is a
984
+ // derived projection of that graph, so its freshness is the review graph's
985
+ // version/signature—not a second source walk and mtime policy.
807
986
  const sessionFacts = new FactStore();
808
987
  const graph = await buildOrUpdateGraph(analysisRoot, [], sessionFacts);
809
988
  if (!runtime.isCurrentSession(sessionGeneration))
810
989
  return;
811
- const { allSymbols, allRefs } = extractSymbolsAndRefsFromGraph(graph);
812
- const callGraph = buildCallGraph(allSymbols, allRefs);
990
+ const identity = getReviewGraphCacheIdentity(analysisRoot, graph);
991
+ if (!identity) {
992
+ dbg("session_start call-graph: canonical review-graph identity unavailable");
993
+ return;
994
+ }
995
+ callGraphIdentity = {
996
+ reviewGraphVersion: identity.version,
997
+ reviewGraphSignature: identity.signature,
998
+ };
999
+ const cached = loadCallGraph(snapshotRoot, {
1000
+ reviewGraphVersion: identity.version,
1001
+ reviewGraphSignature: identity.signature,
1002
+ });
1003
+ if (cached) {
1004
+ runtime.callGraph = cached.graph;
1005
+ dbg(`session_start call-graph: loaded from cache (${cached.graph.edges.length} edges, ${cached.graph.callers.size} callee entries, ${Date.now() - startMs}ms)`);
1006
+ return;
1007
+ }
1008
+ // Build from the canonical review graph (reuses already-parsed data, no
1009
+ // duplicate parser or source walk).
1010
+ const { allSymbols, allRefs, coverage } = extractSymbolsAndRefsFromGraph(graph);
1011
+ const callGraph = buildCallGraph(allSymbols, allRefs, coverage);
813
1012
  runtime.callGraph = callGraph;
814
- const mtimes = readMtimes([...allSymbols.keys()]);
815
- saveCallGraph(snapshotRoot, callGraph, mtimes);
1013
+ saveCallGraph(snapshotRoot, callGraph, {
1014
+ reviewGraphVersion: identity.version,
1015
+ reviewGraphSignature: identity.signature,
1016
+ });
816
1017
  dbg(`session_start call-graph: built ${callGraph.edges.length} edges, ${callGraph.callers.size} callee entries (${Date.now() - startMs}ms)`);
817
1018
  });
818
1019
  // codebase-model — build mental model from call graph (internal-only until validated)
1020
+ // Keep this deferred, but await the call-graph task's completion rather than
1021
+ // racing its independently deferred timer. Otherwise a slow graph build leaves
1022
+ // `runtime.callGraph` unset at this task's start and loses the model for the
1023
+ // entire session (#1070).
819
1024
  runTask("codebase-model", async () => {
1025
+ await callGraphTask;
820
1026
  if (!runtime.isCurrentSession(sessionGeneration))
821
1027
  return;
822
1028
  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);
1029
+ return;
1030
+ const { buildCodebaseModel, saveCodebaseModel, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, } = await import("./codebase-model.js");
1031
+ if (!callGraphIdentity)
1032
+ return;
1033
+ const model = buildCodebaseModel(runtime.callGraph, analysisRoot, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, callGraphIdentity);
826
1034
  saveCodebaseModel(snapshotRoot, model);
827
1035
  const top3 = model.entries
828
1036
  .slice(0, 3)
@@ -912,6 +1120,7 @@ export const SESSION_START_GUIDANCE = [
912
1120
  "• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace, ast_grep_dump.",
913
1121
  ];
914
1122
  export async function handleSessionStart(deps) {
1123
+ resetDegradationLedger();
915
1124
  const handlerEnteredAt = Date.now();
916
1125
  const sessionStartMs = deps.sessionStartFiredAt ?? handlerEnteredAt;
917
1126
  const cwdForTelemetry = deps.ctxCwd ?? process.cwd();
@@ -935,8 +1144,7 @@ export async function handleSessionStart(deps) {
935
1144
  filePath: cwdForTelemetry,
936
1145
  phase: "session_start_prehandler",
937
1146
  startedAt: new Date(deps.sessionStartFiredAt).toISOString(),
938
- durationMs: (deps.handlerEnteredAt ?? handlerEnteredAt) -
939
- deps.sessionStartFiredAt,
1147
+ durationMs: (deps.handlerEnteredAt ?? handlerEnteredAt) - deps.sessionStartFiredAt,
940
1148
  metadata: { reason: deps.sessionReason },
941
1149
  });
942
1150
  }
@@ -977,14 +1185,31 @@ export async function handleSessionStart(deps) {
977
1185
  startupMode = deps.startupModeOverride ?? "quick";
978
1186
  }
979
1187
  processGlobals.__piLensFirstSessionDone = true;
1188
+ // #1154: quick mode is entered on BOTH `pi -p`/`--print` (a one-shot that
1189
+ // exits right after this turn) and an interactive process's first
1190
+ // session_start (forced quick to protect keystroke latency, then warms
1191
+ // caches for the NEXT /new). The warmup only benefits a *future* session in
1192
+ // the same process — a one-shot print invocation has none. Worse, in a
1193
+ // one-shot the warmup is a referenced-handle keep-alive: the +2s timer and
1194
+ // the LSP-prewarm children / language-profile source walk it launches
1195
+ // outlive settle with no session_shutdown teardown, holding the process
1196
+ // open (the #1097/#1110/#1122-hyp-A class). So skip warmup entirely in
1197
+ // print mode; interactive first-sessions (not print) still warm.
980
1198
  if (startupMode === "quick" &&
1199
+ !isPrintMode() &&
981
1200
  process.env.PI_LENS_COLD_START_QUICK !== "0" &&
982
1201
  !processGlobals.__piLensWarmupScheduled) {
983
1202
  processGlobals.__piLensWarmupScheduled = true;
984
1203
  const warmupDelayMs = Number(process.env.PI_LENS_WARMUP_DELAY_MS ?? 2000);
985
1204
  const warmupCwd = deps.ctxCwd ?? process.cwd();
986
1205
  const warmupDbg = deps.dbg;
987
- setTimeout(() => {
1206
+ // #1154: `.unref()` the warmup timer so — even for a warmup that IS
1207
+ // scheduled (an interactive first-session) — the pending timer alone can
1208
+ // never hold the loop open. Interactive processes stay alive for other
1209
+ // reasons, so the warmup still fires; a one-shot never reaches here (the
1210
+ // isPrintMode gate above), and repo convention is that every background
1211
+ // timer is unref'd (this file previously had none).
1212
+ const warmupTimer = setTimeout(() => {
988
1213
  const warmupStartedAt = Date.now();
989
1214
  void (async () => {
990
1215
  try {
@@ -1115,7 +1340,9 @@ export async function handleSessionStart(deps) {
1115
1340
  // #957 review: honor explicit warmFiles (#203) like the full
1116
1341
  // path does — configured projects warm exactly what they
1117
1342
  // asked for; dominant-language warm is the fallback.
1118
- const lspConfig = await loadLSPConfig(warmupCwd).catch(() => ({ warmFiles: [] }));
1343
+ const lspConfig = await loadLSPConfig(warmupCwd).catch(() => ({
1344
+ warmFiles: [],
1345
+ }));
1119
1346
  const warmFiles = lspConfig.warmFiles ?? [];
1120
1347
  if (warmFiles.length > 0) {
1121
1348
  await igniteWarmFiles(warmupCwd, warmFiles, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
@@ -1150,6 +1377,7 @@ export async function handleSessionStart(deps) {
1150
1377
  }
1151
1378
  })();
1152
1379
  }, warmupDelayMs);
1380
+ warmupTimer.unref?.();
1153
1381
  }
1154
1382
  const allowBootstrapTasks = startupMode === "full";
1155
1383
  const quickMode = startupMode === "quick";
@@ -1174,10 +1402,22 @@ export async function handleSessionStart(deps) {
1174
1402
  // previously session-lived with no reset hook at all).
1175
1403
  resetWorkspaceTopology();
1176
1404
  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.
1405
+ // #817/#1199: Windows command resolution is cached per (command, canonical
1406
+ // effective child PATH/PATHEXT/cwd/per-drive provenance); drop it each
1407
+ // session so environment changes (e.g.
1408
+ // a tool installed mid-session or a differently-scoped managed bin) are
1409
+ // picked up fresh.
1180
1410
  resetSafeSpawnWindowsCommandCache();
1411
+ // #1266: install-failure suppression in dispatch runner availability
1412
+ // checkers (`noteInstallFailure`) is meant to last only until "the next
1413
+ // session", per #1222 — but nothing called the reset helper, so a
1414
+ // transient install failure (npm registry blip, locked file) suppressed
1415
+ // the tool for the rest of the process lifetime. Clear it here, same
1416
+ // boundary as the other per-session caches on this line.
1417
+ resetDispatchAvailabilityState();
1418
+ // #1123 item 3: a fresh session can re-report smells that a prior session
1419
+ // already surfaced once (see `checkSmellsAndNoteOnce`'s once-per-session gate).
1420
+ resetSmellsSessionState();
1181
1421
  runtime.resetForSession(sessionStartMs);
1182
1422
  logLatency({
1183
1423
  type: "phase",
@@ -1226,22 +1466,53 @@ export async function handleSessionStart(deps) {
1226
1466
  }
1227
1467
  const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
1228
1468
  const cwd = ctxCwd ?? process.cwd();
1469
+ // #1228: generic atomic-write stages are shared by several project stores,
1470
+ // so the review-graph-specific sweep cannot own this namespace. Sweep the
1471
+ // project data roots and machine-global registry root once per session start;
1472
+ // this is fire-and-forget and bounded so it never delays startup.
1473
+ const projectDataDir = getProjectDataDir(cwd);
1474
+ void sweepAtomicWriteStages([
1475
+ projectDataDir,
1476
+ path.join(projectDataDir, "cache"),
1477
+ path.join(projectDataDir, "sessions"),
1478
+ getGlobalPiLensDir(),
1479
+ ]).catch(() => {
1480
+ // best-effort lifecycle cleanup — never fail session_start
1481
+ });
1229
1482
  if (quickMode) {
1230
1483
  runtime.projectRoot = cwd;
1231
1484
  const sequenceReadStartedAt = Date.now();
1232
1485
  const snapshotRoot = resolveSnapshotRoot(cwd);
1233
- const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
1486
+ const { latestSeq, timedOut } = await readSequenceWithBudget({
1487
+ snapshotRoot,
1488
+ base: snapshotSequenceBase(snapshotRoot),
1489
+ cwd,
1490
+ runtime,
1491
+ sessionGeneration: runtime.sessionGeneration,
1492
+ dbg,
1493
+ });
1234
1494
  logLatency({
1235
1495
  type: "phase",
1236
1496
  phase: "session_start_sequence_read",
1237
1497
  filePath: cwd,
1238
1498
  startedAt: new Date(sequenceReadStartedAt).toISOString(),
1239
1499
  durationMs: Date.now() - sequenceReadStartedAt,
1240
- metadata: { entries: latestSeq.fileSeqByPath.size },
1500
+ metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
1241
1501
  });
1242
1502
  runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
1243
1503
  const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
1244
1504
  dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
1505
+ // #1162 review follow-up (P3): a timed-out read's cold sentinel is
1506
+ // `projectSeq: 0`, which is indistinguishable from a project's real
1507
+ // first-ever snapshot (persisted with `seq === 0` before any change was
1508
+ // logged). Feeding `effectiveSeq` (0) straight into the freshness check
1509
+ // would let that legit seq-0 snapshot match and hydrate as FRESH even
1510
+ // though the on-disk log has since moved past it. Use a value the
1511
+ // freshness check can never match (`snapshot.seq` is always >= 0) ONLY
1512
+ // for this gate when the read timed out — `runtime.projectSeq` itself
1513
+ // stays 0 (untouched), which is what Fix 1's advancement guard above
1514
+ // relies on to detect an in-window bump.
1515
+ const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
1245
1516
  const snapshotLoadStartedAt = Date.now();
1246
1517
  const snapshotPath = getProjectSnapshotPath(snapshotRoot);
1247
1518
  let snapshotBytes = 0;
@@ -1253,11 +1524,11 @@ export async function handleSessionStart(deps) {
1253
1524
  }
1254
1525
  const snapshotGate = loadSnapshotBodyUnlessStale({
1255
1526
  root: snapshotRoot,
1256
- currentProjectSeq: effectiveSeq,
1527
+ currentProjectSeq: freshnessSeq,
1257
1528
  dbg,
1258
1529
  });
1259
1530
  const snapshot = snapshotGate.snapshot;
1260
- const snapshotFresh = isProjectSnapshotFresh(snapshot, effectiveSeq);
1531
+ const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
1261
1532
  logLatency({
1262
1533
  type: "phase",
1263
1534
  phase: "session_start_snapshot_load",
@@ -1269,12 +1540,13 @@ export async function handleSessionStart(deps) {
1269
1540
  fresh: snapshotFresh,
1270
1541
  seq: snapshot?.seq ?? null,
1271
1542
  ...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
1543
+ ...(timedOut ? { sequenceUnknown: true } : {}),
1272
1544
  },
1273
1545
  });
1274
1546
  logProjectSnapshotProbe({
1275
1547
  dbg,
1276
1548
  root: snapshotRoot,
1277
- currentProjectSeq: effectiveSeq,
1549
+ currentProjectSeq: freshnessSeq,
1278
1550
  snapshot,
1279
1551
  });
1280
1552
  if (snapshotFresh) {
@@ -1297,6 +1569,7 @@ export async function handleSessionStart(deps) {
1297
1569
  durationMs: totalDurationMs,
1298
1570
  metadata: { mode: startupMode, reason: deps.sessionReason },
1299
1571
  });
1572
+ emitSmellsSessionStartLine(dbg, sessionStartMs);
1300
1573
  return;
1301
1574
  }
1302
1575
  const tools = [];
@@ -1309,20 +1582,39 @@ export async function handleSessionStart(deps) {
1309
1582
  dbg(`session_start: cleaned stale tsbuildinfo: ${cleaned.join(", ")}`);
1310
1583
  }
1311
1584
  }
1585
+ // Captured here (not at first use, below) because #1162's bounded read
1586
+ // needs it to gate the deferred reseed against a stale/moved-on session —
1587
+ // it's stable for the rest of this call (no further `resetForSession`
1588
+ // between here and the later uses of the same generation).
1589
+ const sessionGeneration = runtime.sessionGeneration;
1312
1590
  const sequenceReadStartedAt = Date.now();
1313
1591
  const snapshotRoot = resolveSnapshotRoot(cwd);
1314
- const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
1592
+ const { latestSeq, timedOut } = await readSequenceWithBudget({
1593
+ snapshotRoot,
1594
+ base: snapshotSequenceBase(snapshotRoot),
1595
+ cwd,
1596
+ runtime,
1597
+ sessionGeneration,
1598
+ dbg,
1599
+ });
1315
1600
  logLatency({
1316
1601
  type: "phase",
1317
1602
  phase: "session_start_sequence_read",
1318
1603
  filePath: cwd,
1319
1604
  startedAt: new Date(sequenceReadStartedAt).toISOString(),
1320
1605
  durationMs: Date.now() - sequenceReadStartedAt,
1321
- metadata: { entries: latestSeq.fileSeqByPath.size },
1606
+ metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
1322
1607
  });
1323
1608
  runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
1324
1609
  const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
1325
1610
  dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
1611
+ // #1162 review follow-up (P3) — see the matching comment in the quick-mode
1612
+ // path above: a timed-out read's cold sentinel (`projectSeq: 0`) must
1613
+ // never satisfy the freshness check, or a project's real first-ever
1614
+ // snapshot (persisted at `seq === 0`) would hydrate as fresh despite the
1615
+ // on-disk log having moved past it. `runtime.projectSeq` itself is left
1616
+ // at 0, which Fix 1's advancement guard above depends on.
1617
+ const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
1326
1618
  const snapshotLoadStartedAt = Date.now();
1327
1619
  const snapshotPath = getProjectSnapshotPath(snapshotRoot);
1328
1620
  let snapshotBytes = 0;
@@ -1334,11 +1626,11 @@ export async function handleSessionStart(deps) {
1334
1626
  }
1335
1627
  const snapshotGate = loadSnapshotBodyUnlessStale({
1336
1628
  root: snapshotRoot,
1337
- currentProjectSeq: effectiveSeq,
1629
+ currentProjectSeq: freshnessSeq,
1338
1630
  dbg,
1339
1631
  });
1340
1632
  const snapshot = snapshotGate.snapshot;
1341
- const snapshotFresh = isProjectSnapshotFresh(snapshot, effectiveSeq);
1633
+ const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
1342
1634
  logLatency({
1343
1635
  type: "phase",
1344
1636
  phase: "session_start_snapshot_load",
@@ -1350,12 +1642,13 @@ export async function handleSessionStart(deps) {
1350
1642
  fresh: snapshotFresh,
1351
1643
  seq: snapshot?.seq ?? null,
1352
1644
  ...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
1645
+ ...(timedOut ? { sequenceUnknown: true } : {}),
1353
1646
  },
1354
1647
  });
1355
1648
  logProjectSnapshotProbe({
1356
1649
  dbg,
1357
1650
  root: snapshotRoot,
1358
- currentProjectSeq: effectiveSeq,
1651
+ currentProjectSeq: freshnessSeq,
1359
1652
  snapshot,
1360
1653
  });
1361
1654
  const freshSnapshot = snapshotFresh ? snapshot : null;
@@ -1523,7 +1816,8 @@ export async function handleSessionStart(deps) {
1523
1816
  dbg("session_start: no project rules found");
1524
1817
  }
1525
1818
  cacheManager.writeCache("session-start-guidance", { content: agentStartupGuidance.join("\n") }, analysisRoot);
1526
- const sessionGeneration = runtime.sessionGeneration;
1819
+ // `sessionGeneration` was captured earlier (#1162's bounded sequence read
1820
+ // needs it before this point) and is stable across this whole call.
1527
1821
  if (!allowBootstrapTasks) {
1528
1822
  dbg("session_start: skipping startup background scans (startup mode)");
1529
1823
  }
@@ -1595,4 +1889,21 @@ export async function handleSessionStart(deps) {
1595
1889
  durationMs: totalDurationMs,
1596
1890
  metadata: { mode: startupMode, reason: deps.sessionReason },
1597
1891
  });
1892
+ emitSmellsSessionStartLine(dbg, sessionStartMs);
1893
+ }
1894
+ /**
1895
+ * #1123 item 3: one bounded `session_start` line surfacing smells the manual
1896
+ * `npm run logs:smells` analyzer would otherwise only catch on demand — see
1897
+ * `clients/smells-rollup.ts` for the tail-scan cost bound and threshold
1898
+ * gating. Never throws: a rollup miss must not break session_start.
1899
+ */
1900
+ function emitSmellsSessionStartLine(dbg, sessionStartMs) {
1901
+ try {
1902
+ const line = formatSmellsSessionStartLine(countRecentSmells(undefined, sessionStartMs));
1903
+ if (line)
1904
+ dbg(line);
1905
+ }
1906
+ catch {
1907
+ // best-effort — smells rollup must never break session_start
1908
+ }
1598
1909
  }