@herbertgao/pi-extensions 2026.10.0 → 2026.10.2

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 (769) hide show
  1. package/README.md +16 -12
  2. package/THIRD_PARTY_NOTICES.md +3 -3
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  9. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  11. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  12. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  13. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  14. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  15. package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
  16. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  17. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  18. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  24. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  25. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  26. package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
  27. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  28. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  29. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
  34. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
  35. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
  36. package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
  37. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  38. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  39. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  45. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  46. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  47. package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
  48. package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
  49. package/node_modules/@tifan/pi-recap/package.json +1 -1
  50. package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
  51. package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
  52. package/node_modules/pi-antigravity/README.md +23 -2
  53. package/node_modules/pi-antigravity/package.json +5 -3
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
  55. package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
  56. package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +86 -47
  58. package/node_modules/pi-antigravity/src/image/native.ts +69 -0
  59. package/node_modules/pi-antigravity/src/index.ts +52 -137
  60. package/node_modules/pi-antigravity/src/search/search.ts +130 -31
  61. package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
  62. package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
  63. package/node_modules/pi-antigravity/src/types/types.ts +1 -1
  64. package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
  65. package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
  66. package/node_modules/pi-lens/CHANGELOG.md +1064 -0
  67. package/node_modules/pi-lens/README.md +115 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
  70. package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
  71. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  72. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  73. package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
  74. package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
  75. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  76. package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
  77. package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
  78. package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
  79. package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
  80. package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
  81. package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
  82. package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
  86. package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
  87. package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
  88. package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
  89. package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
  90. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  91. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  92. package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
  93. package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
  94. package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
  95. package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
  96. package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
  97. package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
  98. package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
  99. package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
  100. package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
  103. package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
  104. package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
  106. package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
  107. package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
  108. package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
  109. package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
  110. package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
  111. package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
  112. package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
  113. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  114. package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
  115. package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
  116. package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
  117. package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
  118. package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
  119. package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
  120. package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
  121. package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
  122. package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
  123. package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
  124. package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
  125. package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
  126. package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
  127. package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
  128. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  129. package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
  130. package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
  131. package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
  132. package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
  133. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  134. package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
  135. package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
  136. package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
  137. package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
  138. package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
  139. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  140. package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
  142. package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
  143. package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
  144. package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
  145. package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
  146. package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
  147. package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
  148. package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
  149. package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
  150. package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
  151. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  152. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  153. package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
  154. package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
  155. package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
  156. package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
  157. package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
  158. package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
  159. package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
  160. package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
  161. package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
  162. package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
  163. package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
  164. package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
  165. package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
  166. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  167. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  168. package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
  169. package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
  170. package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
  171. package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
  172. package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
  173. package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
  174. package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
  175. package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
  176. package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
  177. package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
  178. package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
  181. package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
  182. package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
  183. package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
  184. package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
  185. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  186. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  187. package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
  188. package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
  189. package/node_modules/pi-lens/dist/index.js +16456 -8335
  190. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
  191. package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
  192. package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
  193. package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
  194. package/node_modules/pi-lens/dist/probes/installer.js +135 -0
  195. package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
  196. package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
  197. package/node_modules/pi-lens/docs/agent-guide.md +13 -4
  198. package/node_modules/pi-lens/docs/agent-tools.md +42 -0
  199. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
  200. package/node_modules/pi-lens/docs/configuration.md +20 -1
  201. package/node_modules/pi-lens/docs/environment-variables.md +52 -2
  202. package/node_modules/pi-lens/docs/features.md +8 -4
  203. package/node_modules/pi-lens/docs/globalconfig.md +1 -0
  204. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  205. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
  206. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  207. package/node_modules/pi-lens/docs/mcp.md +21 -5
  208. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  209. package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
  210. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  211. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
  212. package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
  213. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  214. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
  215. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  216. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  217. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  218. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  219. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  220. package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
  221. package/node_modules/pi-lens/docs/settings.md +1 -0
  222. package/node_modules/pi-lens/docs/usage.md +5 -1
  223. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  224. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  225. package/node_modules/pi-lens/package.json +18 -6
  226. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  227. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  228. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  229. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  230. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  231. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  232. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  233. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  234. package/node_modules/pi-typesafe/README.md +1 -1
  235. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  236. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  237. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  238. package/node_modules/pi-typesafe/dist/client.js +22 -7
  239. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  240. package/node_modules/pi-typesafe/dist/index.js +1 -1
  241. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  242. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  243. package/node_modules/pi-typesafe/package.json +1 -1
  244. package/node_modules/pi-web-access/CHANGELOG.md +48 -0
  245. package/node_modules/pi-web-access/README.md +57 -11
  246. package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
  247. package/node_modules/pi-web-access/curator-page.ts +5 -1
  248. package/node_modules/pi-web-access/curator-server.ts +2 -0
  249. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  250. package/node_modules/pi-web-access/degoog.ts +323 -0
  251. package/node_modules/pi-web-access/dist/index.js +3483 -2745
  252. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  253. package/node_modules/pi-web-access/exa.ts +5 -1
  254. package/node_modules/pi-web-access/extract.ts +41 -3
  255. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  256. package/node_modules/pi-web-access/gemini-search.ts +34 -11
  257. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  258. package/node_modules/pi-web-access/index.ts +153 -1202
  259. package/node_modules/pi-web-access/keenable.ts +221 -0
  260. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  261. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  262. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  263. package/node_modules/pi-web-access/openai-search.ts +34 -1
  264. package/node_modules/pi-web-access/package.json +23 -5
  265. package/node_modules/pi-web-access/perplexity.ts +64 -56
  266. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  267. package/node_modules/pi-web-access/video-extract.ts +1 -1
  268. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  269. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  270. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  271. package/package.json +17 -12
  272. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
  273. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
  274. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  275. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  276. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  277. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  278. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  279. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  280. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  281. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  282. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  283. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  284. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  285. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  286. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  287. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  288. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  289. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  290. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  291. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  292. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  293. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  294. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  295. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  296. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  297. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  298. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  299. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  300. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  301. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  302. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  303. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  304. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  305. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  306. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  307. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  308. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  309. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  310. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  311. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  312. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  313. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  314. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  315. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  316. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  317. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  318. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  319. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  320. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  321. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  322. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  323. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  324. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  325. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  326. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  327. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  328. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  329. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  330. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  331. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  332. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  333. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  334. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  335. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  336. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  337. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  338. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  339. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  340. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  341. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  342. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  343. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  344. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  345. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  346. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  347. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  348. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  349. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  350. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  351. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  352. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  353. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  354. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  355. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  356. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  357. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  358. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  359. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  360. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  361. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  362. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  363. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  364. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  365. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  366. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  367. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  368. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  369. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  370. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  371. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  372. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  373. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  374. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  375. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  376. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  377. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  378. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  379. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  380. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  381. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  382. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  383. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  384. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  385. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  386. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  387. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  388. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  389. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  390. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  391. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  392. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  393. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  394. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  395. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  396. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  397. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  398. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  399. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  400. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  401. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  402. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  403. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  404. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  405. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  406. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  407. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  447. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  448. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  449. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  450. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  451. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  452. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  453. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  454. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  455. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  456. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  457. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  458. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  459. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  460. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  461. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  462. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  463. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  464. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  465. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  466. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  467. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  468. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  469. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  470. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  471. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  472. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  473. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  474. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  475. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  476. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  477. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  478. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  479. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  480. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  481. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  482. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  483. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  484. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  485. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  486. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  487. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  488. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  489. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  490. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  491. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  492. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  493. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  494. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  495. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  496. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  497. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  498. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  499. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  500. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  501. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  502. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  503. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  504. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  505. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  506. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  507. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  508. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  509. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  510. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  511. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  512. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  513. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  514. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  515. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  516. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  517. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  518. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  519. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  520. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  521. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  522. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  523. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  524. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  525. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  526. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  527. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  528. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  529. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  530. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  531. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  532. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  533. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  534. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  535. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  536. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  537. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  538. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  539. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  540. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  541. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  542. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  543. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  544. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  545. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  546. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  547. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  548. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  549. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  550. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  551. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  552. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  553. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  554. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  555. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  556. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  557. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  558. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  559. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  560. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  561. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  562. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  563. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  564. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  565. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  566. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  567. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  568. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  569. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  570. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  571. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  572. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  573. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  574. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  575. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  576. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  577. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  578. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  579. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  580. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  581. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  582. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  583. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  584. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  585. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  586. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  587. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  588. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  589. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  590. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  591. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  592. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  593. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  594. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  595. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  596. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  597. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  598. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  599. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  600. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  601. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  602. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  603. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  604. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  605. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  606. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  607. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  608. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  609. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  610. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  611. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  612. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  613. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  614. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  615. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  616. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  617. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  618. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  619. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  620. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  621. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  623. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  624. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  625. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  626. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  627. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  628. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  629. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  630. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  631. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  632. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  633. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  634. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  635. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  636. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  637. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  638. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  639. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  640. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  641. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  642. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  643. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  644. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  645. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  646. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  647. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  648. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  649. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  650. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  651. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  652. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  653. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  654. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  655. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  656. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  657. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  658. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  659. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  660. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  661. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  662. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  663. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  664. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  665. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  666. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  667. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  668. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  669. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  670. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  671. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  672. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  673. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  674. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  675. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  676. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  677. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  678. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  679. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  680. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  681. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  682. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  683. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  684. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  685. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  686. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  687. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  688. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  689. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  690. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  691. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  692. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  693. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  694. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  695. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  696. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  697. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  698. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  699. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  700. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  701. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  702. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  703. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  704. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  705. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  706. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  707. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  708. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  709. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  710. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  711. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  712. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  713. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  714. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  715. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  716. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  717. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  718. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  719. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  720. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  721. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  722. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  723. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  724. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  725. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  726. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  727. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  728. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  729. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  730. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  731. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  732. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  733. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  734. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  735. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  736. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  737. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  738. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  739. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  740. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  741. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  742. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  743. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  744. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  745. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  746. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  747. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  748. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  749. package/node_modules/pi-lens/dist/i18n.js +0 -69
  750. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  751. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  752. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  753. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  754. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  755. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  756. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  757. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  758. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  759. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  760. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  761. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  762. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  763. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  764. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  765. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  766. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  767. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  768. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  769. package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
@@ -1,2485 +0,0 @@
1
- import * as nodeFs from "node:fs";
2
- import * as path from "node:path";
3
- import { resetBoundedTelemetry } from "./bounded-telemetry.js";
4
- import { rotateMessageEndAttribution } from "./message-end-attribution.js";
5
- import { createDeadline, yieldIfOverBudget } from "./cooperative-budget.js";
6
- import { deadCodeIssueCount } from "./dead-code-client.js";
7
- import { logDeadCodeScan } from "./dead-code-logger.js";
8
- import { incrementDegradationCount, recordDegradationOnce, resetDegradationLedger, } from "./degradation-ledger.js";
9
- import { getDiagnosticTracker } from "./diagnostic-tracker.js";
10
- import { resetPsScriptAnalyzerAvailability } from "./dispatch/runners/psscriptanalyzer.js";
11
- import { resetInstallRetryLatches } from "./dispatch/runners/utils/availability-policy.js";
12
- import { resetLazyInstallAttempts } from "./dispatch/runners/utils/lazy-installer.js";
13
- import { resetDispatchAvailabilityState } from "./dispatch/runners/utils/runner-helpers.js";
14
- import { resetObservedRunnerLatency } from "./dispatch/collect-later-tier.js";
15
- import { resetPendingRunnerFindings } from "./dispatch/pending-runner-findings.js";
16
- import { clearAllSessions as clearFileTimeSessions } from "./file-time.js";
17
- import { drainProjectDataDirMigrations, getGlobalPiLensDir, getKnipIgnorePatterns, getProjectDataDir, resetProjectDataDirSessionState, } from "./file-utils.js";
18
- import { GitleaksClient } from "./gitleaks-client.js";
19
- import { resetGoAvailability } from "./go-client.js";
20
- import { GovulncheckClient, } from "./govulncheck-client.js";
21
- import { sweepAtomicWriteStages } from "./instance-reaper.js";
22
- import { canRunStartupHeavyScans } from "./language-policy.js";
23
- import { detectProjectLanguageProfile, getDefaultStartupTools, } from "./language-profile.js";
24
- import { logLatency } from "./latency-logger.js";
25
- import { runLogCleanup } from "./log-cleanup.js";
26
- import { resetCascadeTierSessionState } from "./lsp/cascade-tier.js";
27
- import { initLSPConfig, loadLSPConfig } from "./lsp/config.js";
28
- import { resetWorkspaceDiagnosticsCacheSession } from "./lsp/workspace-diagnostics-session.js";
29
- import { resetDirectLspCommandAvailability, resetLSPCaseSensitivityState, } from "./lsp/server.js";
30
- import { loadLspService } from "./lsp-lazy.js";
31
- import { resetManagedToolRefreshSession } from "./installer/managed-tool-refresh-session.js";
32
- import { resetResolvedPathCache } from "./installer/index.js";
33
- import { _resetPackageManagerCache } from "./package-manager.js";
34
- import { clearFormatterCache } from "./formatters.js";
35
- import { isAtOrAboveHomeDir } from "./path-utils.js";
36
- import { isPrintMode } from "./print-mode.js";
37
- import { readLatestProjectSequence, readLatestProjectSequenceAsync, } from "./project-changes.js";
38
- import { getProjectSnapshotPath, getProjectSnapshotLegacyPath, hydrateRuntimeFromProjectSnapshot, hydrateRuntimeFromProjectSnapshotIfIdle, isProjectSnapshotFresh, isProjectSnapshotMetaStale, loadProjectSnapshot, loadProjectSnapshotExportsAndRules, PROJECT_SNAPSHOT_VERSION, readProjectSnapshotMeta, saveRuntimeProjectSnapshot, } from "./project-snapshot.js";
39
- import { scanProjectRules } from "./rules-scanner.js";
40
- import { resetRustAvailability } from "./rust-client.js";
41
- import { resetSafeSpawnWindowsCommandCache } from "./safe-spawn.js";
42
- import { resetAnalyzerBootstrapSessionState, } from "./bootstrap.js";
43
- import { getSlowFsVerdict, isSlowFs, slowFsDegradationNotice, } from "./slow-fs.js";
44
- import { countRecentSmells, formatSmellsSessionStartLine, resetSmellsSessionState, } from "./smells-rollup.js";
45
- import { resetSituationalToolTelemetry } from "./situational-tool-telemetry.js";
46
- import { findNearestProjectRoot, getStartupScanMaxEntries, isStartupScanVerdictFresh, resolveStartupScanContext, } from "./startup-scan.js";
47
- import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
48
- import { TrivyClient } from "./trivy-client.js";
49
- import { isWarmAttached } from "./warm-attach.js";
50
- import { setSessionLanguages } from "./widget-state.js";
51
- import { logWordIndex } from "./word-index-logger.js";
52
- import { resetOpaqueMutationState } from "./opaque-mutation-scan.js";
53
- import { primePersistedMutationAttribution, resetMutationAttribution, } from "./mutation-attribution.js";
54
- import { resetObservedMutationNet } from "./observed-mutation.js";
55
- import { resetPendingAuxiliaryCoverage } from "./lsp/pending-aux-coverage.js";
56
- import { resetWorkspaceTopology } from "./workspace-topology.js";
57
- import { resetZizmorTokenAvailability } from "./zizmor-config.js";
58
- import { resetSpawnTimeoutCooldowns } from "./spawn-timeout-cooldown.js";
59
- import { resetTestRunnerDelivery } from "./test-runner-delivery.js";
60
- import { resetLspMutationNoBridgeDbgLatch } from "./lsp-mutation.js";
61
- /**
62
- * `deps` with resolved analyzer clients merged in, or `null` when the
63
- * bootstrap could not be served and the caller must proceed without them.
64
- *
65
- * No cast and no "is one representative field present?" probe: the seam
66
- * answers with all seventeen clients or with `null`, so the two outcomes are
67
- * the two branches. A skipped consumer is already counted in the ledger under
68
- * `analyzer-bootstrap-unavailable`, so it is never mistaken for a clean one.
69
- */
70
- async function demandBootstrapDeps(deps, reason) {
71
- const clients = await deps.bootstrap.request(reason);
72
- if (!clients)
73
- return null;
74
- return { ...deps, ...clients };
75
- }
76
- const HOST_STALL_THRESHOLD_MS = 30_000;
77
- function logHostReadyDelay(deps, cwd) {
78
- if (!deps.emitHostReadyDelay ||
79
- deps.sessionStartMonotonicAt === undefined ||
80
- deps.extensionLoadedAt === undefined) {
81
- return;
82
- }
83
- const durationMs = Math.max(0, deps.sessionStartMonotonicAt - deps.extensionLoadedAt);
84
- logLatency({
85
- type: "phase",
86
- filePath: cwd,
87
- phase: "host_ready_delay",
88
- durationMs,
89
- metadata: {
90
- hostStallSuspected: durationMs > HOST_STALL_THRESHOLD_MS,
91
- sessionStart: "first-process-session",
92
- reason: deps.sessionReason,
93
- },
94
- });
95
- }
96
- function resolveSnapshotRoot(cwd) {
97
- const resolvedCwd = path.resolve(cwd);
98
- const nearest = findNearestProjectRoot(resolvedCwd);
99
- // Reject a root at — or above — $HOME (the #250/#253 escape); fall back to
100
- // the cwd so the snapshot stays scoped to the actual workspace.
101
- if (!nearest || isAtOrAboveHomeDir(nearest)) {
102
- return resolvedCwd;
103
- }
104
- return nearest;
105
- }
106
- /**
107
- * #947: meta-first staleness gate. Both interactive paths used to sync-parse
108
- * the whole `project-snapshot.json` body (110-130ms at 40MB, ~0.5s at the
109
- * observed 112MB) BEFORE checking freshness — wasted work in the 71% of
110
- * sessions where the snapshot turns out stale. Read the tiny meta sidecar
111
- * (`project-snapshot.meta.json`, written on every save) first; when it says
112
- * stale (seq or version mismatch — the exact fields `isProjectSnapshotFresh`
113
- * checks), skip the body parse entirely. Callers already tolerate a missing
114
- * snapshot (fail-open contract). A missing meta file (legacy install, or a
115
- * snapshot written before the sidecar existed) falls through to parsing the
116
- * body exactly as before.
117
- */
118
- /**
119
- * #1019: build the bounded-replay base from the snapshot's tiny meta sidecar.
120
- * The sequence index is mirrored into the meta (not just the body) precisely so
121
- * this read is cheap — reading it does NOT parse the 40-112MB body, preserving
122
- * the #947 skip-stale optimization. Returns `undefined` (→ full replay) for a
123
- * legacy meta with no embedded index, a version-mismatched meta (a foreign
124
- * snapshot generation), or a missing meta. A seq-stale meta is NOT rejected
125
- * here — folding the newer entries onto it is the whole point; the base's
126
- * trustworthiness against a truncated/ahead log is guarded inside
127
- * `readLatestProjectSequence` itself.
128
- */
129
- function snapshotSequenceBase(root) {
130
- const meta = readProjectSnapshotMeta(root);
131
- if (!meta || meta.version !== PROJECT_SNAPSHOT_VERSION)
132
- return undefined;
133
- const index = meta.sequenceIndex;
134
- if (!index)
135
- return undefined;
136
- return {
137
- projectSeq: index.projectSeq,
138
- fileSeqByPath: index.fileSeqByPath,
139
- sinceSeq: meta.seq,
140
- };
141
- }
142
- /**
143
- * #1785: one bounded ledger record per timed-out sequence read, so a real
144
- * occurrence is diagnosable from the ledger alone rather than only from a
145
- * `dbg` line that most hosts never surface. Carries the project root and the
146
- * on-disk snapshot's age/size (when one exists) — the identity AC1 asks for:
147
- * which project, how stale/large the snapshot that got skipped was.
148
- */
149
- function recordSnapshotSequenceTimeout(args) {
150
- let reason = "sequence read timed out; no snapshot on disk";
151
- try {
152
- const stat = nodeFs.statSync(args.snapshotPath);
153
- reason = `sequence read timed out; snapshot age=${Math.round(Date.now() - stat.mtimeMs)}ms size=${stat.size}b`;
154
- }
155
- catch {
156
- // No snapshot on disk yet — the reason above already covers it.
157
- }
158
- incrementDegradationCount({
159
- kind: "snapshot-sequence-read-timeout",
160
- subject: args.snapshotRoot,
161
- reason,
162
- });
163
- }
164
- /**
165
- * #1785: the synchronous freshness check made a conservative call with the
166
- * `UNKNOWN_PROJECT_SEQ` sentinel (never hydrate when the real sequence is
167
- * unknown). Once the deferred read resolves — a deterministic completion
168
- * signal, not a sleep — this re-checks freshness against the now-known
169
- * sequence and hydrates late when the snapshot really was current, instead of
170
- * leaving the runtime without `cachedExports`/`projectRulesScan` for the rest
171
- * of the session.
172
- *
173
- * #1785 review round F5 (design (a), replacing rounds 1-3's synchronous
174
- * capture entirely): quick mode's own cold-start warmup ALREADY loads the
175
- * on-disk snapshot for its own purposes — `cachedSnapshot` at the top of the
176
- * warmup body, used to reuse a still-fresh `startupScan` verdict — strictly
177
- * BEFORE the warmup's own possible save (`saveRuntimeProjectSnapshot`, only
178
- * reached when that verdict was NOT fresh). Reusing THAT already-loaded
179
- * value costs nothing new: the read was always going to happen regardless of
180
- * this fix, and it is guaranteed to predate any overwrite the SAME warmup
181
- * invocation might go on to make — a read-before-write invariant of the
182
- * warmup's own code, not something this function has to defend against with
183
- * a race window of its own. `getWarmupOwnSnapshotRead` is a closure over a
184
- * per-`handleSessionStart`-call LOCAL variable (never module-scope — #1785
185
- * F6 is exactly the class of bug a module-scope holder here would
186
- * reintroduce) that the warmup publishes into once its own read completes.
187
- *
188
- * When the getter still returns `undefined` — the deferred read resolved
189
- * before the warmup got that far (or no warmup was ever armed by this call:
190
- * a later quick-mode call in the same process, print mode, full mode) —
191
- * there is nothing published yet, so this falls back to a live re-load via
192
- * `loadProjectSnapshotExportsAndRules`. That fallback is exactly as safe as
193
- * the primary path: if the warmup hasn't reached its own read yet, it
194
- * certainly hasn't saved yet either (same invariant), so disk still holds
195
- * whatever was there before any warmup activity.
196
- *
197
- * Both the published value and the fallback re-load are the narrow,
198
- * postings-free `ProjectSnapshotExportsAndRules` shape — never the full
199
- * `loadProjectSnapshot`. This path only ever hydrates
200
- * `cachedExports`/`projectRulesScan`; it can NOT hydrate `wordIndex` even
201
- * when the underlying snapshot would otherwise have one, because the narrow
202
- * type never carries it. That is intentional, not a regression: F2's hazard
203
- * was this exact path nulling a `wordIndex` that quick mode's warmup had, in
204
- * the meantime, built for real — the warmup remains the sole source of a
205
- * late-arriving `wordIndex` for the interactive path.
206
- */
207
- function retroactivelyHydrateAfterDeferredSequence(args) {
208
- return (latestSeq) => {
209
- const published = args.getWarmupOwnSnapshotRead();
210
- const snapshot = published !== undefined
211
- ? published
212
- : loadProjectSnapshotExportsAndRules(args.snapshotRoot);
213
- if (!snapshot ||
214
- snapshot.version !== PROJECT_SNAPSHOT_VERSION ||
215
- snapshot.seq !== latestSeq.projectSeq) {
216
- return;
217
- }
218
- const hydrated = hydrateRuntimeFromProjectSnapshotIfIdle(args.runtime, snapshot);
219
- if (hydrated) {
220
- args.dbg(`session_start: deferred sequence read confirmed snapshot freshness — hydrating cachedExports/projectRulesScan late (seq=${snapshot.seq})`);
221
- }
222
- else {
223
- args.dbg(`session_start: deferred sequence read confirmed snapshot freshness (seq=${snapshot.seq}), but the runtime already has live state (warmup or another task got there first) — skipping late hydration to avoid clobbering it`);
224
- }
225
- };
226
- }
227
- function loadSnapshotBodyUnlessStale(args) {
228
- const meta = readProjectSnapshotMeta(args.root);
229
- if (meta && isProjectSnapshotMetaStale(meta, args.currentProjectSeq)) {
230
- args.dbg(`project_snapshot: meta gate stale (metaSeq=${meta.seq} metaVersion=${meta.version} current=${args.currentProjectSeq}) — skipping body parse`);
231
- return { snapshot: null, skippedStale: true };
232
- }
233
- return {
234
- snapshot: loadProjectSnapshot(args.root),
235
- skippedStale: false,
236
- };
237
- }
238
- function describeSnapshotMiss(snapshot, currentProjectSeq, args) {
239
- if (args.skippedStale)
240
- return "stale-meta-gate";
241
- if (!snapshot)
242
- return args.bodyPresent ? "invalid-body" : "missing";
243
- if (snapshot.seq !== currentProjectSeq) {
244
- return `stale(seq=${snapshot.seq}, current=${currentProjectSeq})`;
245
- }
246
- // Defensive-only arm: parseSnapshot rejects incompatible versions, and a
247
- // same-sequence parsed snapshot is fresh. Keep this classification explicit
248
- // so a future loader change cannot collapse it into a generic miss.
249
- return "incompatible";
250
- }
251
- function logProjectSnapshotProbe(args) {
252
- args.dbg(`project_snapshot: probe root=${args.root} path=${getProjectSnapshotPath(args.root)} currentSeq=${args.currentProjectSeq}`);
253
- if (isProjectSnapshotFresh(args.snapshot, args.currentProjectSeq)) {
254
- args.dbg(`project_snapshot: loaded seq=${args.snapshot.seq} exports=${args.snapshot.cachedExports.length} files=${Object.keys(args.snapshot.files ?? {}).length} reverseDeps=${Object.keys(args.snapshot.reverseDeps ?? {}).length} startupScan=${Boolean(args.snapshot.startupScan)} languageProfile=${Boolean(args.snapshot.languageProfile)}`);
255
- }
256
- else {
257
- args.dbg(`project_snapshot: miss reason=${args.missReason}`);
258
- }
259
- }
260
- function resolveStartupMode(projectConfig, globalConfig) {
261
- const envMode = (process.env.PI_LENS_STARTUP_MODE ?? "").trim().toLowerCase();
262
- if (envMode === "full" || envMode === "minimal" || envMode === "quick") {
263
- return envMode;
264
- }
265
- const configMode = projectConfig?.startup?.mode ?? globalConfig?.startup?.mode;
266
- if (configMode)
267
- return configMode;
268
- if (isPrintMode()) {
269
- return "quick";
270
- }
271
- return "full";
272
- }
273
- // --- Session-start helpers ---
274
- // #1162: bound the session_start sequence read to this budget. The default
275
- // mirrors the issue's ~250ms figure; overridable for tests (mirrors the
276
- // `PI_LENS_WARMUP_DELAY_MS` precedent above) so a regression can use a tiny
277
- // budget instead of racing a real 250ms wall-clock wait.
278
- function sequenceReadBudgetMs() {
279
- const raw = Number(process.env.PI_LENS_SEQUENCE_READ_BUDGET_MS ?? 250);
280
- return Number.isFinite(raw) && raw > 0 ? raw : 250;
281
- }
282
- // #1162 review follow-up (P3): the sentinel used ONLY for the snapshot
283
- // freshness check (`isProjectSnapshotFresh`/`isProjectSnapshotMetaStale`,
284
- // both `=== currentProjectSeq` equality) when a sequence read timed out.
285
- // Every real `projectSeq`/`snapshot.seq` is >= 0 (both derive from
286
- // `Math.max(0, ...)`/a monotonic fold starting at 0), so this value can
287
- // never collide with a legitimate seq — including a project's real
288
- // first-ever snapshot persisted at `seq === 0`, which the cold-seed's OWN
289
- // `projectSeq: 0` would otherwise alias. Deliberately kept separate from
290
- // `runtime.projectSeq` (which a timed-out read still seeds to plain `0`):
291
- // the reseed-advancement guard on the deferred continuation below needs
292
- // `runtime.projectSeq` to read exactly 0 immediately after a cold seed so
293
- // `> 0` reliably means "a bump happened in the stall window", which this
294
- // sentinel is never fed into.
295
- const UNKNOWN_PROJECT_SEQ = -1;
296
- /**
297
- * Bound the sequence read that gates snapshot freshness (#1162). Normally
298
- * ~2ms, but the underlying read is a blocking `fs.readFileSync` that can
299
- * balloon under host I/O pressure (2125ms observed) with NO escape hatch —
300
- * and a `setTimeout`/`Promise.race` timeout can never preempt a *synchronous*
301
- * read (the thread only returns to the event loop once the OS call
302
- * returns). The fix is to read asynchronously (`readLatestProjectSequenceAsync`,
303
- * which uses `fs.promises.readFile` and genuinely yields) so a timeout CAN
304
- * win the race.
305
- *
306
- * On the healthy path (the overwhelming common case) the async read settles
307
- * first and this adds ~zero overhead beyond one microtask hop. On a stalled
308
- * read, the timeout wins: the caller proceeds immediately with a cold
309
- * sequence (`{ projectSeq: 0, fileSeqByPath: empty }` — byte-identical to
310
- * what a full replay of a missing/empty log already produces, so this is
311
- * exactly the existing cold-start case, not a new code path) while the real
312
- * read keeps running in the background. When it resolves, it re-seeds the
313
- * runtime so the warm-start optimization is recovered for the NEXT
314
- * session_start/turn instead of lost for the rest of the process — unless
315
- * the session has since moved on (`runtime.isCurrentSession`) or this is a
316
- * one-shot `pi --print` process (`isPrintMode()`), which has no future
317
- * session in-process to benefit and where letting background work outlive
318
- * settle is exactly the referenced-handle retention class #1154/#1153 just
319
- * closed. The timeout timer itself is `.unref()`'d and always cleared on
320
- * both race outcomes, so it never holds the process open either.
321
- *
322
- * Never silent (shape 10): the caller's `session_start_sequence_read`
323
- * latency line always carries `timedOut`, and a successful deferred reseed
324
- * logs its own `session_start_sequence_read_deferred_reseed` phase.
325
- */
326
- async function readSequenceWithBudget(args) {
327
- const { snapshotRoot, base, cwd, runtime, sessionGeneration, dbg, onDeferredSequenceResolved, } = args;
328
- const readPromise = readLatestProjectSequenceAsync(snapshotRoot, base);
329
- let timeoutHandle;
330
- const timeoutPromise = new Promise((resolve) => {
331
- timeoutHandle = setTimeout(() => resolve({ timedOut: true }), sequenceReadBudgetMs());
332
- timeoutHandle.unref();
333
- });
334
- const raced = await Promise.race([
335
- readPromise.then((latestSeq) => ({ timedOut: false, latestSeq })),
336
- timeoutPromise,
337
- ]);
338
- if (timeoutHandle)
339
- clearTimeout(timeoutHandle);
340
- if (!raced.timedOut) {
341
- return { latestSeq: raced.latestSeq, timedOut: false };
342
- }
343
- dbg(`session_start: sequence read exceeded ${sequenceReadBudgetMs()}ms budget — falling back to cold-start sequence`);
344
- if (!isPrintMode()) {
345
- readPromise
346
- .then((latestSeq) => {
347
- if (!runtime.isCurrentSession(sessionGeneration))
348
- return;
349
- // #1162 review follow-up (P3): `isCurrentSession` alone catches a
350
- // CROSS-session move-on but not a SAME-session advancement in the
351
- // stall window. Interleaving: timeout -> cold seed sets
352
- // projectSeq=0 -> handler returns -> agent edits file X ->
353
- // `bumpFileSeq(X)` advances `_projectSeq`/`_fileSeq[X]` -> this
354
- // deferred read completes. Reseeding here would blindly `.clear()`
355
- // and repopulate `_fileSeq`/`_fileLastProjectSeq` from the STALE
356
- // pre-edit snapshot, erasing the in-window bump for file X (a
357
- // transient inconsistency that self-heals into a full sweep per
358
- // `seedProjectSequence`'s empty-changed-map invariant — safe, but
359
- // worth not doing). The cold seed always sets `projectSeq` to
360
- // exactly 0, so `runtime.projectSeq > 0` here can ONLY be true if a
361
- // `bumpFileSeq` ran since — i.e. an in-window edit — never a false
362
- // positive from a legitimately-empty log (which leaves it at 0, and
363
- // the reseed below is then still wanted: it recovers the real,
364
- // possibly non-empty, result that arrived too late for the budget).
365
- if (runtime.projectSeq > 0) {
366
- dbg("session_start: deferred sequence read completed, but the session already advanced (in-window edit) — skipping reseed to avoid clobbering it");
367
- return;
368
- }
369
- const reseedStartedAt = Date.now();
370
- runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
371
- logLatency({
372
- type: "phase",
373
- phase: "session_start_sequence_read_deferred_reseed",
374
- filePath: cwd,
375
- startedAt: new Date(reseedStartedAt).toISOString(),
376
- durationMs: Date.now() - reseedStartedAt,
377
- metadata: { entries: latestSeq.fileSeqByPath.size, deferred: true },
378
- });
379
- dbg(`session_start: deferred sequence read completed — reseeded projectSeq=${latestSeq.projectSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
380
- onDeferredSequenceResolved?.(latestSeq);
381
- })
382
- .catch((err) => {
383
- dbg(`session_start: deferred sequence read failed: ${err}`);
384
- });
385
- }
386
- return {
387
- latestSeq: { projectSeq: 0, fileSeqByPath: new Map() },
388
- timedOut: true,
389
- };
390
- }
391
- async function igniteWarmFiles(cwd, warmFiles, runtime, sessionGeneration, dbg) {
392
- try {
393
- dbg(`session_start lsp-warm: ${warmFiles.length} warm file(s) configured`);
394
- await initLSPConfig(cwd);
395
- if (!runtime.isCurrentSession(sessionGeneration))
396
- return;
397
- const lspService = (await loadLspService()).getLSPService();
398
- const total = warmFiles.length;
399
- let loaded = 0;
400
- let errors = 0;
401
- for (const relPath of warmFiles) {
402
- if (!runtime.isCurrentSession(sessionGeneration))
403
- return;
404
- const filePath = path.isAbsolute(relPath)
405
- ? relPath
406
- : path.resolve(cwd, relPath);
407
- if (!nodeFs.existsSync(filePath)) {
408
- dbg(`session_start lsp-warm: not found: ${relPath}`);
409
- errors++;
410
- continue;
411
- }
412
- try {
413
- const content = nodeFs.readFileSync(filePath, "utf-8");
414
- await lspService.touchFile(filePath, content, {
415
- diagnostics: "none",
416
- source: "startup-warm",
417
- clientScope: "primary",
418
- maxClientWaitMs: 2000,
419
- });
420
- loaded++;
421
- }
422
- catch (err) {
423
- dbg(`session_start lsp-warm: error ${relPath}: ${err}`);
424
- errors++;
425
- }
426
- }
427
- dbg(`session_start lsp-warm: ${loaded}/${total} opened (${errors} err)`);
428
- }
429
- catch (err) {
430
- dbg(`session_start lsp-warm: config/init error: ${err}`);
431
- }
432
- }
433
- /**
434
- * Fallback warm when a project has no explicit `warmFiles`: pre-spawn the LSP
435
- * for the project's *dominant* language (highest source-file count) by opening
436
- * one representative file. This eliminates the cold-spawn stall the first edit
437
- * would otherwise pay (`lsp_client_wait_timeout`, observed up to 5s on
438
- * TypeScript/Deno). Only a single server is warmed — launching every detected
439
- * language's server at once (rust-analyzer + gopls + tsserver …) would spike CPU
440
- * and the event loop at startup, working against the very latency we protect.
441
- * Projects that want more pre-warming can list explicit `warmFiles` (#203).
442
- */
443
- async function igniteDominantLanguageWarm(cwd, runtime, sessionGeneration, dbg) {
444
- try {
445
- await initLSPConfig(cwd);
446
- if (!runtime.isCurrentSession(sessionGeneration))
447
- return;
448
- const lspService = (await loadLspService()).getLSPService();
449
- const { collectSourceFilesAsync } = await import("./source-filter.js");
450
- const { CODE_KINDS, detectFileKind } = await import("./file-kinds.js");
451
- // Async, event-loop-yielding walk (deferred off the interactive path).
452
- // inspectGeneratedHeaders:false keeps the walk to directory reads only — no
453
- // per-file content opens — so we never hold a file handle (cheaper, and it
454
- // can't collide with concurrent fs teardown). Picking a representative file
455
- // doesn't need generated-banner filtering.
456
- const files = await collectSourceFilesAsync(cwd, {
457
- inspectGeneratedHeaders: false,
458
- });
459
- if (!runtime.isCurrentSession(sessionGeneration))
460
- return;
461
- // Rank languages by source-file count. Computed here from the scan rather
462
- // than reused from languageProfile.counts, which is left empty on the
463
- // no-warm-caches startup path (detectProjectLanguageProfile is called with
464
- // an empty file list there).
465
- const counts = new Map();
466
- for (const f of files) {
467
- const kind = detectFileKind(f);
468
- if (kind)
469
- counts.set(kind, (counts.get(kind) ?? 0) + 1);
470
- }
471
- if (counts.size === 0) {
472
- dbg("session_start lsp-warm: no detected languages to auto-warm");
473
- return;
474
- }
475
- const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
476
- // #894 review: data/doc kinds (json/yaml/markdown/…) have builtin LSP
477
- // servers too, and broadened enumeration makes them countable — so a TS
478
- // repo with more .json/.yml than .ts files would otherwise warm the
479
- // json/yaml server while tsserver still pays the ~5s cold-spawn stall
480
- // this warm exists to prevent (#203). Rank CODE_KINDS first; non-code
481
- // kinds stay as a fallback so a pure-config/docs repo still warms its
482
- // dominant server.
483
- const warmOrder = [
484
- ...ranked.filter(([kind]) => CODE_KINDS.has(kind)),
485
- ...ranked.filter(([kind]) => !CODE_KINDS.has(kind)),
486
- ];
487
- // Walk languages by descending count; warm the first that has both an LSP
488
- // server and a representative on-disk file.
489
- for (const [kind] of warmOrder) {
490
- const sample = files.find((f) => detectFileKind(f) === kind && lspService.supportsLSP(f));
491
- if (!sample)
492
- continue;
493
- const content = await nodeFs.promises.readFile(sample, "utf-8");
494
- if (!runtime.isCurrentSession(sessionGeneration))
495
- return;
496
- await lspService.touchFile(sample, content, {
497
- diagnostics: "none",
498
- source: "startup-warm-dominant",
499
- clientScope: "primary",
500
- maxClientWaitMs: 2000,
501
- });
502
- dbg(`session_start lsp-warm: dominant=${kind} via ${path.basename(sample)}`);
503
- return;
504
- }
505
- dbg("session_start lsp-warm: no dominant-language LSP file found to warm");
506
- }
507
- catch (err) {
508
- dbg(`session_start lsp-warm: dominant warm error: ${err}`);
509
- }
510
- }
511
- function firePreinstallDefaults(ensureTool, dbg, startupDefaults) {
512
- for (const tool of startupDefaults) {
513
- const startedAt = Date.now();
514
- dbg(`session_start preinstall ${tool}: start`);
515
- ensureTool(tool)
516
- .then((toolPath) => {
517
- if (toolPath) {
518
- dbg(`session_start: ${tool} ready at ${toolPath}`);
519
- dbg(`session_start preinstall ${tool}: success (${Date.now() - startedAt}ms)`);
520
- }
521
- else {
522
- dbg(`session_start: ${tool} installation unavailable`);
523
- dbg(`session_start preinstall ${tool}: unavailable (${Date.now() - startedAt}ms)`);
524
- }
525
- })
526
- .catch((err) => {
527
- dbg(`session_start: ${tool} pre-install error: ${err}`);
528
- dbg(`session_start preinstall ${tool}: error (${Date.now() - startedAt}ms)`);
529
- });
530
- }
531
- }
532
- /**
533
- * Default delay before the managed-tool version refresh runs (#1730). Long
534
- * enough that `session_start`, the startup scans, and the tool preinstalls have
535
- * all settled — the refresh may spawn one `npm update`, and that must never
536
- * compete with the work the user is waiting on.
537
- */
538
- const MANAGED_TOOL_REFRESH_DELAY_MS = 30_000;
539
- /**
540
- * Schedule the periodic managed-tool refresh (#1730) on an unref'd background
541
- * timer.
542
- *
543
- * `session_start`'s own cost here is a single `setTimeout` registration: the
544
- * refresh module is not even imported until the timer fires. The timer is
545
- * unref'd so a one-shot process exits without waiting for it, matching the
546
- * warmup timer above. Whether a refresh actually runs is decided inside
547
- * `runManagedToolRefresh` against a PERSISTED per-tool stamp, so a launch loop
548
- * cannot escalate the weekly cadence.
549
- */
550
- function scheduleManagedToolRefresh(dbg) {
551
- const delayMs = Number(process.env.PI_LENS_TOOL_REFRESH_DELAY_MS ?? MANAGED_TOOL_REFRESH_DELAY_MS);
552
- const timer = setTimeout(() => {
553
- void (async () => {
554
- try {
555
- const refresh = await import("./installer/managed-tool-refresh.js");
556
- const outcome = await refresh.runManagedToolRefresh();
557
- if (outcome.skipped) {
558
- dbg(`session_start tool-refresh: skipped (${outcome.skipped})`);
559
- return;
560
- }
561
- for (const result of outcome.refreshed) {
562
- dbg(`session_start tool-refresh: ${result.toolId} ${result.ok ? "ok" : "failed"}` +
563
- `${result.changed ? ` ${result.previousVersion ?? "unknown"} → ${result.currentVersion}` : " (unchanged)"}`);
564
- }
565
- }
566
- catch (err) {
567
- dbg(`session_start tool-refresh: error ${err}`);
568
- }
569
- })();
570
- }, Number.isFinite(delayMs) ? delayMs : MANAGED_TOOL_REFRESH_DELAY_MS);
571
- timer.unref?.();
572
- }
573
- async function probePrettierInstall(ensureTool, dbg, analysisRoot) {
574
- const pkgPath = path.join(analysisRoot, "package.json");
575
- try {
576
- const raw = await nodeFs.promises.readFile(pkgPath, "utf-8");
577
- const pkg = JSON.parse(raw);
578
- const usesPrettier = !!pkg.devDependencies?.prettier ||
579
- !!pkg.dependencies?.prettier ||
580
- pkg.prettier !== undefined;
581
- if (usesPrettier) {
582
- dbg("session_start: project uses prettier, ensuring install...");
583
- ensureTool("prettier")
584
- .then((p) => {
585
- if (p)
586
- dbg(`session_start: prettier ready at ${p}`);
587
- else
588
- dbg("session_start: prettier install failed silently");
589
- })
590
- .catch((err) => dbg(`session_start: prettier install error: ${err}`));
591
- }
592
- }
593
- catch {
594
- // no package.json at cwd root
595
- }
596
- }
597
- /** Scan one file via the per-file API, pushing any items. Tolerates an
598
- * unreadable file and a scanner without `scanFile` (no-op). */
599
- function scanOneTodoFile(scanner, filePath, items) {
600
- if (typeof scanner.scanFile !== "function")
601
- return;
602
- try {
603
- // scanFile returns TodoItem[] directly (not a { items } result).
604
- const result = scanner.scanFile(filePath);
605
- if (Array.isArray(result))
606
- items.push(...result);
607
- }
608
- catch {
609
- // Per-file error: skip and continue (matches scanDirectory's tolerance).
610
- }
611
- }
612
- /** Collect the TODO baseline without blocking: enumerate source files and scan
613
- * them per-file, yielding to the event loop every 30 files. Falls back to the
614
- * blocking `scanDirectory` if the chunked path can't run (import error or a
615
- * scanner without `scanFile`). */
616
- async function collectTodoBaselineItems(scanner, analysisRoot, stillCurrent) {
617
- const items = [];
618
- try {
619
- const { getSourceFilesAsync } = await import("./scan-utils.js");
620
- // Enumerate with the chunked-yield walker so the file collection itself
621
- // (the previously-synchronous ~1.5s burst on a 2k-file tree) no longer
622
- // blocks the event loop before the per-file scan loop below even starts.
623
- const files = await getSourceFilesAsync(analysisRoot, true);
624
- if (!stillCurrent())
625
- return items;
626
- const deadline = createDeadline(8);
627
- for (const file of files) {
628
- if (!stillCurrent())
629
- return items;
630
- scanOneTodoFile(scanner, file, items);
631
- // scanFile cost scales with the file contents, so a count cadence cannot
632
- // bound the event-loop block across differently-sized corpora.
633
- await yieldIfOverBudget(deadline);
634
- }
635
- }
636
- catch {
637
- const todoResult = scanner.scanDirectory(analysisRoot);
638
- items.push(...todoResult.items);
639
- }
640
- return items;
641
- }
642
- async function buildOrRefreshWordIndex(args) {
643
- const { runtime, sessionGeneration, analysisRoot, snapshotRoot, dbg } = args;
644
- if (!runtime.isCurrentSession(sessionGeneration))
645
- return;
646
- const startMs = Date.now();
647
- let rebuildPreflightFiles;
648
- const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
649
- const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
650
- const snapshotLoadStartMs = Date.now();
651
- const snapshot = loadProjectSnapshot(snapshotRoot);
652
- const snapshotLoadMs = Date.now() - snapshotLoadStartMs;
653
- if (snapshot?.wordIndex) {
654
- const { deserializeWordIndex, refreshWordIndexIncrementally, countWordIndexPostingEntries, estimateWordIndexResidentBytes, } = await import("./word-index.js");
655
- const deserializeStartMs = Date.now();
656
- const index = deserializeWordIndex(snapshot.wordIndex);
657
- const deserializeMs = Date.now() - deserializeStartMs;
658
- if (index) {
659
- try {
660
- const result = await refreshWordIndexIncrementally(index, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
661
- if (!runtime.isCurrentSession(sessionGeneration))
662
- return;
663
- if (result.mode === "full-required") {
664
- rebuildPreflightFiles = result.preflightFiles;
665
- dbg(`session_start word-index: incremental preflight selected full rebuild (${result.reason})`);
666
- logWordIndex({
667
- phase: "incremental_fallback",
668
- cwd: snapshotRoot,
669
- trigger: "session_start",
670
- reason: result.reason,
671
- phaseDurationsMs: {
672
- snapshotLoadMs,
673
- deserializeMs,
674
- ...result.timings,
675
- },
676
- });
677
- }
678
- else {
679
- runtime.wordIndex = index;
680
- let snapshotSaveSyncMs = 0;
681
- // A stale project seq must be advanced even when mtimes prove every
682
- // indexed document reusable. Fresh snapshots with no changes avoid
683
- // an unnecessary rewrite of the large shared snapshot.
684
- if (!isProjectSnapshotFresh(snapshot, effectiveSeq) ||
685
- result.refreshed > 0 ||
686
- result.dropped > 0 ||
687
- snapshot.wordIndex.truncated !== index.truncated) {
688
- const serializeSaveEnqueueStartMs = Date.now();
689
- saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
690
- snapshotSaveSyncMs = Date.now() - serializeSaveEnqueueStartMs;
691
- }
692
- const phases = `snapshot-load=${snapshotLoadMs}ms, deserialize=${deserializeMs}ms, ` +
693
- `source-walk=${result.timings.sourceWalkMs}ms, stat-walk=${result.timings.statWalkMs}ms, ` +
694
- `refresh-reads=${result.timings.refreshReadsMs}ms, ` +
695
- `snapshot-save-sync=${snapshotSaveSyncMs}ms`;
696
- 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})`);
697
- // M2, #958: the incremental-vs-full decision + honest coverage
698
- // (indexedFileCount/truncated/skipped), independent of host `dbg`.
699
- logWordIndex({
700
- phase: "incremental_refresh",
701
- cwd: snapshotRoot,
702
- trigger: "session_start",
703
- durationMs: Date.now() - startMs,
704
- indexedFileCount: index.docCount,
705
- tokens: index.postings.size,
706
- postingEntries: countWordIndexPostingEntries(index),
707
- residentBytes: estimateWordIndexResidentBytes(index),
708
- truncated: index.truncated,
709
- phaseDurationsMs: {
710
- snapshotLoadMs,
711
- deserializeMs,
712
- ...result.timings,
713
- snapshotSaveSyncMs,
714
- },
715
- refreshed: result.refreshed,
716
- dropped: result.dropped,
717
- skipped: result.skipped,
718
- reused: result.reused,
719
- });
720
- return result;
721
- }
722
- }
723
- catch (err) {
724
- // Supersession is an expected transition, not a failure: a newer
725
- // session_start bumped the generation mid-refresh. Return undefined the
726
- // way master's synchronous path did instead of reporting a fallback and
727
- // re-walking for a rebuild whose result this generation may not publish.
728
- if (!runtime.isCurrentSession(sessionGeneration)) {
729
- dbg("session_start word-index: incremental refresh superseded");
730
- return;
731
- }
732
- dbg(`session_start word-index: incremental refresh failed; falling back to full rebuild (${err})`);
733
- logWordIndex({
734
- phase: "incremental_fallback",
735
- cwd: snapshotRoot,
736
- trigger: "session_start",
737
- reason: err instanceof Error ? err.message : String(err),
738
- phaseDurationsMs: { snapshotLoadMs, deserializeMs },
739
- });
740
- }
741
- }
742
- }
743
- // Shared file-walk-and-read helper (#348): the ONE collectWordIndexDocs
744
- // implementation backs this task, the quick-mode warmup call below, AND the
745
- // stateless cold-query background trigger in word-index.ts — a bound/skip
746
- // -rule change lands once, not in three copies.
747
- const { buildWordIndexAsync, collectWordIndexDocs, countWordIndexPostingEntries, estimateWordIndexResidentBytes, } = await import("./word-index.js");
748
- const docs = await collectWordIndexDocs(analysisRoot, () => runtime.isCurrentSession(sessionGeneration), rebuildPreflightFiles);
749
- if (!runtime.isCurrentSession(sessionGeneration))
750
- return;
751
- // #1197 review finding 2: `buildWordIndexAsync` THROWS on supersession, and
752
- // the quick-mode warmup caller (below) awaits this function OUTSIDE any
753
- // per-step try/catch — an escaping throw there skips the rest of warmup,
754
- // including the #947 LSP pre-warm, and resets `__piLensWarmupScheduled`.
755
- // Supersession is an expected transition (master's synchronous path just
756
- // returned), so absorb it here and let the caller continue.
757
- let rebuiltIndex;
758
- try {
759
- rebuiltIndex = await buildWordIndexAsync(docs, () => runtime.isCurrentSession(sessionGeneration));
760
- }
761
- catch (err) {
762
- if (!runtime.isCurrentSession(sessionGeneration)) {
763
- dbg("session_start word-index: rebuild superseded");
764
- return;
765
- }
766
- throw err;
767
- }
768
- if (!runtime.isCurrentSession(sessionGeneration))
769
- return;
770
- runtime.wordIndex = rebuiltIndex;
771
- saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
772
- dbg(`session_start word-index: rebuilt (absent/stale, seq=${effectiveSeq}) ` +
773
- `${runtime.wordIndex.docCount} files, ${runtime.wordIndex.postings.size} tokens (${Date.now() - startMs}ms)`);
774
- // M2, #958: full-rebuild decision + honest coverage. `docs.skipped` (L1)
775
- // counts files the walk saw but could not index (unreadable / over the byte
776
- // cap) so a partial index is never reported as complete (#533).
777
- logWordIndex({
778
- phase: "full_rebuild",
779
- cwd: snapshotRoot,
780
- trigger: "session_start",
781
- durationMs: Date.now() - startMs,
782
- indexedFileCount: runtime.wordIndex.docCount,
783
- tokens: runtime.wordIndex.postings.size,
784
- postingEntries: countWordIndexPostingEntries(runtime.wordIndex),
785
- residentBytes: estimateWordIndexResidentBytes(runtime.wordIndex),
786
- truncated: runtime.wordIndex.truncated,
787
- skipped: docs.skipped,
788
- });
789
- return {
790
- mode: "full",
791
- refreshed: runtime.wordIndex.docCount,
792
- dropped: 0,
793
- skipped: docs.skipped,
794
- reused: 0,
795
- };
796
- }
797
- // Fire off heavy scans as background tasks — don't block session start.
798
- // Each consumer already handles the "not ready yet" case gracefully
799
- // (cachedExports.size > 0, cache miss paths).
800
- function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg) {
801
- // #2467: the analyzers these scans drive are loaded HERE, on the deferred
802
- // background path, instead of before the handler ran. This function was
803
- // already fire-and-forget ("don't block session start"), so resolving them
804
- // across one await adds no wait to the interactive path and changes no
805
- // ordering its caller depends on — the caller never awaited it.
806
- void (async () => {
807
- const resolved = await demandBootstrapDeps(deps, "session-start-scans");
808
- // Fail open: no analyzers means no scans, said out loud. The demand is
809
- // already counted in the ledger, so this is not a silent skip.
810
- if (!resolved) {
811
- dbg("session_start scans: analyzer bootstrap unavailable — skipped");
812
- return;
813
- }
814
- scheduleStartupScansWithClients(resolved, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg);
815
- })();
816
- }
817
- /** The scan bodies, once the analyzers they need are resolved. */
818
- function scheduleStartupScansWithClients(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg) {
819
- const { todoScanner, cacheManager, knipClient, jscpdClient, deadCodeClients, govulncheckClient, gitleaksClient, trivyClient, opengrepClient, astGrepClient, depChecker, } = deps;
820
- const analyzerEnabled = (flag) => !deps.getFlag(flag);
821
- if (!analyzerEnabled("no-complexity")) {
822
- dbg("session_start complexity: skipped (disabled by config)");
823
- recordDegradationOnce({
824
- kind: "startup-analyzer-disabled",
825
- subject: "complexity",
826
- reason: "skipped (disabled by config)",
827
- });
828
- }
829
- // Some background scans are CPU-heavy and arrive on the event loop
830
- // just as the user is most likely typing (right after /new). Defer
831
- // those by a few seconds so the perceptible 50-100ms sync bursts they
832
- // contain land during LLM streaming idle time instead. All other
833
- // tasks (those not listed here) run on the next `setImmediate` tick
834
- // as before. The delays are deliberately staggered (200ms apart) so
835
- // two heavy tasks don't both run on the same macrotask.
836
- const taskDeferMsByName = {
837
- "call-graph": 5000,
838
- "codebase-model": 5200,
839
- "ast-grep exports": 5400,
840
- "project index": 5400,
841
- };
842
- const runTask = (name, task) => {
843
- const queuedAt = Date.now();
844
- dbg(`session_start task ${name}: scheduled`);
845
- runtime.markStartupScanInFlight(name, sessionGeneration);
846
- const completion = new Promise((resolve) => {
847
- const fire = () => {
848
- const startedAt = Date.now();
849
- dbg(`session_start task ${name}: start queuedMs=${startedAt - queuedAt}`);
850
- void task()
851
- .then(() => {
852
- dbg(`session_start task ${name}: success runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
853
- })
854
- .catch((err) => {
855
- dbg(`session_start: ${name} background scan failed: ${err}`);
856
- dbg(`session_start task ${name}: failed runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
857
- })
858
- .finally(() => {
859
- runtime.clearStartupScanInFlight(name, sessionGeneration);
860
- dbg(`session_start task ${name}: end`);
861
- resolve();
862
- });
863
- };
864
- const delay = taskDeferMsByName[name] ?? 0;
865
- if (delay > 0) {
866
- // #1154: unref to match the repo-wide convention that every
867
- // background timer is `.unref()`'d. Full-mode deferred scans only
868
- // run in long-lived/interactive sessions today (never a one-shot),
869
- // so this is defensive — but a referenced 5s timer would keep a
870
- // process open were a full-mode one-shot ever introduced.
871
- setTimeout(fire, delay).unref?.();
872
- }
873
- else {
874
- setImmediate(fire);
875
- }
876
- });
877
- return completion;
878
- };
879
- const canRunJsTsHeavyScans = canRunStartupHeavyScans(languageProfile, "jsts");
880
- const scanNames = ["todo", "dead-code"];
881
- if (canRunJsTsHeavyScans) {
882
- scanNames.push("knip", "jscpd", "ast-grep exports", "project index");
883
- }
884
- dbg(`session_start: launching background scans (${scanNames.join(", ")})`);
885
- void runTask("todo", async () => {
886
- if (!runtime.isCurrentSession(sessionGeneration))
887
- return;
888
- // The original implementation called todoScanner.scanDirectory(), which
889
- // walks the project synchronously and freezes the TUI for ~3s on a 2k-file
890
- // project. collectTodoBaselineItems re-implements the walk in async chunks
891
- // (per-file scan, yielding every 30 files), falling back to scanDirectory.
892
- const items = await collectTodoBaselineItems(todoScanner, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
893
- if (!runtime.isCurrentSession(sessionGeneration))
894
- return;
895
- dbg(`session_start TODO scan: ${items.length} items (baseline stored)`);
896
- cacheManager.writeCache("todo-baseline", { items }, analysisRoot);
897
- });
898
- if (!canRunJsTsHeavyScans) {
899
- dbg("session_start: skipping JS/TS startup scans (requires JS/TS language + project config)");
900
- return;
901
- }
902
- // #462: knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep each spawn an
903
- // external CLI that walks the whole project tree on its own — a walk we
904
- // don't control or get to route to an async collector. On a measured-slow
905
- // filesystem that walk reproduces the exact multi-second freeze this
906
- // feature exists to prevent, so skip exactly those seven with a visible
907
- // reason instead of leaving the agent to read an empty/stale cache as
908
- // "clean". The other scans in this function (todo above; call-graph/
909
- // codebase-model/ast-grep-exports/word-index below) walk via
910
- // `collectSourceFilesAsync` or build from cached review-graph data, so
911
- // they stay on.
912
- //
913
- // #449 slice 0: the same gate also fires inside a nicobailon/pi-subagents
914
- // child `pi` process (`PI_SUBAGENT_CHILD=1`) — a fan-out of N subagents in
915
- // the same cwd otherwise pays N full heavyweight-scan fleets for
916
- // short-lived task agents that rarely consult them. In-process scans stay
917
- // on for the same reason as slow-FS: the subagent may still use symbol
918
- // search / word-index.
919
- const isSubagent = isSubagentSession();
920
- const skipHeavyweightScans = isSlowFs(analysisRoot) || isSubagent;
921
- const runHeavyweightTask = (name, task) => {
922
- const flag = `no-${name}`;
923
- if (name !== "opengrep" && name !== "trivy" && !analyzerEnabled(flag)) {
924
- dbg(`session_start ${name}: skipped (disabled by config)`);
925
- recordDegradationOnce({
926
- kind: "startup-analyzer-disabled",
927
- subject: name,
928
- reason: "skipped (disabled by config)",
929
- });
930
- return;
931
- }
932
- if (skipHeavyweightScans)
933
- return;
934
- void runTask(name, task);
935
- };
936
- if (skipHeavyweightScans) {
937
- dbg(`session_start: skipping knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep (${isSubagent ? "subagent" : "slow-fs"})`);
938
- deps.notify(`⏭️ Skipped background code-quality scans (knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep): ${isSubagent ? subagentLightModeNotice() : slowFsDegradationNotice()}`, "info");
939
- }
940
- // Knip — dead code / unused exports
941
- runHeavyweightTask("knip", async () => {
942
- if (!runtime.isCurrentSession(sessionGeneration))
943
- return;
944
- const cached = cacheManager.readCache("knip", analysisRoot);
945
- if (cached) {
946
- if (!runtime.isCurrentSession(sessionGeneration))
947
- return;
948
- dbg(`session_start Knip: cache hit (${Math.round((Date.now() - new Date(cached.meta.timestamp).getTime()) / 1000)}s ago)`);
949
- }
950
- else {
951
- // KnipClient skips before probing/installing when analysisRoot is not a real
952
- // JS/Knip project. This avoids running knip from Unity/C#/generic repos.
953
- const startMs = Date.now();
954
- const knipResult = await knipClient.analyze(analysisRoot, getKnipIgnorePatterns(), { projectSeq: runtime.projectSeq });
955
- if (!runtime.isCurrentSession(sessionGeneration))
956
- return;
957
- if (knipResult.success) {
958
- cacheManager.writeCache("knip", knipResult, analysisRoot, {
959
- scanDurationMs: Date.now() - startMs,
960
- });
961
- }
962
- dbg(`session_start Knip scan done (${Date.now() - startMs}ms)`);
963
- }
964
- });
965
- // jscpd — duplicate code detection
966
- runHeavyweightTask("jscpd", async () => {
967
- if (await jscpdClient.ensureAvailable()) {
968
- if (!runtime.isCurrentSession(sessionGeneration))
969
- return;
970
- // Detect TS projects by tsconfig.json at the analysis root. When
971
- // set, JscpdClient.scan adds **/*.js and **/*.jsx to its ignore
972
- // pattern so compiled artifacts under dist/ aren't flagged as
973
- // duplicates of their TypeScript sources (closes #126's latent
974
- // dist/-as-duplicate bug). Cache scanner key varies by this flag
975
- // so a stale cache built with the wrong setting invalidates on
976
- // first read.
977
- const isTsProject = nodeFs.existsSync(path.join(analysisRoot, "tsconfig.json"));
978
- const scannerKey = isTsProject ? "jscpd-ts" : "jscpd";
979
- const cached = cacheManager.readCache(scannerKey, analysisRoot);
980
- if (cached) {
981
- if (!runtime.isCurrentSession(sessionGeneration))
982
- return;
983
- dbg(`session_start jscpd: cache hit (${scannerKey})`);
984
- }
985
- else {
986
- const startMs = Date.now();
987
- const jscpdResult = await jscpdClient.scan(analysisRoot, undefined, undefined, isTsProject);
988
- if (!runtime.isCurrentSession(sessionGeneration))
989
- return;
990
- if (jscpdResult.success) {
991
- cacheManager.writeCache(scannerKey, jscpdResult, analysisRoot, {
992
- scanDurationMs: Date.now() - startMs,
993
- });
994
- }
995
- dbg(`session_start jscpd scan done (${Date.now() - startMs}ms, isTsProject=${isTsProject})`);
996
- }
997
- }
998
- else {
999
- if (!runtime.isCurrentSession(sessionGeneration))
1000
- return;
1001
- dbg("session_start jscpd: not available");
1002
- }
1003
- });
1004
- // dead-code — cross-file dead-code for non-JS/TS languages (#127). Each
1005
- // client self-gates via detect() (a cheap fs marker probe), so only a
1006
- // matching-language project incurs the whole-tree scan cost. Knip remains
1007
- // the JS/TS path (above); these run alongside it for polyglot repos.
1008
- runHeavyweightTask("dead-code", async () => {
1009
- const applicable = deadCodeClients.filter((c) => c.detect(analysisRoot));
1010
- if (applicable.length === 0)
1011
- return;
1012
- await Promise.all(applicable.map(async (client) => {
1013
- if (!runtime.isCurrentSession(sessionGeneration))
1014
- return undefined;
1015
- const cacheKey = `dead-code-${client.id}`;
1016
- const cached = cacheManager.readCache(cacheKey, analysisRoot);
1017
- if (cached) {
1018
- dbg(`session_start dead-code(${client.id}): cache hit`);
1019
- return undefined;
1020
- }
1021
- const startMs = Date.now();
1022
- const result = await client.analyze(analysisRoot);
1023
- if (!runtime.isCurrentSession(sessionGeneration))
1024
- return undefined;
1025
- if (result.success) {
1026
- cacheManager.writeCache(cacheKey, result, analysisRoot, {
1027
- scanDurationMs: Date.now() - startMs,
1028
- });
1029
- }
1030
- logDeadCodeScan({
1031
- language: client.language,
1032
- success: result.success,
1033
- cached: false,
1034
- unusedExports: result.unusedExports.length,
1035
- unusedFiles: result.unusedFiles.length,
1036
- unusedDeps: result.unusedDeps.length,
1037
- unlistedDeps: result.unlistedDeps.length,
1038
- durationMs: result.durationMs ?? Date.now() - startMs,
1039
- ...(!result.success && { reason: result.summary }),
1040
- });
1041
- dbg(`session_start dead-code(${client.id}) done (${Date.now() - startMs}ms, ${deadCodeIssueCount(result)} issues)`);
1042
- }));
1043
- });
1044
- // govulncheck — Go module CVE detection (#132)
1045
- // Skipped silently when the project isn't a Go module or when
1046
- // `govulncheck` isn't installed (no auto-install in this slice).
1047
- runHeavyweightTask("govulncheck", async () => {
1048
- if (!GovulncheckClient.hasGoModule(analysisRoot)) {
1049
- dbg("session_start govulncheck: no go.mod — skipped");
1050
- return;
1051
- }
1052
- if (!(await govulncheckClient.ensureAvailable())) {
1053
- if (!runtime.isCurrentSession(sessionGeneration))
1054
- return;
1055
- dbg("session_start govulncheck: not installed (go install golang.org/x/vuln/cmd/govulncheck@latest)");
1056
- return;
1057
- }
1058
- if (!runtime.isCurrentSession(sessionGeneration))
1059
- return;
1060
- const cached = cacheManager.readCache("govulncheck", analysisRoot);
1061
- if (cached) {
1062
- if (!runtime.isCurrentSession(sessionGeneration))
1063
- return;
1064
- dbg(`session_start govulncheck: cache hit (${cached.data.findings.length} findings)`);
1065
- return;
1066
- }
1067
- const startMs = Date.now();
1068
- const result = await govulncheckClient.analyze(analysisRoot);
1069
- if (!runtime.isCurrentSession(sessionGeneration))
1070
- return;
1071
- if (result.success) {
1072
- cacheManager.writeCache("govulncheck", result, analysisRoot, {
1073
- scanDurationMs: Date.now() - startMs,
1074
- });
1075
- }
1076
- dbg(`session_start govulncheck: ${result.findings.length} reachable findings (${Date.now() - startMs}ms)`);
1077
- });
1078
- // gitleaks — committed-secrets detection (#130)
1079
- // Config-gated: opts in via .gitleaks.toml / .gitleaksignore / git
1080
- // hook / gitleaks dep. Cross-language by design.
1081
- runHeavyweightTask("gitleaks", async () => {
1082
- if (!GitleaksClient.hasGitleaksSignal(analysisRoot)) {
1083
- dbg("session_start gitleaks: no opt-in signal — skipped");
1084
- return;
1085
- }
1086
- if (!(await gitleaksClient.ensureAvailable())) {
1087
- if (!runtime.isCurrentSession(sessionGeneration))
1088
- return;
1089
- dbg("session_start gitleaks: not available (install failed?)");
1090
- return;
1091
- }
1092
- if (!runtime.isCurrentSession(sessionGeneration))
1093
- return;
1094
- const cached = cacheManager.readCache("gitleaks", analysisRoot);
1095
- if (cached) {
1096
- if (!runtime.isCurrentSession(sessionGeneration))
1097
- return;
1098
- dbg(`session_start gitleaks: cache hit (${cached.data.findings.length} findings)`);
1099
- return;
1100
- }
1101
- const startMs = Date.now();
1102
- const result = await gitleaksClient.scan(analysisRoot);
1103
- if (!runtime.isCurrentSession(sessionGeneration))
1104
- return;
1105
- if (result.success) {
1106
- cacheManager.writeCache("gitleaks", result, analysisRoot, {
1107
- scanDurationMs: Date.now() - startMs,
1108
- });
1109
- }
1110
- dbg(`session_start gitleaks: ${result.findings.length} findings (${Date.now() - startMs}ms)`);
1111
- });
1112
- // opengrep — full-workspace security/quality findings via a single CLI scan
1113
- // (#584). Structurally always-on (mirrors the LSP auxiliary's own
1114
- // enablement — see `OpengrepClient.resolveConfig`): the local rule file or
1115
- // `auto` registry ruleset always runs, `resolveOpengrepConfig` only picks
1116
- // which. Replaces the per-file LSP sweep as the source of opengrep
1117
- // findings for `lens_diagnostics mode=full` (`runWorkspaceDiagnostics` now
1118
- // excludes the opengrep server from its per-file "all"-scope touch —
1119
- // clients/lsp/index.ts `WORKSPACE_SWEEP_EXCLUDED_SERVER_IDS`); the per-edit
1120
- // real-time LSP path is untouched.
1121
- runHeavyweightTask("opengrep", async () => {
1122
- if (!(await opengrepClient.ensureAvailable())) {
1123
- if (!runtime.isCurrentSession(sessionGeneration))
1124
- return;
1125
- dbg("session_start opengrep: not available (install failed?)");
1126
- return;
1127
- }
1128
- if (!runtime.isCurrentSession(sessionGeneration))
1129
- return;
1130
- const cached = cacheManager.readCache("opengrep", analysisRoot);
1131
- if (cached) {
1132
- if (!runtime.isCurrentSession(sessionGeneration))
1133
- return;
1134
- dbg(`session_start opengrep: cache hit (${cached.data.findings.length} findings)`);
1135
- return;
1136
- }
1137
- const startMs = Date.now();
1138
- const result = await opengrepClient.scan(analysisRoot);
1139
- if (!runtime.isCurrentSession(sessionGeneration))
1140
- return;
1141
- if (result.success) {
1142
- cacheManager.writeCache("opengrep", result, analysisRoot, {
1143
- scanDurationMs: Date.now() - startMs,
1144
- });
1145
- }
1146
- dbg(`session_start opengrep: ${result.findings.length} findings (${Date.now() - startMs}ms)`);
1147
- });
1148
- // madge — whole-project circular-dependency detection. Session-start + cached
1149
- // (uniform with knip/jscpd/gitleaks) so lens_diagnostics mode=full reads it
1150
- // from the `madge` cache via the extractor registry — never a fresh scan.
1151
- runHeavyweightTask("madge", async () => {
1152
- if (!(await depChecker.ensureAvailable())) {
1153
- if (!runtime.isCurrentSession(sessionGeneration))
1154
- return;
1155
- dbg("session_start madge: not available");
1156
- return;
1157
- }
1158
- if (!runtime.isCurrentSession(sessionGeneration))
1159
- return;
1160
- const cached = cacheManager.readCache("madge", analysisRoot);
1161
- if (cached) {
1162
- dbg(`session_start madge: cache hit (${cached.data.circular.length} cycles)`);
1163
- return;
1164
- }
1165
- const startMs = Date.now();
1166
- const result = await depChecker.scanProject(analysisRoot);
1167
- if (!runtime.isCurrentSession(sessionGeneration))
1168
- return;
1169
- cacheManager.writeCache("madge", result, analysisRoot, {
1170
- scanDurationMs: Date.now() - startMs,
1171
- });
1172
- dbg(`session_start madge: ${result.circular.length} circular dependency chain(s) (${Date.now() - startMs}ms)`);
1173
- });
1174
- // trivy — dependency CVE detection (#131, Phase 1)
1175
- // Explicit opt-in: `trivy.enabled: true` in .pi-lens.json AND a dependency
1176
- // manifest present. The first run downloads Trivy's vuln DB (~30-200 MB);
1177
- // harmless here since this whole task runs in the background session_start
1178
- // wrapper.
1179
- runHeavyweightTask("trivy", async () => {
1180
- if (!TrivyClient.shouldScan(analysisRoot)) {
1181
- dbg("session_start trivy: not enabled / no dependency manifest — skipped");
1182
- return;
1183
- }
1184
- if (!(await trivyClient.ensureAvailable())) {
1185
- if (!runtime.isCurrentSession(sessionGeneration))
1186
- return;
1187
- dbg("session_start trivy: not available (install failed?)");
1188
- return;
1189
- }
1190
- if (!runtime.isCurrentSession(sessionGeneration))
1191
- return;
1192
- const cached = cacheManager.readCache("trivy", analysisRoot);
1193
- if (cached) {
1194
- if (!runtime.isCurrentSession(sessionGeneration))
1195
- return;
1196
- dbg(`session_start trivy: cache hit (${cached.data.findings.length} findings)`);
1197
- return;
1198
- }
1199
- const startMs = Date.now();
1200
- const result = await trivyClient.scan(analysisRoot);
1201
- if (!runtime.isCurrentSession(sessionGeneration))
1202
- return;
1203
- if (result.success) {
1204
- cacheManager.writeCache("trivy", result, analysisRoot, {
1205
- scanDurationMs: Date.now() - startMs,
1206
- });
1207
- }
1208
- dbg(`session_start trivy: ${result.findings.length} CVE findings (${Date.now() - startMs}ms)`);
1209
- });
1210
- // call-graph — build function-level call graph from review graph data
1211
- let callGraphIdentity;
1212
- const callGraphTask = runTask("call-graph", async () => {
1213
- const { FactStore } = await import("./dispatch/fact-store.js");
1214
- const { buildOrUpdateGraph, extractSymbolsAndRefsFromGraph, getReviewGraphCacheIdentity, } = await import("./review-graph/builder.js");
1215
- const { buildCallGraph, saveCallGraph, loadCallGraph } = await import("./call-graph.js");
1216
- if (!runtime.isCurrentSession(sessionGeneration))
1217
- return;
1218
- const startMs = Date.now();
1219
- // Build (or hydrate) the canonical review graph first. The call graph is a
1220
- // derived projection of that graph, so its freshness is the review graph's
1221
- // version/signature—not a second source walk and mtime policy.
1222
- // Subject labels this store's capacity-eviction telemetry distinctly
1223
- // from the other five production FactStore instances (#2243 review
1224
- // round 3, F1) — this session-start walk runs BEFORE any dispatch and
1225
- // can visit every file in the project, so a shared subject would let
1226
- // it consume the dispatch store's once-per-session ledger slot first.
1227
- const sessionFacts = new FactStore("runtime-session-call-graph");
1228
- const graph = await buildOrUpdateGraph(analysisRoot, [], sessionFacts);
1229
- if (!runtime.isCurrentSession(sessionGeneration))
1230
- return;
1231
- const identity = getReviewGraphCacheIdentity(analysisRoot, graph);
1232
- if (!identity) {
1233
- dbg("session_start call-graph: canonical review-graph identity unavailable");
1234
- return;
1235
- }
1236
- callGraphIdentity = {
1237
- reviewGraphVersion: identity.version,
1238
- reviewGraphSignature: identity.signature,
1239
- };
1240
- const cached = loadCallGraph(snapshotRoot, {
1241
- reviewGraphVersion: identity.version,
1242
- reviewGraphSignature: identity.signature,
1243
- });
1244
- if (cached) {
1245
- runtime.callGraph = cached.graph;
1246
- dbg(`session_start call-graph: loaded from cache (${cached.graph.edges.length} edges, ${cached.graph.callers.size} callee entries, ${Date.now() - startMs}ms)`);
1247
- return;
1248
- }
1249
- // Build from the canonical review graph (reuses already-parsed data, no
1250
- // duplicate parser or source walk).
1251
- const { allSymbols, allRefs, coverage } = extractSymbolsAndRefsFromGraph(graph);
1252
- const callGraph = buildCallGraph(allSymbols, allRefs, coverage);
1253
- runtime.callGraph = callGraph;
1254
- saveCallGraph(snapshotRoot, callGraph, {
1255
- reviewGraphVersion: identity.version,
1256
- reviewGraphSignature: identity.signature,
1257
- });
1258
- dbg(`session_start call-graph: built ${callGraph.edges.length} edges, ${callGraph.callers.size} callee entries (${Date.now() - startMs}ms)`);
1259
- });
1260
- // codebase-model — build mental model from call graph (internal-only until validated)
1261
- // Keep this deferred, but await the call-graph task's completion rather than
1262
- // racing its independently deferred timer. Otherwise a slow graph build leaves
1263
- // `runtime.callGraph` unset at this task's start and loses the model for the
1264
- // entire session (#1070).
1265
- void runTask("codebase-model", async () => {
1266
- await callGraphTask;
1267
- if (!runtime.isCurrentSession(sessionGeneration))
1268
- return;
1269
- if (!runtime.callGraph)
1270
- return;
1271
- const { buildCodebaseModel, saveCodebaseModel, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, } = await import("./codebase-model.js");
1272
- if (!callGraphIdentity)
1273
- return;
1274
- const model = buildCodebaseModel(runtime.callGraph, analysisRoot, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, callGraphIdentity);
1275
- saveCodebaseModel(snapshotRoot, model);
1276
- const top3 = model.entries
1277
- .slice(0, 3)
1278
- .map((e) => e.name)
1279
- .join(", ");
1280
- dbg(`session_start codebase-model: ${model.entries.length} entries, ` +
1281
- `${model.totalTokens} tokens, top symbols: ${top3 || "(none)"}`);
1282
- });
1283
- // ast-grep — export scan for duplicate detection
1284
- void runTask("ast-grep-exports", async () => {
1285
- if (await astGrepClient.ensureAvailable()) {
1286
- if (!runtime.isCurrentSession(sessionGeneration))
1287
- return;
1288
- const exports = await astGrepClient.scanExports(analysisRoot, "typescript");
1289
- if (!runtime.isCurrentSession(sessionGeneration))
1290
- return;
1291
- dbg(`session_start exports scan: ${exports.size} functions found`);
1292
- for (const [name, file] of exports) {
1293
- runtime.cachedExports.set(name, file);
1294
- }
1295
- saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
1296
- }
1297
- });
1298
- // word-index — identifier inverted index + BM25 for ranked symbol search
1299
- // (#162). Load -> rebuild-if-stale -> persist lifecycle (#348), shared with
1300
- // the quick-mode cold-start warmup pass below.
1301
- void runTask("word-index", async () => {
1302
- await buildOrRefreshWordIndex({
1303
- runtime,
1304
- sessionGeneration,
1305
- analysisRoot,
1306
- snapshotRoot,
1307
- dbg,
1308
- });
1309
- });
1310
- }
1311
- function scheduleDeferredToolProbes(deps, languageProfile, startupDefaults, startupScansWillRun, dbg) {
1312
- // #2467: same deferral as `scheduleStartupScans` — the probes were already
1313
- // fire-and-forget, so the analyzers they probe load here rather than before
1314
- // the handler.
1315
- void (async () => {
1316
- const resolved = await demandBootstrapDeps(deps, "session-start-tool-probes");
1317
- if (!resolved) {
1318
- dbg("session_start tools: analyzer bootstrap unavailable — no probes");
1319
- return;
1320
- }
1321
- scheduleDeferredToolProbesWithClients(resolved, languageProfile, startupDefaults, startupScansWillRun, dbg);
1322
- })();
1323
- }
1324
- /** The probe bodies, once the analyzers they probe are resolved. */
1325
- function scheduleDeferredToolProbesWithClients(deps, languageProfile, startupDefaults, startupScansWillRun, dbg) {
1326
- const { biomeClient, ruffClient, depChecker } = deps;
1327
- const defaultTools = new Set(startupDefaults);
1328
- const probes = [];
1329
- // Do not probe tools already covered by startup preinstall or startup scans.
1330
- // This keeps session_start logs from showing duplicate "ensure X: start" lines
1331
- // while preserving lazy checks for tools that are actually relevant.
1332
- if (languageProfile.present.jsts && !defaultTools.has("biome")) {
1333
- probes.push(["biome", () => biomeClient.ensureAvailable()]);
1334
- }
1335
- if (languageProfile.present.python && !defaultTools.has("ruff")) {
1336
- probes.push(["ruff", () => ruffClient.ensureAvailable()]);
1337
- }
1338
- if (startupScansWillRun) {
1339
- probes.push(["madge", () => depChecker.ensureAvailable()]);
1340
- }
1341
- if (probes.length === 0) {
1342
- dbg("session_start tools: no deferred availability probes needed");
1343
- return;
1344
- }
1345
- void (async () => {
1346
- const warmStart = Date.now();
1347
- const results = await Promise.all(probes.map(async ([name, run]) => {
1348
- try {
1349
- return [name, await run()];
1350
- }
1351
- catch (err) {
1352
- dbg(`session_start: ${name} availability check failed: ${err}`);
1353
- return [name, false];
1354
- }
1355
- }));
1356
- const summary = results
1357
- .map(([name, ready]) => `${name}=${ready}`)
1358
- .join(" ");
1359
- dbg(`session_start tools (deferred probes complete, ${Date.now() - warmStart}ms): ${summary}`);
1360
- })();
1361
- }
1362
- /**
1363
- * Session-start orientation prepended as a context message (gated by the
1364
- * context-injection toggle). Deliberately lean: it names the high-value tools
1365
- * and the distinction between cached reporting and active verification
1366
- * — per-tool argument detail lives in each tool's own registered description, so
1367
- * re-documenting it here would just pay the tokens twice every session.
1368
- */
1369
- export const SESSION_START_GUIDANCE = [
1370
- "📌 pi-lens active — automated checks run on every edit/write; blocking errors (including pre-existing) show inline and must be fixed.\n" +
1371
- "Key tools (see each tool's own description for args):\n" +
1372
- "• lens_diagnostics — source=session reads cache; empty cache ≠ clean; use source=lsp scope=paths for changed files with absent or stale findings (aggregate hosts: lens(action=diagnostics)).\n" +
1373
- "• symbol_search → module_report → read_symbol/read_enclosing — ranked identifier search, then navigable outline/callback handles + exact body reads; cheaper than reading a whole file before editing.\n" +
1374
- "• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace. Use ast_grep_search with dump=true to inspect AST nodes.",
1375
- ];
1376
- export async function handleSessionStart(deps) {
1377
- resetDegradationLedger();
1378
- // #2467: re-arm the analyzer bootstrap's shutdown gate. The gate is a
1379
- // per-SESSION claim ("this session is over") held in process-lived storage,
1380
- // so without this a replacement session in the same process would find
1381
- // every analyzer refused for the rest of the process — AGENTS.md defect
1382
- // shape 17. The resident clients themselves are deliberately kept.
1383
- resetAnalyzerBootstrapSessionState();
1384
- resetProjectDataDirSessionState();
1385
- resetTestRunnerDelivery();
1386
- // #2450 fix round 3, catalog shape 17: the "bridge unavailable" dbg latch
1387
- // (`clients/lsp-mutation.ts`) is a process-lifetime once-per-session flag,
1388
- // same shape as the ledger's own once-per-subject bookkeeping above, but
1389
- // kept as its own reset (not folded into `resetDegradationLedger`) to
1390
- // avoid that module reaching back into this one — see the latch's own
1391
- // comment for why.
1392
- resetLspMutationNoBridgeDbgLatch();
1393
- // #1743: the bounded-telemetry per-turn counters. The rising-edge state is
1394
- // NOT here — it is the ledger's own tally, reset on the line above. These
1395
- // counters are keyed by turn index, and a new session restarts turn
1396
- // numbering at 0, so without this a stale count could survive a session
1397
- // boundary that happened to land on the same index.
1398
- resetBoundedTelemetry();
1399
- // #1956 R3: stale message_end events drain after replacement. Rotate the
1400
- // live anchor into one bounded previous slot so the queued event keeps the
1401
- // replaced session's attribution until a newer live message_end is seen.
1402
- rotateMessageEndAttribution();
1403
- // #2526: the once-per-session `config_resolved` claim is NOT re-armed here.
1404
- // index.ts resolves this session's config in `ensureLSPConfigInitialized`
1405
- // BEFORE it calls this handler, so a re-arm on this line would fire between
1406
- // the session's own two resolutions and the deferred `loadLSPConfig` below
1407
- // would write a second row for one session (review round 2, F1). The re-arm
1408
- // lives in index.ts's `session_start` closure, ahead of that ensure and
1409
- // behind the #473 gate — see `resetOncePerSessionPhases`'s doc comment.
1410
- const handlerEnteredAt = Date.now();
1411
- const sessionStartMs = deps.sessionStartFiredAt ?? handlerEnteredAt;
1412
- const cwdForTelemetry = deps.ctxCwd ?? process.cwd();
1413
- if (deps.sessionStartFiredAt !== undefined) {
1414
- logLatency({
1415
- type: "phase",
1416
- filePath: cwdForTelemetry,
1417
- phase: "session_start_prehandler",
1418
- startedAt: new Date(deps.sessionStartFiredAt).toISOString(),
1419
- durationMs: (deps.handlerEnteredAt ?? handlerEnteredAt) - deps.sessionStartFiredAt,
1420
- metadata: { reason: deps.sessionReason },
1421
- });
1422
- }
1423
- // Cold-start input-latency mitigation. The first `session_start` of
1424
- // the process — i.e. the one that fires immediately after the user
1425
- // launches `pi` — must return as fast as possible so the TUI input
1426
- // box becomes responsive. The full startup mode runs several
1427
- // expensive synchronous walks (resolveStartupScanContext,
1428
- // detectProjectLanguageProfile, scanProjectRules, scheduleStartupScans)
1429
- // that together can block the event loop for 3-6s on a 2k-file
1430
- // project, during which keystrokes are dropped or batched.
1431
- //
1432
- // Strategy:
1433
- // - Force the very first invocation to "quick" mode, which exits
1434
- // after a minimal runtime reset and snapshot hydration.
1435
- // - 2 seconds later, schedule a background warmup that walks the
1436
- // project asynchronously (yielding every 100 entries) and
1437
- // populates the in-process memo caches
1438
- // (startupScanContextCache + languageProfileCache).
1439
- // - The user's first /new (or any subsequent session_start) sees
1440
- // a cache hit on both walks and finishes the full path in <50ms.
1441
- //
1442
- // Opt-out: PI_LENS_COLD_START_QUICK=0 disables this behaviour.
1443
- // Override: PI_LENS_STARTUP_MODE explicitly set wins (we honour it).
1444
- // deps.startupModeOverride lets a host (e.g. the MCP server, which has
1445
- // no TUI keystroke latency to protect) skip the quick-mode heuristic
1446
- // entirely — but only when PI_LENS_STARTUP_MODE is unset in the env
1447
- // (an explicit env var still takes highest precedence).
1448
- // Tunable: PI_LENS_WARMUP_DELAY_MS adjusts the warmup delay.
1449
- let startupMode = resolveStartupMode(deps.projectConfig, deps.globalConfig);
1450
- // SAFETY: these two flags are process-lifetime state pi-lens stashes on
1451
- // `globalThis` so a second extension instance in the same process sees the
1452
- // first one's warmup. There is no ambient declaration for them, and adding
1453
- // one would let any module write them. Both are declared OPTIONAL here, so
1454
- // the reads below are `undefined`-safe on a process where nothing set them.
1455
- const processGlobals = globalThis;
1456
- const isFirstSessionOfProcess = !processGlobals.__piLensFirstSessionDone;
1457
- if (isFirstSessionOfProcess &&
1458
- process.env.PI_LENS_COLD_START_QUICK !== "0" &&
1459
- !process.env.PI_LENS_STARTUP_MODE &&
1460
- !deps.projectConfig?.startup?.mode &&
1461
- !deps.globalConfig?.startup?.mode) {
1462
- // Apply host-provided override (e.g. MCP server forces "full") before
1463
- // falling back to the TUI quick-mode heuristic.
1464
- startupMode = deps.startupModeOverride ?? "quick";
1465
- }
1466
- processGlobals.__piLensFirstSessionDone = true;
1467
- // #1785 F5 (round 4, design (a)): the warmup timer body below ALREADY
1468
- // loads the on-disk snapshot for its own purposes (`cachedSnapshot`,
1469
- // reused for the `startupScan` verdict cache) strictly BEFORE any save it
1470
- // might go on to make. This LOCAL variable — per-`handleSessionStart`-call,
1471
- // deliberately NOT module-scope (see #1785 F6) — lets the warmup publish
1472
- // that already-loaded read (narrowed to exports+rules, never the full
1473
- // object with `wordIndex`) for `retroactivelyHydrateAfterDeferredSequence`
1474
- // to reuse at zero additional cost, instead of this call paying for its
1475
- // own separate synchronous capture (rounds 1-3's approach, reverted here:
1476
- // see that function's doc comment for the full history). `undefined`
1477
- // means the warmup hasn't reached its own read yet (or was never armed by
1478
- // this call at all) — the quick-mode block's retroactive-hydration
1479
- // callback then falls back to a live re-load instead.
1480
- let warmupOwnSnapshotRead;
1481
- // #1154: quick mode is entered on BOTH `pi -p`/`--print` (a one-shot that
1482
- // exits right after this turn) and an interactive process's first
1483
- // session_start (forced quick to protect keystroke latency, then warms
1484
- // caches for the NEXT /new). The warmup only benefits a *future* session in
1485
- // the same process — a one-shot print invocation has none. Worse, in a
1486
- // one-shot the warmup is a referenced-handle keep-alive: the +2s timer and
1487
- // the LSP-prewarm children / language-profile source walk it launches
1488
- // outlive settle with no session_shutdown teardown, holding the process
1489
- // open (the #1097/#1110/#1122-hyp-A class). So skip warmup entirely in
1490
- // print mode; interactive first-sessions (not print) still warm.
1491
- if (startupMode === "quick" &&
1492
- !isPrintMode() &&
1493
- process.env.PI_LENS_COLD_START_QUICK !== "0" &&
1494
- !processGlobals.__piLensWarmupScheduled) {
1495
- processGlobals.__piLensWarmupScheduled = true;
1496
- const warmupDelayMs = Number(process.env.PI_LENS_WARMUP_DELAY_MS ?? 2000);
1497
- const warmupCwd = deps.ctxCwd ?? process.cwd();
1498
- const warmupDbg = deps.dbg;
1499
- // #1154: `.unref()` the warmup timer so — even for a warmup that IS
1500
- // scheduled (an interactive first-session) — the pending timer alone can
1501
- // never hold the loop open. Interactive processes stay alive for other
1502
- // reasons, so the warmup still fires; a one-shot never reaches here (the
1503
- // isPrintMode gate above), and repo convention is that every background
1504
- // timer is unref'd (this file previously had none).
1505
- const warmupTimer = setTimeout(() => {
1506
- const warmupStartedAt = Date.now();
1507
- void (async () => {
1508
- try {
1509
- warmupDbg("warmup: starting background warmup");
1510
- const scanContextStartedAt = Date.now();
1511
- // Dynamic imports keep the warmup pipeline off the hot
1512
- // startup path — these modules don't load until the timer
1513
- // fires, well after the TUI is interactive.
1514
- const startupScanModule = await import("./startup-scan.js");
1515
- const languageProfileModule = await import("./language-profile.js");
1516
- const warmupSnapshotRoot = resolveSnapshotRoot(warmupCwd);
1517
- // #699: this background timer is the ONLY place a quick-mode
1518
- // session (i.e. every `-p`/`--print` invocation, which forces
1519
- // quick mode and returns before line ~1253 without ever touching
1520
- // scan-context) computes scan-context — and that process exits
1521
- // right after, discarding the result. Before walking, check for a
1522
- // still-fresh persisted verdict (mirrors the interactive path's
1523
- // snapshot reuse); after walking, persist unconditionally
1524
- // (canWarmCaches true OR false) so the NEXT one-shot process can
1525
- // reuse it instead of re-walking a possibly huge tree from
1526
- // scratch on every single startup.
1527
- const cachedSnapshot = loadProjectSnapshot(warmupSnapshotRoot);
1528
- // #1785 F5 (round 4): publish this ALREADY-loaded read — narrowed
1529
- // to exports+rules, never a reference to `cachedSnapshot` itself
1530
- // (which carries `wordIndex`/`files`/`symbols`/`reverseDeps`) — for
1531
- // `retroactivelyHydrateAfterDeferredSequence` to reuse. This read
1532
- // strictly precedes the warmup's own possible save a few lines
1533
- // below, so it is guaranteed to reflect disk as it stood before
1534
- // THIS warmup could have touched it. Zero marginal cost: this read
1535
- // already happens unconditionally for the `startupScan` verdict
1536
- // cache below, with or without this fix.
1537
- warmupOwnSnapshotRead = cachedSnapshot
1538
- ? {
1539
- version: cachedSnapshot.version,
1540
- seq: cachedSnapshot.seq,
1541
- cachedExports: cachedSnapshot.cachedExports,
1542
- projectRulesScan: cachedSnapshot.projectRulesScan,
1543
- }
1544
- : null;
1545
- const cachedVerdict = cachedSnapshot?.startupScan;
1546
- let scan;
1547
- if (cachedVerdict &&
1548
- startupScanModule.isStartupScanVerdictFresh(cachedVerdict)) {
1549
- scan = { ...cachedVerdict, cwd: path.resolve(warmupCwd) };
1550
- warmupDbg(`warmup: scan-context reused from cache (canWarm=${scan.canWarmCaches}${scan.reason ? `, reason=${scan.reason}` : ""})`);
1551
- }
1552
- else {
1553
- scan =
1554
- await startupScanModule.resolveStartupScanContextAsync(warmupCwd);
1555
- logLatency({
1556
- type: "phase",
1557
- phase: "session_start_scan_context_compute",
1558
- filePath: warmupCwd,
1559
- startedAt: new Date(scanContextStartedAt).toISOString(),
1560
- durationMs: Date.now() - scanContextStartedAt,
1561
- metadata: { mode: "quick-background" },
1562
- });
1563
- warmupDbg(`warmup: scan-context done in ${Date.now() - warmupStartedAt}ms (canWarm=${scan.canWarmCaches})`);
1564
- // Best-effort: a save failure must never affect warmup itself —
1565
- // saveRuntimeProjectSnapshot already swallows its own errors.
1566
- saveRuntimeProjectSnapshot({
1567
- cwd: warmupSnapshotRoot,
1568
- runtime: deps.runtime,
1569
- startupScan: scan,
1570
- dbg: warmupDbg,
1571
- });
1572
- }
1573
- logLatency({
1574
- type: "phase",
1575
- phase: "warmup_scan_context",
1576
- filePath: warmupCwd,
1577
- startedAt: new Date(scanContextStartedAt).toISOString(),
1578
- durationMs: Date.now() - scanContextStartedAt,
1579
- });
1580
- // Respect the startup-scan guard (#250): canWarmCaches is false for
1581
- // home-dir / no-project-root / too-many-source-files. Proceeding into
1582
- // the language-profile source walk in those cases lets it root at an
1583
- // ancestor (e.g. a marker in $HOME when pi runs in ~/tmp) and traverse
1584
- // the entire home tree — multi-hour scans. Nothing to warm anyway when
1585
- // the guard says caches can't be warmed.
1586
- if (!scan.canWarmCaches) {
1587
- warmupDbg(`warmup: skipping language-profile (canWarm=false, reason=${scan.reason ?? "unknown"})`);
1588
- return;
1589
- }
1590
- const languageRoot = scan.projectRoot ?? warmupCwd;
1591
- const languageProfileStartedAt = Date.now();
1592
- await languageProfileModule.detectProjectLanguageProfileAsync(languageRoot);
1593
- warmupDbg(`warmup: language-profile done in ${Date.now() - languageProfileStartedAt}ms`);
1594
- logLatency({
1595
- type: "phase",
1596
- phase: "warmup_language_profile",
1597
- filePath: warmupCwd,
1598
- startedAt: new Date(languageProfileStartedAt).toISOString(),
1599
- durationMs: Date.now() - languageProfileStartedAt,
1600
- });
1601
- // #348: fold the word-index build/refresh into this existing
1602
- // cold-start warmup pass so quick-mode (and any session whose very
1603
- // first session_start is forced quick) still ends up with a
1604
- // queryable index — full-mode sessions get it via the "word-index"
1605
- // runTask above; this is the quick-mode equivalent, once per
1606
- // process, off the hot path.
1607
- const wordIndexStartedAt = Date.now();
1608
- const wordIndexResult = await buildOrRefreshWordIndex({
1609
- runtime: deps.runtime,
1610
- sessionGeneration: deps.runtime.sessionGeneration,
1611
- analysisRoot: languageRoot,
1612
- snapshotRoot: warmupSnapshotRoot,
1613
- dbg: warmupDbg,
1614
- });
1615
- warmupDbg(`warmup: word-index done in ${Date.now() - wordIndexStartedAt}ms`);
1616
- logLatency({
1617
- type: "phase",
1618
- phase: "warmup_word_index",
1619
- filePath: warmupCwd,
1620
- startedAt: new Date(wordIndexStartedAt).toISOString(),
1621
- durationMs: Date.now() - wordIndexStartedAt,
1622
- metadata: wordIndexResult ? { ...wordIndexResult } : undefined,
1623
- });
1624
- // #947: fold the dominant-language LSP pre-warm into this
1625
- // warmup pass. The first-session-of-process heuristic forces
1626
- // quick mode, and the pre-warm below is gated on
1627
- // allowBootstrapTasks (full mode only) — so it NEVER ran in
1628
- // practice and every session's first edit paid a cold LSP
1629
- // spawn (~750ms wait + ~2.5s spawn, measured). Fire it here
1630
- // instead: off the interactive path, once per process (the
1631
- // __piLensWarmupScheduled guard above), generation-guarded
1632
- // inside igniteDominantLanguageWarm, and honoring the same
1633
- // skips as the full-mode path — subagent light mode (#449),
1634
- // warm-attach (#822), the no-lsp flag, and the
1635
- // canWarmCaches guard (the early return above). Concurrent
1636
- // secondaries never reach handleSessionStart at all (the
1637
- // #473 guard in index.ts), so they never schedule this
1638
- // warmup in the first place.
1639
- const lspPrewarmStartedAt = Date.now();
1640
- if (deps.getFlag("no-lsp")) {
1641
- warmupDbg("warmup: skipping LSP pre-warm (no-lsp)");
1642
- }
1643
- else if (isSubagentSession()) {
1644
- warmupDbg("warmup: skipping LSP pre-warm (subagent session)");
1645
- }
1646
- else if (isWarmAttached()) {
1647
- warmupDbg("warmup: skipping LSP pre-warm (attached to incumbent)");
1648
- }
1649
- else {
1650
- // #957 review: honor explicit warmFiles (#203) like the full
1651
- // path does — configured projects warm exactly what they
1652
- // asked for; dominant-language warm is the fallback.
1653
- const lspConfig = await loadLSPConfig(warmupCwd).catch(() => ({
1654
- warmFiles: [],
1655
- }));
1656
- const warmFiles = lspConfig.warmFiles ?? [];
1657
- if (warmFiles.length > 0) {
1658
- await igniteWarmFiles(warmupCwd, warmFiles, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
1659
- }
1660
- else {
1661
- await igniteDominantLanguageWarm(languageRoot, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
1662
- }
1663
- logLatency({
1664
- type: "phase",
1665
- phase: "warmup_lsp_prewarm",
1666
- filePath: warmupCwd,
1667
- startedAt: new Date(lspPrewarmStartedAt).toISOString(),
1668
- durationMs: Date.now() - lspPrewarmStartedAt,
1669
- });
1670
- warmupDbg(`warmup: lsp-prewarm done in ${Date.now() - lspPrewarmStartedAt}ms`);
1671
- }
1672
- warmupDbg(`warmup: total ${Date.now() - warmupStartedAt}ms`);
1673
- }
1674
- catch (err) {
1675
- warmupDbg(`warmup: error ${err}`);
1676
- // Allow a future session to retry the warmup.
1677
- processGlobals.__piLensWarmupScheduled = false;
1678
- }
1679
- finally {
1680
- logLatency({
1681
- type: "phase",
1682
- phase: "warmup_total",
1683
- filePath: warmupCwd,
1684
- startedAt: new Date(warmupStartedAt).toISOString(),
1685
- durationMs: Date.now() - warmupStartedAt,
1686
- });
1687
- }
1688
- })();
1689
- }, warmupDelayMs);
1690
- warmupTimer.unref?.();
1691
- }
1692
- const allowBootstrapTasks = startupMode === "full";
1693
- const quickMode = startupMode === "quick";
1694
- const { ctxCwd, getFlag, notify, dbg, log, runtime, cacheManager, ensureTool, cleanStaleTsBuildInfo, resetDispatchBaselines, resetLSPService, } = deps;
1695
- // Lightweight phase timer — resets after each call so each log line shows
1696
- // the cost of that phase alone, not cumulative time from session start.
1697
- let _phaseT = Date.now();
1698
- const phase = (name) => {
1699
- dbg(`session_start phase ${name}: ${Date.now() - _phaseT}ms`);
1700
- _phaseT = Date.now();
1701
- };
1702
- // #2467: `peek`, never a load. A metrics client that was never constructed
1703
- // holds no per-session state to re-arm, so the reset is vacuous — and
1704
- // loading the analyzer graph in order to reset nothing is the interactive-
1705
- // path cost this issue removes.
1706
- deps.bootstrap.peek()?.metricsClient.reset();
1707
- getDiagnosticTracker().reset();
1708
- clearFileTimeSessions();
1709
- runtime.complexityBaselines.clear();
1710
- resetDispatchBaselines(ctxCwd);
1711
- // #806: clear the shared per-directory marker index and registered
1712
- // topology-derived caches only at session start. Mid-session edits are NOT
1713
- // detected; #805's tsconfig matcher cache follows the same registry reset.
1714
- resetWorkspaceTopology();
1715
- // #2000: opaque-recovery baselines are keyed cwd:generation (unreachable
1716
- // after reset) and the git-worktree memo must re-probe after a session
1717
- // that may have seen a non-git dir become one.
1718
- resetOpaqueMutationState();
1719
- // #2430: the observational net's pending baselines are keyed by tool-call
1720
- // id and its content ledger describes the previous session's files; both
1721
- // mis-answer forever without this. The learned attribution map is cleared
1722
- // and then RE-PRIMED from this project's persisted file, so a tool an
1723
- // earlier session observed is classified by name with no snapshot at all.
1724
- resetObservedMutationNet();
1725
- resetMutationAttribution();
1726
- primePersistedMutationAttribution(ctxCwd);
1727
- // #2026: pending auxiliary baselines are unreachable after generation bump.
1728
- resetPendingAuxiliaryCoverage();
1729
- // #817/#1199: Windows command resolution is cached per (command, canonical
1730
- // effective child PATH/PATHEXT/cwd/per-drive provenance); drop it each
1731
- // session so environment changes (e.g.
1732
- // a tool installed mid-session or a differently-scoped managed bin) are
1733
- // picked up fresh.
1734
- resetSafeSpawnWindowsCommandCache();
1735
- // #1266: install-failure suppression in dispatch runner availability
1736
- // checkers (`noteInstallFailure`) is meant to last only until "the next
1737
- // session", per #1222 — but nothing called the reset helper, so a
1738
- // transient install failure (npm registry blip, locked file) suppressed
1739
- // the tool for the rest of the process lifetime. Clear it here, same
1740
- // boundary as the other per-session caches on this line.
1741
- resetDispatchAvailabilityState();
1742
- // Runner collect-later observations and their late findings are session
1743
- // scoped. A new session must re-probe rather than inherit a process latch.
1744
- resetObservedRunnerLatency();
1745
- resetPendingRunnerFindings();
1746
- // #1995: a command that TIMED OUT (not merely failed a probe) cools down
1747
- // for the rest of the session so a hot loop of edits cannot hand the same
1748
- // wedged .cmd shim a second budget. A new session may retry: the executable
1749
- // or its environment may have changed.
1750
- resetSpawnTimeoutCooldowns();
1751
- // #1895: formatter PATH verdicts are session-scoped, but they live in
1752
- // formatters.ts and are not covered by the dispatch generation. Re-arm them
1753
- // here so a formatter installed or removed between sessions is observed by
1754
- // the next session. `clearFormatterCache` drops the per-cwd selection cache
1755
- // as well as the which latches, which is what a real re-probe needs: the
1756
- // selection cache answers a same-cwd lookup before any probe runs, so
1757
- // clearing the latches alone would leave the stale verdict standing in the
1758
- // one directory the user is working in.
1759
- clearFormatterCache();
1760
- // #1535: same #1266 pattern, one caller earlier — the `gh auth token` latch
1761
- // zizmor's spawn reads is process-lived storage whose durability contract is
1762
- // per SESSION, not per process. Without this, a user who runs `gh auth
1763
- // login` and starts a fresh session still reads the previous session's
1764
- // stale "no token" verdict until the cooldown (if any) happens to expire.
1765
- resetZizmorTokenAvailability();
1766
- // #2455: same #1496/#1535 process-lifetime-latch shape, one caller later —
1767
- // the `goClient`/`rustClient` singletons each hold their own
1768
- // `createAvailabilityLatch()` outside the dispatch generation counter above
1769
- // (they predate `createCwdCachedProbe`). A "missing" verdict never expires
1770
- // on its own, so without these a go/cargo install between sessions stayed
1771
- // unobserved for the rest of the process's life. Found by hand while
1772
- // auditing #2455's detector widening, NOT surfaced by it: the sweep skips
1773
- // any file exporting no reset at all, and neither client module exported
1774
- // one, so no container predicate could have yielded them (#2455 fix round 4,
1775
- // F5 — an earlier draft of this comment had the causality backwards).
1776
- resetGoAvailability();
1777
- resetRustAvailability();
1778
- // psscriptanalyzer's three latches (interpreter, module, -File exec) are
1779
- // module-local, so the generation counter above does not reach them
1780
- // (#1490, #1540).
1781
- resetPsScriptAnalyzerAvailability();
1782
- // #1497: the install-class retry ceiling is terminal for a SESSION, but the
1783
- // latches holding it live on process-lived client instances (bootstrap builds
1784
- // them once). Same #1266/#1490/#1535 shape — without this line "terminal for
1785
- // the session" is terminal for the process, and a repaired network never
1786
- // re-earns its `go install`.
1787
- resetInstallRetryLatches();
1788
- // #1537: the lazy-install seam's hold (`gem install rubocop`, `rustup
1789
- // component add`) is durable for a SESSION. Its map is module-local, so the
1790
- // generation counter above does not reach it — same #1490/#1497 shape. It
1791
- // belongs on THIS line and not on the turn_end path: the first cut cleared it
1792
- // from `clearFormatterRuntimeState()`, which runs every turn, so a failing
1793
- // install re-spawned every turn instead of once per session.
1794
- resetLazyInstallAttempts();
1795
- // #1897: direct-LSP negative availability and bare installer paths are
1796
- // session facts. A command or PATH entry can appear between sessions.
1797
- resetDirectLspCommandAvailability();
1798
- // #2052: a root may not exist when the case-sensitivity probe first sees it.
1799
- // Re-probe it at the next session boundary after the workspace is created.
1800
- resetLSPCaseSensitivityState();
1801
- resetResolvedPathCache();
1802
- // #1653: pnpm/yarn/bun/npm's availability latches (package-manager.ts) are
1803
- // module-local, same #1490/#1535 shape as the two lines above — the
1804
- // generation counter above does not reach them. Without this line, a
1805
- // pnpm/yarn/bun install done mid-day stayed invisible: a genuine "missing"
1806
- // verdict from one session latched into the next until a process restart.
1807
- _resetPackageManagerCache();
1808
- // #1730: the managed-tool refresh budget is a per-SESSION allowance (at most
1809
- // one `npm update` per session). Left process-lived, one long-running pi
1810
- // would refresh a single tool at launch and never look at the other 21
1811
- // again — the same latch shape as the two lines above. The weekly cadence
1812
- // itself is NOT reset here: it lives in the persisted per-tool stamp, so
1813
- // re-arming the budget only restores the session's right to ask.
1814
- resetManagedToolRefreshSession();
1815
- // #1123 item 3: a fresh session can re-report smells that a prior session
1816
- // already surfaced once (see `checkSmellsAndNoteOnce`'s once-per-session gate).
1817
- resetSmellsSessionState();
1818
- // #2800 item 8: the situational-tool dead-weight observation (which tools
1819
- // this session never activated or called) is session-scoped, so its sets
1820
- // and once-latch re-arm here beside the other registered resets. Both hosts
1821
- // open the session's row through startSituationalToolTelemetrySession()
1822
- // BEFORE this handler runs, and a repeated MCP session_start refresh
1823
- // legitimately re-runs this handler — so the reset only acts when no
1824
- // telemetry session is open; clearing a live session here would wipe the
1825
- // calls recorded before the refresh.
1826
- resetSituationalToolTelemetry();
1827
- // #1782: re-arm the workspace-diagnostics cache session clock. Entries
1828
- // written before this instant assert findings from a session that is over,
1829
- // so they must revalidate before they can be served as current again.
1830
- resetWorkspaceDiagnosticsCacheSession(sessionStartMs);
1831
- // Some embedders inject a capability-shaped Knip client rather than the
1832
- // concrete KnipClient. Session reset is an optional lifecycle capability;
1833
- // its absence must not make session_start fail.
1834
- // #2467: `peek` for the same reason as `metricsClient.reset()` above —
1835
- // an unloaded knip client has no session state to clear.
1836
- deps.bootstrap.peek()?.knipClient.resetSessionState?.();
1837
- // #1910: the tier-3 cascade outstanding-touch registry and its
1838
- // sweep-scoped expired/evicted counters (clients/lsp/cascade-tier.ts) are
1839
- // a per-SESSION claim about touches THIS session fired. #1899 bounded the
1840
- // registry between sweeps but, by its own review, left the session
1841
- // boundary unwired — a session replacement inherited the prior session's
1842
- // outstanding touches and misattributed a stray expiry/eviction to the
1843
- // next session's first reconcile gauge.
1844
- resetCascadeTierSessionState();
1845
- runtime.resetForSession(sessionStartMs);
1846
- logLatency({
1847
- type: "phase",
1848
- phase: "session_start_runtime_reset",
1849
- filePath: ctxCwd ?? process.cwd(),
1850
- startedAt: new Date(handlerEnteredAt).toISOString(),
1851
- durationMs: Date.now() - handlerEnteredAt,
1852
- metadata: { mode: startupMode, reason: deps.sessionReason },
1853
- });
1854
- // #1019: log cleanup is deferred OFF the interactive critical path. It does
1855
- // synchronous fs sweeps (~7ms) whose only consumer is an async notification —
1856
- // nothing on the hot path reads its result — so running it inline just taxed
1857
- // every session start. It still runs every session (correctness unchanged),
1858
- // now on the next tick, and notifies when done. Errors are swallowed to a
1859
- // dbg line: a best-effort log sweep must never surface as a session failure.
1860
- const cleanupCwd = ctxCwd ?? process.cwd();
1861
- setImmediate(() => {
1862
- const logCleanupStartedAt = Date.now();
1863
- try {
1864
- const logCleanup = runLogCleanup(dbg);
1865
- logLatency({
1866
- type: "phase",
1867
- phase: "session_start_log_cleanup",
1868
- filePath: cleanupCwd,
1869
- startedAt: new Date(logCleanupStartedAt).toISOString(),
1870
- durationMs: Date.now() - logCleanupStartedAt,
1871
- metadata: {
1872
- mode: startupMode,
1873
- reason: deps.sessionReason,
1874
- deferred: true,
1875
- },
1876
- });
1877
- if (logCleanup.cleaned > 0 || logCleanup.rotated > 0) {
1878
- notify(`🧹 ${logCleanup.report}`, "info");
1879
- }
1880
- }
1881
- catch (err) {
1882
- dbg(`session_start: deferred log cleanup failed: ${err}`);
1883
- }
1884
- });
1885
- dbg(`session_start startup mode: ${startupMode}`);
1886
- if (!getFlag("no-lsp")) {
1887
- resetLSPService({ fast: true, reason: "session_start" });
1888
- dbg("session_start: LSP service reset");
1889
- dbg("session_start: phase0 workspace diagnostics observation enabled (capability probe only)");
1890
- }
1891
- const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
1892
- const cwd = ctxCwd ?? process.cwd();
1893
- // #2526 review round 3, S1: this handler no longer predicts whether config
1894
- // will resolve. `loadLSPConfig` itself publishes a `config_resolution_pending`
1895
- // mark, from inside the function, at the instant a resolution is actually
1896
- // attempted — see that function's doc comment (`clients/lsp/config.ts`) for
1897
- // why a prediction mirrored against this handler's own gates drifted from
1898
- // what index.ts and this handler's callers actually schedule.
1899
- // #1228: generic atomic-write stages are shared by several project stores,
1900
- // so the review-graph-specific sweep cannot own this namespace. Sweep the
1901
- // project data roots and machine-global registry root once per session start;
1902
- // this is fire-and-forget and bounded so it never delays startup.
1903
- const projectDataDir = getProjectDataDir(cwd);
1904
- // #2874: `getProjectDataDir` queues one migration per old-slug directory
1905
- // it renames (or finds coexisting with its hashed successor). Drain here
1906
- // so each migration emits one bounded record per session at most.
1907
- for (const migration of drainProjectDataDirMigrations()) {
1908
- const targetName = path.basename(migration.to);
1909
- const hash = targetName.match(/([0-9a-f]{8})$/)?.[1] ?? "unknown";
1910
- recordDegradationOnce({
1911
- kind: "data_dir_migrated",
1912
- subject: hash,
1913
- reason: migration.outcome === "renamed"
1914
- ? "using hashed directory after renaming legacy directory"
1915
- : migration.outcome === "coexisting"
1916
- ? "using hashed directory because legacy and hashed directories both exist"
1917
- : migration.outcome === "rename-failed"
1918
- ? "using legacy directory after hashed-directory rename failure"
1919
- : "using resolved directory after realpath fallback",
1920
- });
1921
- }
1922
- // #1609 review F1: sweepOwnStagingFiles does not recurse, so the installer's
1923
- // bin/ and tools/ subdirectories (clients/installer/index.ts's
1924
- // GITHUB_BIN_DIR / TOOLS_DIR, now atomic-write.js writers too) need their
1925
- // own entries — otherwise an orphaned staging file from a kill mid-install
1926
- // (this PR's own motivating scenario) never gets reaped, and unique
1927
- // pid-thread-seq staging names mean repeated kills ACCUMULATE full-size
1928
- // orphan binaries instead of being cleaned up.
1929
- const globalDir = getGlobalPiLensDir();
1930
- void sweepAtomicWriteStages([
1931
- projectDataDir,
1932
- path.join(projectDataDir, "cache"),
1933
- path.join(projectDataDir, "sessions"),
1934
- globalDir,
1935
- path.join(globalDir, "bin"),
1936
- path.join(globalDir, "tools"),
1937
- ]).catch(() => {
1938
- // best-effort lifecycle cleanup — never fail session_start
1939
- });
1940
- if (quickMode) {
1941
- runtime.projectRoot = cwd;
1942
- const sequenceReadStartedAt = Date.now();
1943
- const snapshotRoot = resolveSnapshotRoot(cwd);
1944
- // #1785 F5 (round 4): the getter closes over `warmupOwnSnapshotRead`
1945
- // directly (a `let` in this same `handleSessionStart` invocation's
1946
- // scope), so it reads whatever value is current AT DEFERRED-RESOLVE
1947
- // TIME — not whatever it was at this wiring instant. See
1948
- // `retroactivelyHydrateAfterDeferredSequence`'s doc comment for the
1949
- // full design.
1950
- const { latestSeq, timedOut } = await readSequenceWithBudget({
1951
- snapshotRoot,
1952
- base: snapshotSequenceBase(snapshotRoot),
1953
- cwd,
1954
- runtime,
1955
- sessionGeneration: runtime.sessionGeneration,
1956
- dbg,
1957
- onDeferredSequenceResolved: retroactivelyHydrateAfterDeferredSequence({
1958
- getWarmupOwnSnapshotRead: () => warmupOwnSnapshotRead,
1959
- snapshotRoot,
1960
- runtime,
1961
- dbg,
1962
- }),
1963
- });
1964
- logLatency({
1965
- type: "phase",
1966
- phase: "session_start_sequence_read",
1967
- filePath: cwd,
1968
- startedAt: new Date(sequenceReadStartedAt).toISOString(),
1969
- durationMs: Date.now() - sequenceReadStartedAt,
1970
- metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
1971
- });
1972
- if (timedOut) {
1973
- recordSnapshotSequenceTimeout({
1974
- snapshotRoot,
1975
- snapshotPath: getProjectSnapshotPath(snapshotRoot),
1976
- });
1977
- }
1978
- runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
1979
- const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
1980
- dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
1981
- // #1162 review follow-up (P3): a timed-out read's cold sentinel is
1982
- // `projectSeq: 0`, which is indistinguishable from a project's real
1983
- // first-ever snapshot (persisted with `seq === 0` before any change was
1984
- // logged). Feeding `effectiveSeq` (0) straight into the freshness check
1985
- // would let that legit seq-0 snapshot match and hydrate as FRESH even
1986
- // though the on-disk log has since moved past it. Use a value the
1987
- // freshness check can never match (`snapshot.seq` is always >= 0) ONLY
1988
- // for this gate when the read timed out — `runtime.projectSeq` itself
1989
- // stays 0 (untouched), which is what Fix 1's advancement guard above
1990
- // relies on to detect an in-window bump.
1991
- const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
1992
- const snapshotLoadStartedAt = Date.now();
1993
- const snapshotPath = getProjectSnapshotPath(snapshotRoot);
1994
- let snapshotBytes = 0;
1995
- try {
1996
- snapshotBytes = nodeFs.statSync(snapshotPath).size;
1997
- }
1998
- catch {
1999
- // Missing snapshots are the normal cold-start case.
2000
- }
2001
- const snapshotBodyPresent = nodeFs.existsSync(snapshotPath) ||
2002
- nodeFs.existsSync(getProjectSnapshotLegacyPath(snapshotRoot));
2003
- const snapshotGate = loadSnapshotBodyUnlessStale({
2004
- root: snapshotRoot,
2005
- currentProjectSeq: freshnessSeq,
2006
- dbg,
2007
- });
2008
- const snapshot = snapshotGate.snapshot;
2009
- const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
2010
- const snapshotMissReason = describeSnapshotMiss(snapshot, freshnessSeq, {
2011
- skippedStale: snapshotGate.skippedStale,
2012
- bodyPresent: snapshotBodyPresent,
2013
- });
2014
- logLatency({
2015
- type: "phase",
2016
- phase: "session_start_snapshot_load",
2017
- filePath: cwd,
2018
- startedAt: new Date(snapshotLoadStartedAt).toISOString(),
2019
- durationMs: Date.now() - snapshotLoadStartedAt,
2020
- metadata: {
2021
- bytes: snapshotBytes,
2022
- fresh: snapshotFresh,
2023
- seq: snapshot?.seq ?? null,
2024
- reason: snapshotFresh ? undefined : snapshotMissReason,
2025
- ...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
2026
- ...(timedOut ? { sequenceUnknown: true } : {}),
2027
- },
2028
- });
2029
- logProjectSnapshotProbe({
2030
- dbg,
2031
- root: snapshotRoot,
2032
- currentProjectSeq: freshnessSeq,
2033
- snapshot,
2034
- missReason: snapshotMissReason,
2035
- });
2036
- if (snapshotFresh) {
2037
- hydrateRuntimeFromProjectSnapshot(runtime, snapshot);
2038
- }
2039
- const quickTools = [];
2040
- if (!getFlag("no-lsp")) {
2041
- quickTools.push("LSP Service");
2042
- }
2043
- log(`Active tools: ${quickTools.join(", ")}`);
2044
- dbg(`session_start tools: ${quickTools.join(", ") || "deferred (quick mode)"}`);
2045
- dbg("session_start: quick mode active - skipping slow tool probes, language profiling, preinstall, scans, and error debt baseline");
2046
- // #1911: the debug line above says WHICH steps were skipped, but nothing
2047
- // bounded or structured said so — quick mode's absence of work and an
2048
- // absent LOGGER read identically in latency.log. This record is that
2049
- // line's structured twin: one bounded gauge per quick-mode session_start,
2050
- // naming exactly the step set skipped, so a reader can tell "quick mode
2051
- // correctly skipped these" from "the probes silently never ran".
2052
- logLatency({
2053
- type: "phase",
2054
- phase: "session_start_skipped_steps",
2055
- filePath: cwd,
2056
- durationMs: 0,
2057
- metadata: {
2058
- mode: startupMode,
2059
- reason: deps.sessionReason,
2060
- // #1911 review F3: this `if (quickMode)` branch is an early return —
2061
- // the whole quick path — so there is no enumerable list of "steps
2062
- // run vs. skipped" this array could be derived from mechanically. It
2063
- // is DESCRIPTIVE documentation of what the early return skips,
2064
- // matching the `dbg()` line above word for word; keep both in sync by
2065
- // hand if either changes. It also does NOT narrow under
2066
- // `getFlag("no-lsp")` — quick mode skips the same five steps either
2067
- // way; the LSP flag instead affects `quickTools` above, a separate
2068
- // record.
2069
- steps: [
2070
- "slow_tool_probes",
2071
- "language_profiling",
2072
- "tool_preinstall",
2073
- "startup_scans",
2074
- "error_debt_baseline",
2075
- ],
2076
- },
2077
- });
2078
- const totalDurationMs = Date.now() - sessionStartMs;
2079
- dbg(`session_start total: ${totalDurationMs}ms (interactive path)`);
2080
- logLatency({
2081
- type: "phase",
2082
- phase: "session_start_total",
2083
- filePath: cwd,
2084
- startedAt: new Date(sessionStartMs).toISOString(),
2085
- durationMs: totalDurationMs,
2086
- metadata: {
2087
- mode: startupMode,
2088
- reason: deps.sessionReason,
2089
- // #2129: the root-identity input decideSessionStart consulted,
2090
- // alongside `mode` — every start reaching this line already
2091
- // classified `primary`/`sequential-replacement` (a `secondary-root`
2092
- // start returns before `handleSessionStart` is ever called).
2093
- classification: deps.sessionStartClassification,
2094
- // `undefined` is omitted by JSON.stringify. Keep unknown explicit so
2095
- // strict log readers can distinguish it from legacy omission.
2096
- sameRoot: deps.sessionStartSameRoot ?? "unknown",
2097
- },
2098
- });
2099
- logHostReadyDelay(deps, cwd);
2100
- emitSmellsSessionStartLine(dbg, sessionStartMs);
2101
- return;
2102
- }
2103
- const tools = [];
2104
- if (!getFlag("no-lsp"))
2105
- tools.push("LSP Service");
2106
- if (allowBootstrapTasks && !getFlag("no-lsp")) {
2107
- const cleaned = cleanStaleTsBuildInfo(ctxCwd ?? process.cwd());
2108
- if (cleaned.length > 0) {
2109
- notify(`🧹 Deleted stale TypeScript build cache (${cleaned.map((f) => path.basename(f)).join(", ")}) — phantom errors suppressed.`, "warning");
2110
- dbg(`session_start: cleaned stale tsbuildinfo: ${cleaned.join(", ")}`);
2111
- }
2112
- }
2113
- // Captured here (not at first use, below) because #1162's bounded read
2114
- // needs it to gate the deferred reseed against a stale/moved-on session —
2115
- // it's stable for the rest of this call (no further `resetForSession`
2116
- // between here and the later uses of the same generation).
2117
- const sessionGeneration = runtime.sessionGeneration;
2118
- const sequenceReadStartedAt = Date.now();
2119
- const snapshotRoot = resolveSnapshotRoot(cwd);
2120
- const { latestSeq, timedOut } = await readSequenceWithBudget({
2121
- snapshotRoot,
2122
- base: snapshotSequenceBase(snapshotRoot),
2123
- cwd,
2124
- runtime,
2125
- sessionGeneration,
2126
- dbg,
2127
- });
2128
- logLatency({
2129
- type: "phase",
2130
- phase: "session_start_sequence_read",
2131
- filePath: cwd,
2132
- startedAt: new Date(sequenceReadStartedAt).toISOString(),
2133
- durationMs: Date.now() - sequenceReadStartedAt,
2134
- metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
2135
- });
2136
- if (timedOut) {
2137
- // #1785: instrumentation only here, deliberately no
2138
- // `onDeferredSequenceResolved` — unlike quick mode's cold path (which
2139
- // does nothing when the snapshot isn't trusted), full mode's cold path
2140
- // actively kicks off a real rescan below to rebuild this same state.
2141
- // Retroactively hydrating from the on-disk snapshot once the deferred
2142
- // read lands could overwrite that fresher, actively-computed state with
2143
- // the stale disk copy — a regression, not a fix. See the PR body for the
2144
- // full reasoning.
2145
- recordSnapshotSequenceTimeout({
2146
- snapshotRoot,
2147
- snapshotPath: getProjectSnapshotPath(snapshotRoot),
2148
- });
2149
- }
2150
- runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
2151
- const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
2152
- dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
2153
- // #1162 review follow-up (P3) — see the matching comment in the quick-mode
2154
- // path above: a timed-out read's cold sentinel (`projectSeq: 0`) must
2155
- // never satisfy the freshness check, or a project's real first-ever
2156
- // snapshot (persisted at `seq === 0`) would hydrate as fresh despite the
2157
- // on-disk log having moved past it. `runtime.projectSeq` itself is left
2158
- // at 0, which Fix 1's advancement guard above depends on.
2159
- const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
2160
- const snapshotLoadStartedAt = Date.now();
2161
- const snapshotPath = getProjectSnapshotPath(snapshotRoot);
2162
- let snapshotBytes = 0;
2163
- try {
2164
- snapshotBytes = nodeFs.statSync(snapshotPath).size;
2165
- }
2166
- catch {
2167
- // Missing snapshots are the normal cold-start case.
2168
- }
2169
- const snapshotBodyPresent = nodeFs.existsSync(snapshotPath) ||
2170
- nodeFs.existsSync(getProjectSnapshotLegacyPath(snapshotRoot));
2171
- const snapshotGate = loadSnapshotBodyUnlessStale({
2172
- root: snapshotRoot,
2173
- currentProjectSeq: freshnessSeq,
2174
- dbg,
2175
- });
2176
- const snapshot = snapshotGate.snapshot;
2177
- const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
2178
- const snapshotMissReason = describeSnapshotMiss(snapshot, freshnessSeq, {
2179
- skippedStale: snapshotGate.skippedStale,
2180
- bodyPresent: snapshotBodyPresent,
2181
- });
2182
- logLatency({
2183
- type: "phase",
2184
- phase: "session_start_snapshot_load",
2185
- filePath: cwd,
2186
- startedAt: new Date(snapshotLoadStartedAt).toISOString(),
2187
- durationMs: Date.now() - snapshotLoadStartedAt,
2188
- metadata: {
2189
- bytes: snapshotBytes,
2190
- fresh: snapshotFresh,
2191
- seq: snapshot?.seq ?? null,
2192
- reason: snapshotFresh ? undefined : snapshotMissReason,
2193
- ...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
2194
- ...(timedOut ? { sequenceUnknown: true } : {}),
2195
- },
2196
- });
2197
- logProjectSnapshotProbe({
2198
- dbg,
2199
- root: snapshotRoot,
2200
- currentProjectSeq: freshnessSeq,
2201
- snapshot,
2202
- missReason: snapshotMissReason,
2203
- });
2204
- const freshSnapshot = snapshotFresh ? snapshot : null;
2205
- if (freshSnapshot) {
2206
- hydrateRuntimeFromProjectSnapshot(runtime, freshSnapshot);
2207
- }
2208
- // #699: a persisted `too-many-source-files` verdict is only reused while
2209
- // still within its TTL (isStartupScanVerdictFresh) — the seq-based
2210
- // freshness check above (isProjectSnapshotFresh) never fires for that
2211
- // reason on its own, since pi-lens never wrote anything while
2212
- // canWarmCaches was false, so the snapshot's seq never advances.
2213
- const cachedStartupScan = freshSnapshot?.startupScan &&
2214
- isStartupScanVerdictFresh(freshSnapshot.startupScan)
2215
- ? freshSnapshot.startupScan
2216
- : undefined;
2217
- const startupScanSource = cachedStartupScan ? "snapshot" : "computed";
2218
- let startupScan;
2219
- if (cachedStartupScan) {
2220
- startupScan = { ...cachedStartupScan, cwd: path.resolve(cwd) };
2221
- }
2222
- else {
2223
- const scanContextStartedAt = Date.now();
2224
- startupScan = resolveStartupScanContext(cwd);
2225
- logLatency({
2226
- type: "phase",
2227
- phase: "session_start_scan_context_compute",
2228
- filePath: cwd,
2229
- startedAt: new Date(scanContextStartedAt).toISOString(),
2230
- durationMs: Date.now() - scanContextStartedAt,
2231
- metadata: { mode: startupMode },
2232
- });
2233
- }
2234
- phase("scan-context");
2235
- dbg(`session_start scan-context source=${startupScanSource}`);
2236
- const scanRoot = startupScan.projectRoot ?? cwd;
2237
- // Both "tree is too big" verdicts still found a real project root — the
2238
- // walk just refused to warm caches over it — so signals stay anchored at
2239
- // that root rather than falling back to cwd (#758 added too-many-entries
2240
- // as the sibling of too-many-source-files).
2241
- const useScanRootForSignals = startupScan.canWarmCaches ||
2242
- startupScan.reason === "too-many-source-files" ||
2243
- startupScan.reason === "too-many-entries";
2244
- const analysisRoot = useScanRootForSignals ? scanRoot : cwd;
2245
- runtime.projectRoot = cwd;
2246
- const languageProfileSource = freshSnapshot?.languageProfile
2247
- ? "snapshot"
2248
- : "computed";
2249
- const languageProfile = freshSnapshot?.languageProfile
2250
- ? freshSnapshot.languageProfile
2251
- : detectProjectLanguageProfile(analysisRoot, startupScan.canWarmCaches ? undefined : []);
2252
- phase("language-profile");
2253
- dbg(`session_start language-profile source=${languageProfileSource}`);
2254
- dbg(`session_start cwd: ${cwd}`);
2255
- dbg(`session_start scan root: ${scanRoot} (warmCaches=${startupScan.canWarmCaches}${startupScan.reason ? `, reason=${startupScan.reason}` : ""})`);
2256
- dbg(`session_start analysis root: ${analysisRoot}`);
2257
- dbg(`session_start workspace root: ${runtime.projectRoot}`);
2258
- dbg(`session_start language profile: ${languageProfile.detectedKinds.join(", ") || "none"}`);
2259
- dbg(`session_start language counts: ${JSON.stringify(languageProfile.counts)} configured=${JSON.stringify(languageProfile.configured)}`);
2260
- dbg(`session_start workspace cwd available: ${hasWorkspaceCwd}`);
2261
- if (useScanRootForSignals && analysisRoot !== cwd) {
2262
- dbg(`session_start: monorepo analysis root override -> ${analysisRoot}`);
2263
- }
2264
- // Slow-FS probe (#462): classify the workspace filesystem by measurement
2265
- // (median fs.statSync cost) before any tree walk runs. WSL 9p mounts cost
2266
- // ~1.3ms/stat vs ~17µs native — a 75x slowdown that turns a 5,000-file sync
2267
- // walk into a multi-second TUI freeze. Logged to the latency log so
2268
- // dogfooding can see the verdict; a visible notice fires when engaged so a
2269
- // degraded scan is never mistaken for a silently-empty one.
2270
- const slowFsVerdict = getSlowFsVerdict(analysisRoot);
2271
- logLatency({
2272
- type: "phase",
2273
- phase: "slow_fs_probe",
2274
- filePath: analysisRoot,
2275
- durationMs: 0,
2276
- metadata: {
2277
- slow: slowFsVerdict.slow,
2278
- medianStatMicros: slowFsVerdict.medianStatMicros,
2279
- samples: slowFsVerdict.samples,
2280
- },
2281
- });
2282
- dbg(`session_start slow-fs probe: slow=${slowFsVerdict.slow} medianStatMicros=${slowFsVerdict.medianStatMicros.toFixed(1)} samples=${slowFsVerdict.samples}`);
2283
- if (slowFsVerdict.slow) {
2284
- notify(`🐢 Slow filesystem detected (median ${slowFsVerdict.medianStatMicros.toFixed(0)}µs/stat) — reduced-scan mode engaged (set PI_LENS_ALLOW_SLOW_FS_SCAN=1 to override).`, "warning");
2285
- }
2286
- // Subagent light mode (#449 slice 0): detected once per session, alongside
2287
- // the slow-FS probe above. Logged to the latency log so dogfooding can see
2288
- // how often subagent fan-outs engage it and what identity they carry.
2289
- const subagentSession = isSubagentSession();
2290
- if (isWarmAttached()) {
2291
- dbg("session_start lsp-warm: skipping pre-warm (attached to incumbent)");
2292
- }
2293
- else if (subagentSession) {
2294
- const identity = getSubagentIdentity();
2295
- logLatency({
2296
- type: "phase",
2297
- phase: "subagent_light_mode",
2298
- filePath: analysisRoot,
2299
- durationMs: 0,
2300
- metadata: {
2301
- runId: identity?.runId,
2302
- agentName: identity?.agentName,
2303
- marker: identity?.marker,
2304
- },
2305
- });
2306
- dbg(`session_start subagent light mode: engaged (marker=${identity?.marker ?? "unknown"} runId=${identity?.runId ?? "unknown"} agentName=${identity?.agentName ?? "unknown"})`);
2307
- }
2308
- const lensLspEnabled = !getFlag("no-lsp");
2309
- const startupDefaults = getDefaultStartupTools(languageProfile).filter((tool) => {
2310
- if ((tool === "typescript-language-server" || tool === "pyright") &&
2311
- !lensLspEnabled) {
2312
- return false;
2313
- }
2314
- return true;
2315
- });
2316
- if (!allowBootstrapTasks) {
2317
- dbg("session_start: skipping tool preinstall (startup mode)");
2318
- }
2319
- else if (startupDefaults.length > 0) {
2320
- dbg(`session_start: pre-install defaults -> ${startupDefaults.join(", ")}`);
2321
- firePreinstallDefaults(ensureTool, dbg, startupDefaults);
2322
- }
2323
- else {
2324
- dbg("session_start: no language defaults selected for pre-install");
2325
- }
2326
- const startupScansEnabled = deps.projectConfig?.startup?.scans?.enabled ??
2327
- deps.globalConfig?.startup?.scans?.enabled ??
2328
- true;
2329
- const startupScansWillRun = allowBootstrapTasks && startupScan.canWarmCaches && startupScansEnabled;
2330
- const jstsHeavyScansWillRun = startupScansWillRun && canRunStartupHeavyScans(languageProfile, "jsts");
2331
- if (allowBootstrapTasks) {
2332
- scheduleDeferredToolProbes(deps, languageProfile, startupDefaults, jstsHeavyScansWillRun, dbg);
2333
- scheduleManagedToolRefresh(dbg);
2334
- }
2335
- if (allowBootstrapTasks) {
2336
- // Fire-and-forget like other tool probes
2337
- void probePrettierInstall(ensureTool, dbg, analysisRoot);
2338
- }
2339
- else {
2340
- dbg("session_start: skipping prettier preinstall probe (startup mode)");
2341
- }
2342
- // #2467: the first point on the FULL-mode path that genuinely needs the
2343
- // analyzer clients, so it is where the load is paid. Quick mode — the
2344
- // process's first session, the one the user is waiting on — returns above
2345
- // and never reaches here. Fail open: an unavailable bootstrap drops these
2346
- // three entries from the "Active tools" line rather than failing the start.
2347
- const summaryClients = await deps.bootstrap.request("session-start-tools");
2348
- const detectedRunner = summaryClients?.testRunnerClient.detectRunner(analysisRoot);
2349
- phase("test-runner-detect");
2350
- if (detectedRunner)
2351
- tools.push(`Test runner (${detectedRunner.runner})`);
2352
- if (await summaryClients?.goClient.isGoAvailableAsync())
2353
- tools.push("Go (go vet)");
2354
- if (await summaryClients?.rustClient.isAvailableAsync())
2355
- tools.push("Rust (cargo)");
2356
- log(`Active tools: ${tools.join(", ")}`);
2357
- dbg(`session_start tools: ${tools.join(", ")}`);
2358
- const agentStartupGuidance = SESSION_START_GUIDANCE;
2359
- runtime.projectRulesScan = scanProjectRules(analysisRoot);
2360
- saveRuntimeProjectSnapshot({
2361
- cwd: snapshotRoot,
2362
- runtime,
2363
- startupScan,
2364
- languageProfile,
2365
- dbg,
2366
- });
2367
- phase("project-rules");
2368
- if (runtime.projectRulesScan.hasCustomRules) {
2369
- const ruleCount = runtime.projectRulesScan.rules.length;
2370
- const sources = [
2371
- ...new Set(runtime.projectRulesScan.rules.map((r) => r.source)),
2372
- ];
2373
- dbg(`session_start: found ${ruleCount} project rule(s) from ${sources.join(", ")}`);
2374
- }
2375
- else {
2376
- dbg("session_start: no project rules found");
2377
- }
2378
- cacheManager.writeCache("session-start-guidance", { content: agentStartupGuidance.join("\n") }, analysisRoot);
2379
- // `sessionGeneration` was captured earlier (#1162's bounded sequence read
2380
- // needs it before this point) and is stable across this whole call.
2381
- if (!allowBootstrapTasks) {
2382
- dbg("session_start: skipping startup background scans (startup mode)");
2383
- }
2384
- else if (!startupScansEnabled) {
2385
- dbg("session_start: skipping startup background scans (disabled by config)");
2386
- recordDegradationOnce({
2387
- kind: "startup-analyzer-disabled",
2388
- subject: "startup-scans",
2389
- reason: "skipped (disabled by config)",
2390
- });
2391
- }
2392
- else if (!startupScan.canWarmCaches) {
2393
- dbg(`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`);
2394
- dbg(`session_start: skipping TODO scan (${startupScan.reason ?? "unknown"})`);
2395
- // #775: mirror the slow-fs notify above — a size-skipped warm pipeline is
2396
- // otherwise silent (debug-log only), so a large project can look like
2397
- // pi-lens just isn't scanning anything. Fires ONCE per session (this
2398
- // `else` branch runs once per handleSessionStart call — the
2399
- // dominant-language LSP pre-warm skip further down reuses this same
2400
- // verdict rather than notifying a second time).
2401
- if (startupScan.reason === "too-many-source-files" ||
2402
- startupScan.reason === "too-many-entries") {
2403
- const overrideHint = startupScan.reason === "too-many-entries"
2404
- ? ` (set PI_LENS_STARTUP_SCAN_MAX_ENTRIES=<n> to override the ${getStartupScanMaxEntries()}-entry cap)`
2405
- : "";
2406
- notify(`📦 Project-size limits disabled background warm scans (heavy scans, TODO scan, LSP pre-warm)${overrideHint}.`, "warning");
2407
- }
2408
- }
2409
- else {
2410
- scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg);
2411
- }
2412
- // LSP warm files — deferred to the next event-loop turn so the config walk
2413
- // (several ENOENT readFile calls up the directory tree) never runs on the
2414
- // interactive path. setImmediate guarantees handleSessionStart has already
2415
- // resolved before loadLSPConfig is even called.
2416
- //
2417
- // #449 slice 0: skip both the explicit warmFiles warm and the
2418
- // dominant-language auto-warm inside a subagent session — a fan-out of N
2419
- // subagents otherwise pays N full LSP pre-warms in the same cwd. Per-edit
2420
- // LSP dispatch is untouched (see `pipeline.ts`), so a subagent that
2421
- // actually edits code still gets diagnostics; it just spawns the server
2422
- // lazily on first edit instead of eagerly at session start.
2423
- if (subagentSession) {
2424
- dbg("session_start lsp-warm: skipping pre-warm (subagent session)");
2425
- }
2426
- else if (!getFlag("no-lsp") && allowBootstrapTasks) {
2427
- setImmediate(() => {
2428
- void loadLSPConfig(cwd).then((lspConfig) => {
2429
- const warmFiles = lspConfig.warmFiles ?? [];
2430
- dbg(`session_start lsp-config: loaded (${warmFiles.length} warm file(s) configured)`);
2431
- if (warmFiles.length > 0) {
2432
- igniteWarmFiles(cwd, warmFiles, runtime, sessionGeneration, dbg).catch((err) => dbg(`session_start lsp-warm: unhandled error: ${err}`));
2433
- }
2434
- else if (startupScan.canWarmCaches) {
2435
- // No explicit warmFiles — pre-spawn just the dominant language's
2436
- // LSP so the first edit doesn't pay the cold-spawn stall (#203).
2437
- // Only do the auto-discovery warm on guarded real project roots; on
2438
- // home/no-project/too-large roots this source walk can become the same
2439
- // delayed background tree scan that the startup-scan guard prevents.
2440
- igniteDominantLanguageWarm(analysisRoot, runtime, sessionGeneration, dbg).catch((err) => dbg(`session_start lsp-warm: unhandled dominant error: ${err}`));
2441
- }
2442
- else {
2443
- dbg(`session_start lsp-warm: skipping dominant-language auto-warm (${startupScan.reason ?? "unknown"})`);
2444
- }
2445
- });
2446
- });
2447
- phase("lsp-config");
2448
- }
2449
- setSessionLanguages(languageProfile.detectedKinds);
2450
- const totalDurationMs = Date.now() - sessionStartMs;
2451
- dbg(`session_start total: ${totalDurationMs}ms (interactive path; background tasks may continue)`);
2452
- logLatency({
2453
- type: "phase",
2454
- phase: "session_start_total",
2455
- filePath: cwd,
2456
- startedAt: new Date(sessionStartMs).toISOString(),
2457
- durationMs: totalDurationMs,
2458
- metadata: {
2459
- mode: startupMode,
2460
- reason: deps.sessionReason,
2461
- // #2129: see the quick-mode session_start_total record above.
2462
- classification: deps.sessionStartClassification,
2463
- // Keep the full path's durable shape identical to quick mode.
2464
- sameRoot: deps.sessionStartSameRoot ?? "unknown",
2465
- },
2466
- });
2467
- logHostReadyDelay(deps, cwd);
2468
- emitSmellsSessionStartLine(dbg, sessionStartMs);
2469
- }
2470
- /**
2471
- * #1123 item 3: one bounded `session_start` line surfacing smells the manual
2472
- * `npm run logs:smells` analyzer would otherwise only catch on demand — see
2473
- * `clients/smells-rollup.ts` for the tail-scan cost bound and threshold
2474
- * gating. Never throws: a rollup miss must not break session_start.
2475
- */
2476
- function emitSmellsSessionStartLine(dbg, sessionStartMs) {
2477
- try {
2478
- const line = formatSmellsSessionStartLine(countRecentSmells(undefined, sessionStartMs));
2479
- if (line)
2480
- dbg(line);
2481
- }
2482
- catch {
2483
- // best-effort — smells rollup must never break session_start
2484
- }
2485
- }