@herbertgao/pi-extensions 2026.10.1 → 2026.10.3

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 (934) hide show
  1. package/README.md +14 -17
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  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/.qoder/rules/ponytail.md +21 -21
  10. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  11. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  12. package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  15. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  16. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  17. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  18. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
  19. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  20. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
  21. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
  22. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
  23. package/node_modules/{remote-pi → @herbertgao/pi-agency}/LICENSE +1 -1
  24. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  25. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  26. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  32. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  33. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  34. package/node_modules/@herbertgao/pi-subagents/README.md +1 -1
  35. package/node_modules/@herbertgao/pi-subagents/package.json +4 -5
  36. package/node_modules/@herbertgao/pi-subagents/src/index.ts +1 -1
  37. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +1 -1
  38. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +48 -72
  39. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +1 -1
  40. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +15 -6
  41. package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +4 -1
  42. package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +9 -8
  43. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +6 -1
  44. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  45. package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
  46. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +13 -1
  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 +1159 -0
  67. package/node_modules/pi-lens/README.md +116 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  70. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  71. package/node_modules/pi-lens/dist/chunk-2FQ62SPM.js +83 -0
  72. package/node_modules/pi-lens/dist/chunk-2UMYMRI3.js +118 -0
  73. package/node_modules/pi-lens/dist/chunk-34OCDWOS.js +48 -0
  74. package/node_modules/pi-lens/dist/chunk-3FERWMCH.js +144 -0
  75. package/node_modules/pi-lens/dist/chunk-3JL6IFD7.js +97 -0
  76. package/node_modules/pi-lens/dist/chunk-3JSEWUWI.js +327 -0
  77. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  78. package/node_modules/pi-lens/dist/chunk-3MRXIJ6I.js +79 -0
  79. package/node_modules/pi-lens/dist/chunk-3P4PF2BX.js +2250 -0
  80. package/node_modules/pi-lens/dist/chunk-3PNFWVHT.js +201 -0
  81. package/node_modules/pi-lens/dist/chunk-3TTPKMEF.js +162 -0
  82. package/node_modules/pi-lens/dist/chunk-5JHXUETW.js +326 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-64LU2OVH.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6B5I6ICN.js +44 -0
  86. package/node_modules/pi-lens/dist/chunk-6P54TMWL.js +274 -0
  87. package/node_modules/pi-lens/dist/chunk-6UYA642J.js +145 -0
  88. package/node_modules/pi-lens/dist/chunk-6XX5HCLK.js +398 -0
  89. package/node_modules/pi-lens/dist/chunk-7AXO3ZDF.js +30 -0
  90. package/node_modules/pi-lens/dist/chunk-7BX5DWSA.js +1907 -0
  91. package/node_modules/pi-lens/dist/chunk-7ELY2CSM.js +902 -0
  92. package/node_modules/pi-lens/dist/chunk-7M4T5N3V.js +38 -0
  93. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  94. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  95. package/node_modules/pi-lens/dist/chunk-AFZYMAIV.js +756 -0
  96. package/node_modules/pi-lens/dist/chunk-ALQRYH6O.js +112 -0
  97. package/node_modules/pi-lens/dist/chunk-AXKPQ6ZI.js +1610 -0
  98. package/node_modules/pi-lens/dist/chunk-BHWHXMQ2.js +62 -0
  99. package/node_modules/pi-lens/dist/chunk-BJF25SUG.js +22 -0
  100. package/node_modules/pi-lens/dist/chunk-BNKLYOOO.js +18 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-CBD2B5CP.js +599 -0
  103. package/node_modules/pi-lens/dist/chunk-CFYISHUW.js +42 -0
  104. package/node_modules/pi-lens/dist/chunk-CMU4RMJB.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-CULQYFLU.js +14 -0
  106. package/node_modules/pi-lens/dist/chunk-D65GOCJB.js +289 -0
  107. package/node_modules/pi-lens/dist/chunk-D6YF5HKX.js +59 -0
  108. package/node_modules/pi-lens/dist/chunk-DMS52BEP.js +342 -0
  109. package/node_modules/pi-lens/dist/chunk-ELVWJYRV.js +211 -0
  110. package/node_modules/pi-lens/dist/chunk-EUT3B22Y.js +27 -0
  111. package/node_modules/pi-lens/dist/chunk-F5VLZHUW.js +146 -0
  112. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  113. package/node_modules/pi-lens/dist/chunk-FGALODG3.js +55 -0
  114. package/node_modules/pi-lens/dist/chunk-G3RLJYA2.js +2358 -0
  115. package/node_modules/pi-lens/dist/chunk-GAYGGICG.js +32 -0
  116. package/node_modules/pi-lens/dist/chunk-H2W2AXD3.js +419 -0
  117. package/node_modules/pi-lens/dist/chunk-H5EV3VMF.js +114 -0
  118. package/node_modules/pi-lens/dist/chunk-HREZQCT3.js +106 -0
  119. package/node_modules/pi-lens/dist/chunk-HSIRMK2D.js +517 -0
  120. package/node_modules/pi-lens/dist/chunk-IMSQR4WO.js +184 -0
  121. package/node_modules/pi-lens/dist/chunk-IQZXOZ7S.js +4816 -0
  122. package/node_modules/pi-lens/dist/chunk-IR6XFA5P.js +37 -0
  123. package/node_modules/pi-lens/dist/chunk-IUKPRZER.js +78 -0
  124. package/node_modules/pi-lens/dist/chunk-IUR76K6A.js +262 -0
  125. package/node_modules/pi-lens/dist/chunk-J3FZR3YY.js +98 -0
  126. package/node_modules/pi-lens/dist/chunk-J6DFZDFA.js +562 -0
  127. package/node_modules/pi-lens/dist/chunk-J7WJZ6W7.js +50 -0
  128. package/node_modules/pi-lens/dist/chunk-JONGANYH.js +202 -0
  129. package/node_modules/pi-lens/dist/chunk-KJCWXUUS.js +425 -0
  130. package/node_modules/pi-lens/dist/chunk-KJN4IOEJ.js +17 -0
  131. package/node_modules/pi-lens/dist/chunk-KMGPUKFD.js +25 -0
  132. package/node_modules/pi-lens/dist/chunk-LD7WFTRX.js +4533 -0
  133. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  134. package/node_modules/pi-lens/dist/chunk-LNOMA3MP.js +169 -0
  135. package/node_modules/pi-lens/dist/chunk-MGIWSROG.js +2334 -0
  136. package/node_modules/pi-lens/dist/chunk-MQA2YBAH.js +308 -0
  137. package/node_modules/pi-lens/dist/chunk-MTDRLSI7.js +21720 -0
  138. package/node_modules/pi-lens/dist/chunk-N2IRDBZI.js +2355 -0
  139. package/node_modules/pi-lens/dist/chunk-N77WFUL7.js +50 -0
  140. package/node_modules/pi-lens/dist/chunk-N7YNJE5R.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  142. package/node_modules/pi-lens/dist/chunk-NFXP47RV.js +999 -0
  143. package/node_modules/pi-lens/dist/chunk-NJF2X5XF.js +45 -0
  144. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  145. package/node_modules/pi-lens/dist/chunk-O7HZX7CA.js +309 -0
  146. package/node_modules/pi-lens/dist/chunk-OC7LYAU5.js +708 -0
  147. package/node_modules/pi-lens/dist/chunk-OEX24ECI.js +348 -0
  148. package/node_modules/pi-lens/dist/chunk-OYOB3ZFG.js +78 -0
  149. package/node_modules/pi-lens/dist/chunk-OYTMZXRF.js +41 -0
  150. package/node_modules/pi-lens/dist/chunk-OZJINN4O.js +84 -0
  151. package/node_modules/pi-lens/dist/chunk-PCHDGUKT.js +23 -0
  152. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  153. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  154. package/node_modules/pi-lens/dist/chunk-RJFED73B.js +79 -0
  155. package/node_modules/pi-lens/dist/chunk-RTQNRM5X.js +27 -0
  156. package/node_modules/pi-lens/dist/chunk-S6AMWKPC.js +189 -0
  157. package/node_modules/pi-lens/dist/chunk-T4QZ6FEH.js +308 -0
  158. package/node_modules/pi-lens/dist/chunk-T54VJRAD.js +5456 -0
  159. package/node_modules/pi-lens/dist/chunk-TA5L5KWQ.js +167 -0
  160. package/node_modules/pi-lens/dist/chunk-TQFBZVCG.js +27 -0
  161. package/node_modules/pi-lens/dist/chunk-U52SAGRM.js +62 -0
  162. package/node_modules/pi-lens/dist/chunk-UEAHAMEI.js +5814 -0
  163. package/node_modules/pi-lens/dist/chunk-UGSMCQ4D.js +327 -0
  164. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  165. package/node_modules/pi-lens/dist/chunk-USHG4DLH.js +459 -0
  166. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  167. package/node_modules/pi-lens/dist/chunk-V5MEBST3.js +4139 -0
  168. package/node_modules/pi-lens/dist/chunk-VGEVL5EQ.js +29 -0
  169. package/node_modules/pi-lens/dist/chunk-VPWKF3GN.js +87 -0
  170. package/node_modules/pi-lens/dist/chunk-VSMJK3ZJ.js +1250 -0
  171. package/node_modules/pi-lens/dist/chunk-VYKKMYVT.js +560 -0
  172. package/node_modules/pi-lens/dist/chunk-VZHJS6DR.js +442 -0
  173. package/node_modules/pi-lens/dist/chunk-WATC3IQR.js +213 -0
  174. package/node_modules/pi-lens/dist/chunk-WPE5FPGR.js +54 -0
  175. package/node_modules/pi-lens/dist/chunk-WSEWKCXU.js +188 -0
  176. package/node_modules/pi-lens/dist/chunk-WW5BLB74.js +659 -0
  177. package/node_modules/pi-lens/dist/chunk-XGUL4SLQ.js +406 -0
  178. package/node_modules/pi-lens/dist/chunk-XHSCVSOL.js +170 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-YLN4K6WD.js +385 -0
  181. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  182. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  183. package/node_modules/pi-lens/dist/chunk-Z6BB4JLQ.js +478 -0
  184. package/node_modules/pi-lens/dist/chunk-Z7QQA2KT.js +27 -0
  185. package/node_modules/pi-lens/dist/chunk-ZETYB3XG.js +2626 -0
  186. package/node_modules/pi-lens/dist/chunk-ZMOMPX2Z.js +87 -0
  187. package/node_modules/pi-lens/dist/chunk-ZOEYVXSV.js +149 -0
  188. package/node_modules/pi-lens/dist/chunk-ZZUFZUQZ.js +12120 -0
  189. package/node_modules/pi-lens/dist/index.js +39712 -29876
  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 +22688 -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 +5 -4
  200. package/node_modules/pi-lens/docs/configuration.md +28 -2
  201. package/node_modules/pi-lens/docs/custom-rules.md +25 -2
  202. package/node_modules/pi-lens/docs/environment-variables.md +86 -8
  203. package/node_modules/pi-lens/docs/features.md +45 -5
  204. package/node_modules/pi-lens/docs/globalconfig.md +2 -1
  205. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  206. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +37 -4
  207. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  208. package/node_modules/pi-lens/docs/mcp.md +21 -5
  209. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  210. package/node_modules/pi-lens/docs/pi-lens-fixer.md +160 -88
  211. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  212. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +736 -0
  213. package/node_modules/pi-lens/docs/pi-lens-monitor.md +75 -7
  214. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  215. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +189 -48
  216. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  217. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  218. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  219. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  220. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  221. package/node_modules/pi-lens/docs/schema/pi-lens-config-v1.json +530 -0
  222. package/node_modules/pi-lens/docs/servercapabilities.md +56 -49
  223. package/node_modules/pi-lens/docs/settings.md +1 -0
  224. package/node_modules/pi-lens/docs/usage.md +5 -1
  225. package/node_modules/pi-lens/docs/word-index.md +6 -0
  226. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  227. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  228. package/node_modules/pi-lens/package.json +18 -6
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-identity-operator-on-literals-test.yml +12 -2
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-js-test.yml +2 -0
  232. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-test.yml +2 -0
  233. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  234. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-identity-operator-on-literals.yml +20 -12
  235. package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error-js.yml +2 -2
  236. package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error.yml +2 -2
  237. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  238. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  239. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  240. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  241. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  242. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  243. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  244. package/node_modules/pi-web-access/README.md +10 -6
  245. package/node_modules/pi-web-access/ceramic.ts +219 -0
  246. package/node_modules/pi-web-access/curator-page.ts +3 -1
  247. package/node_modules/pi-web-access/curator-server.ts +1 -0
  248. package/node_modules/pi-web-access/dist/index.js +846 -588
  249. package/node_modules/pi-web-access/dist/mcp-cli.js +725 -516
  250. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  251. package/node_modules/pi-web-access/github-extract.ts +34 -2
  252. package/node_modules/pi-web-access/index.ts +72 -30
  253. package/node_modules/pi-web-access/openai-search.ts +3 -3
  254. package/node_modules/pi-web-access/package.json +2 -2
  255. package/node_modules/pi-web-access/page-query.ts +6 -9
  256. package/node_modules/pi-web-access/parallel-mcp.ts +2 -0
  257. package/node_modules/pi-web-access/query-rewrite.ts +2 -2
  258. package/node_modules/pi-web-access/storage.ts +49 -0
  259. package/node_modules/pi-web-access/summary-review.ts +7 -6
  260. package/node_modules/pi-web-access/utils.ts +21 -6
  261. package/node_modules/pi-web-access/web-tool-core.ts +1 -0
  262. package/package.json +19 -19
  263. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
  264. package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
  265. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
  266. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
  267. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  268. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  269. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  270. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  271. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  272. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  273. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  274. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  275. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  276. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  277. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  278. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  279. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  280. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  281. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  282. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  283. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  284. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  285. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  286. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  287. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  288. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  289. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  290. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  291. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  292. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  293. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  294. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  295. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  296. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  297. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  298. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  299. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  300. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  301. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  302. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  303. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  304. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  305. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  306. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  307. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  308. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  309. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  310. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  311. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  312. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  313. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  314. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  315. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  316. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  317. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  318. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  319. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  320. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  321. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  322. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  323. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  324. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  325. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  326. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  327. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  328. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  329. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  330. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  331. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  332. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  333. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  334. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  335. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  336. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  337. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  338. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  339. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  340. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  341. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  342. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  343. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  344. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  345. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  346. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  347. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  348. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  349. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  350. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  351. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  352. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  353. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  354. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  355. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  356. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  357. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  358. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  359. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  360. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  361. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  362. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  363. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  364. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  365. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  366. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  367. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  368. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  369. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  370. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  371. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  372. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  373. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  374. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  375. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  376. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  377. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  378. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  379. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  380. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  381. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  382. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  383. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  384. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  385. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  386. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  387. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  388. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  389. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  390. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  391. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  392. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  393. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  394. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  395. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  396. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  397. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  398. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  399. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  400. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  401. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  402. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  403. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  404. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  405. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  406. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  407. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  447. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  448. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  449. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  450. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  451. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  452. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  453. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  454. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  455. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  456. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  457. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  458. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  459. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  460. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  461. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  462. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  463. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  464. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  465. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  466. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  467. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  468. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  469. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  470. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  471. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  472. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  473. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  474. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  475. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  476. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  477. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  478. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  479. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  480. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  481. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  482. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  483. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  484. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  485. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  486. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  487. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  488. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  489. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  490. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  491. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  492. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  493. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  494. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  495. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  496. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  497. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  498. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  499. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  500. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  501. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  502. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  503. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  504. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  505. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  506. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  507. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  508. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  509. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  510. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  511. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  512. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  513. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  514. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  515. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  516. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  517. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  518. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  519. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  520. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  521. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  522. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  523. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  524. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  525. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  526. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  527. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  528. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  529. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  530. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  531. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  532. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  533. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  534. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  535. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  536. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  537. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  538. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  539. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  540. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  541. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  542. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  543. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  544. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  545. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  546. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  547. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  548. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  549. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  550. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  551. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  552. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  553. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  554. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  555. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  556. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  557. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  558. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  559. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  560. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  561. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  562. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  563. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  564. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  565. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  566. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  567. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  568. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  569. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  570. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  571. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  572. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  573. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  574. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  575. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  576. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  577. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  578. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  579. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  580. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  581. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  582. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  583. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  584. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  585. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  586. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  587. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  588. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  589. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  590. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  591. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  592. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  593. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  594. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  595. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  596. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  597. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  598. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  599. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  600. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  601. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  602. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  603. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  604. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  605. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  606. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  607. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  608. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  609. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  610. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  611. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  612. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  613. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  614. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  615. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  616. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  617. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  618. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  619. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  620. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  621. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  623. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  624. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  625. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  626. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  627. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  628. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  629. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  630. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  631. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  632. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  633. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  634. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  635. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  636. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  637. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  638. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  639. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  640. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  641. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  642. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  643. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  644. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  645. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  646. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  647. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  648. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  649. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  650. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  651. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  652. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  653. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  654. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  655. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  656. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  657. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  658. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  659. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  660. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  661. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  662. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  663. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  664. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  665. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  666. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  667. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  668. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  669. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  670. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  671. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  672. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  673. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  674. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  675. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  676. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  677. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  678. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  679. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  680. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  681. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  682. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  683. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  684. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  685. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  686. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  687. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  688. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  689. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  690. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  691. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  692. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  693. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  694. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  695. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  696. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  697. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  698. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  699. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  700. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  701. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  702. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  703. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  704. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  705. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  706. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  707. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  708. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  709. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  710. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  711. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  712. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  713. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  714. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  715. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  716. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  717. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  718. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  719. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  720. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  721. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  722. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  723. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  724. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  725. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  726. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  727. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  728. package/node_modules/pi-lens/dist/i18n.js +0 -69
  729. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  730. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  731. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  732. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  733. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  734. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  735. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  736. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  737. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  738. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  739. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  740. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  741. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  742. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  743. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  744. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  745. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  746. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  747. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  748. package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
  749. package/node_modules/pi-web-access/opencode-session-headers.ts +0 -24
  750. package/node_modules/remote-pi/README.md +0 -675
  751. package/node_modules/remote-pi/dist/actions/handlers.d.ts +0 -122
  752. package/node_modules/remote-pi/dist/actions/handlers.js +0 -160
  753. package/node_modules/remote-pi/dist/actions/handlers.js.map +0 -1
  754. package/node_modules/remote-pi/dist/actions/registry.d.ts +0 -39
  755. package/node_modules/remote-pi/dist/actions/registry.js +0 -58
  756. package/node_modules/remote-pi/dist/actions/registry.js.map +0 -1
  757. package/node_modules/remote-pi/dist/bin/supervisord.d.ts +0 -2
  758. package/node_modules/remote-pi/dist/bin/supervisord.js +0 -86
  759. package/node_modules/remote-pi/dist/bin/supervisord.js.map +0 -1
  760. package/node_modules/remote-pi/dist/commands/builtin_mirror.d.ts +0 -58
  761. package/node_modules/remote-pi/dist/commands/builtin_mirror.js +0 -71
  762. package/node_modules/remote-pi/dist/commands/builtin_mirror.js.map +0 -1
  763. package/node_modules/remote-pi/dist/commands/list_commands.d.ts +0 -60
  764. package/node_modules/remote-pi/dist/commands/list_commands.js +0 -73
  765. package/node_modules/remote-pi/dist/commands/list_commands.js.map +0 -1
  766. package/node_modules/remote-pi/dist/config.d.ts +0 -62
  767. package/node_modules/remote-pi/dist/config.js +0 -115
  768. package/node_modules/remote-pi/dist/config.js.map +0 -1
  769. package/node_modules/remote-pi/dist/daemon/client.d.ts +0 -20
  770. package/node_modules/remote-pi/dist/daemon/client.js +0 -131
  771. package/node_modules/remote-pi/dist/daemon/client.js.map +0 -1
  772. package/node_modules/remote-pi/dist/daemon/control_protocol.d.ts +0 -176
  773. package/node_modules/remote-pi/dist/daemon/control_protocol.js +0 -63
  774. package/node_modules/remote-pi/dist/daemon/control_protocol.js.map +0 -1
  775. package/node_modules/remote-pi/dist/daemon/cron_log.d.ts +0 -45
  776. package/node_modules/remote-pi/dist/daemon/cron_log.js +0 -72
  777. package/node_modules/remote-pi/dist/daemon/cron_log.js.map +0 -1
  778. package/node_modules/remote-pi/dist/daemon/cron_registry.d.ts +0 -80
  779. package/node_modules/remote-pi/dist/daemon/cron_registry.js +0 -194
  780. package/node_modules/remote-pi/dist/daemon/cron_registry.js.map +0 -1
  781. package/node_modules/remote-pi/dist/daemon/id.d.ts +0 -24
  782. package/node_modules/remote-pi/dist/daemon/id.js +0 -36
  783. package/node_modules/remote-pi/dist/daemon/id.js.map +0 -1
  784. package/node_modules/remote-pi/dist/daemon/install.d.ts +0 -170
  785. package/node_modules/remote-pi/dist/daemon/install.js +0 -547
  786. package/node_modules/remote-pi/dist/daemon/install.js.map +0 -1
  787. package/node_modules/remote-pi/dist/daemon/registry.d.ts +0 -63
  788. package/node_modules/remote-pi/dist/daemon/registry.js +0 -153
  789. package/node_modules/remote-pi/dist/daemon/registry.js.map +0 -1
  790. package/node_modules/remote-pi/dist/daemon/rpc_child.d.ts +0 -194
  791. package/node_modules/remote-pi/dist/daemon/rpc_child.js +0 -389
  792. package/node_modules/remote-pi/dist/daemon/rpc_child.js.map +0 -1
  793. package/node_modules/remote-pi/dist/daemon/supervisor.d.ts +0 -93
  794. package/node_modules/remote-pi/dist/daemon/supervisor.js +0 -596
  795. package/node_modules/remote-pi/dist/daemon/supervisor.js.map +0 -1
  796. package/node_modules/remote-pi/dist/extension_ui_bridge.d.ts +0 -26
  797. package/node_modules/remote-pi/dist/extension_ui_bridge.js +0 -341
  798. package/node_modules/remote-pi/dist/extension_ui_bridge.js.map +0 -1
  799. package/node_modules/remote-pi/dist/index.d.ts +0 -213
  800. package/node_modules/remote-pi/dist/index.js +0 -4996
  801. package/node_modules/remote-pi/dist/index.js.map +0 -1
  802. package/node_modules/remote-pi/dist/mcp/mesh_result.d.ts +0 -10
  803. package/node_modules/remote-pi/dist/mcp/mesh_result.js +0 -42
  804. package/node_modules/remote-pi/dist/mcp/mesh_result.js.map +0 -1
  805. package/node_modules/remote-pi/dist/mcp/mesh_server.d.ts +0 -16
  806. package/node_modules/remote-pi/dist/mcp/mesh_server.js +0 -327
  807. package/node_modules/remote-pi/dist/mcp/mesh_server.js.map +0 -1
  808. package/node_modules/remote-pi/dist/mesh/canonical.d.ts +0 -30
  809. package/node_modules/remote-pi/dist/mesh/canonical.js +0 -61
  810. package/node_modules/remote-pi/dist/mesh/canonical.js.map +0 -1
  811. package/node_modules/remote-pi/dist/mesh/client.d.ts +0 -18
  812. package/node_modules/remote-pi/dist/mesh/client.js +0 -125
  813. package/node_modules/remote-pi/dist/mesh/client.js.map +0 -1
  814. package/node_modules/remote-pi/dist/mesh/encoding.d.ts +0 -38
  815. package/node_modules/remote-pi/dist/mesh/encoding.js +0 -216
  816. package/node_modules/remote-pi/dist/mesh/encoding.js.map +0 -1
  817. package/node_modules/remote-pi/dist/mesh/self_revoke.d.ts +0 -73
  818. package/node_modules/remote-pi/dist/mesh/self_revoke.js +0 -401
  819. package/node_modules/remote-pi/dist/mesh/self_revoke.js.map +0 -1
  820. package/node_modules/remote-pi/dist/mesh/siblings.d.ts +0 -49
  821. package/node_modules/remote-pi/dist/mesh/siblings.js +0 -159
  822. package/node_modules/remote-pi/dist/mesh/siblings.js.map +0 -1
  823. package/node_modules/remote-pi/dist/mesh/types.d.ts +0 -34
  824. package/node_modules/remote-pi/dist/mesh/types.js +0 -11
  825. package/node_modules/remote-pi/dist/mesh/types.js.map +0 -1
  826. package/node_modules/remote-pi/dist/mesh/verify.d.ts +0 -17
  827. package/node_modules/remote-pi/dist/mesh/verify.js +0 -86
  828. package/node_modules/remote-pi/dist/mesh/verify.js.map +0 -1
  829. package/node_modules/remote-pi/dist/pairing/crypto.d.ts +0 -8
  830. package/node_modules/remote-pi/dist/pairing/crypto.js +0 -22
  831. package/node_modules/remote-pi/dist/pairing/crypto.js.map +0 -1
  832. package/node_modules/remote-pi/dist/pairing/handshake.d.ts +0 -28
  833. package/node_modules/remote-pi/dist/pairing/handshake.js +0 -113
  834. package/node_modules/remote-pi/dist/pairing/handshake.js.map +0 -1
  835. package/node_modules/remote-pi/dist/pairing/noise-sha256.d.ts +0 -16
  836. package/node_modules/remote-pi/dist/pairing/noise-sha256.js +0 -103
  837. package/node_modules/remote-pi/dist/pairing/noise-sha256.js.map +0 -1
  838. package/node_modules/remote-pi/dist/pairing/qr.d.ts +0 -59
  839. package/node_modules/remote-pi/dist/pairing/qr.js +0 -125
  840. package/node_modules/remote-pi/dist/pairing/qr.js.map +0 -1
  841. package/node_modules/remote-pi/dist/pairing/storage.d.ts +0 -120
  842. package/node_modules/remote-pi/dist/pairing/storage.js +0 -604
  843. package/node_modules/remote-pi/dist/pairing/storage.js.map +0 -1
  844. package/node_modules/remote-pi/dist/protocol/codec.d.ts +0 -7
  845. package/node_modules/remote-pi/dist/protocol/codec.js +0 -49
  846. package/node_modules/remote-pi/dist/protocol/codec.js.map +0 -1
  847. package/node_modules/remote-pi/dist/protocol/types.d.ts +0 -393
  848. package/node_modules/remote-pi/dist/protocol/types.js +0 -2
  849. package/node_modules/remote-pi/dist/protocol/types.js.map +0 -1
  850. package/node_modules/remote-pi/dist/rooms.d.ts +0 -29
  851. package/node_modules/remote-pi/dist/rooms.js +0 -57
  852. package/node_modules/remote-pi/dist/rooms.js.map +0 -1
  853. package/node_modules/remote-pi/dist/session/address.d.ts +0 -49
  854. package/node_modules/remote-pi/dist/session/address.js +0 -58
  855. package/node_modules/remote-pi/dist/session/address.js.map +0 -1
  856. package/node_modules/remote-pi/dist/session/agent_bridge.d.ts +0 -55
  857. package/node_modules/remote-pi/dist/session/agent_bridge.js +0 -146
  858. package/node_modules/remote-pi/dist/session/agent_bridge.js.map +0 -1
  859. package/node_modules/remote-pi/dist/session/bridge.d.ts +0 -37
  860. package/node_modules/remote-pi/dist/session/bridge.js +0 -130
  861. package/node_modules/remote-pi/dist/session/bridge.js.map +0 -1
  862. package/node_modules/remote-pi/dist/session/broker.d.ts +0 -189
  863. package/node_modules/remote-pi/dist/session/broker.js +0 -507
  864. package/node_modules/remote-pi/dist/session/broker.js.map +0 -1
  865. package/node_modules/remote-pi/dist/session/broker_remote.d.ts +0 -74
  866. package/node_modules/remote-pi/dist/session/broker_remote.js +0 -583
  867. package/node_modules/remote-pi/dist/session/broker_remote.js.map +0 -1
  868. package/node_modules/remote-pi/dist/session/cwd_lock.d.ts +0 -33
  869. package/node_modules/remote-pi/dist/session/cwd_lock.js +0 -126
  870. package/node_modules/remote-pi/dist/session/cwd_lock.js.map +0 -1
  871. package/node_modules/remote-pi/dist/session/envelope.d.ts +0 -33
  872. package/node_modules/remote-pi/dist/session/envelope.js +0 -117
  873. package/node_modules/remote-pi/dist/session/envelope.js.map +0 -1
  874. package/node_modules/remote-pi/dist/session/global_config.d.ts +0 -33
  875. package/node_modules/remote-pi/dist/session/global_config.js +0 -73
  876. package/node_modules/remote-pi/dist/session/global_config.js.map +0 -1
  877. package/node_modules/remote-pi/dist/session/ipc.d.ts +0 -27
  878. package/node_modules/remote-pi/dist/session/ipc.js +0 -22
  879. package/node_modules/remote-pi/dist/session/ipc.js.map +0 -1
  880. package/node_modules/remote-pi/dist/session/leader_election.d.ts +0 -32
  881. package/node_modules/remote-pi/dist/session/leader_election.js +0 -106
  882. package/node_modules/remote-pi/dist/session/leader_election.js.map +0 -1
  883. package/node_modules/remote-pi/dist/session/local_config.d.ts +0 -55
  884. package/node_modules/remote-pi/dist/session/local_config.js +0 -157
  885. package/node_modules/remote-pi/dist/session/local_config.js.map +0 -1
  886. package/node_modules/remote-pi/dist/session/mesh_message.d.ts +0 -20
  887. package/node_modules/remote-pi/dist/session/mesh_message.js +0 -28
  888. package/node_modules/remote-pi/dist/session/mesh_message.js.map +0 -1
  889. package/node_modules/remote-pi/dist/session/mesh_node.d.ts +0 -116
  890. package/node_modules/remote-pi/dist/session/mesh_node.js +0 -573
  891. package/node_modules/remote-pi/dist/session/mesh_node.js.map +0 -1
  892. package/node_modules/remote-pi/dist/session/peer.d.ts +0 -139
  893. package/node_modules/remote-pi/dist/session/peer.js +0 -451
  894. package/node_modules/remote-pi/dist/session/peer.js.map +0 -1
  895. package/node_modules/remote-pi/dist/session/peer_inventory.d.ts +0 -13
  896. package/node_modules/remote-pi/dist/session/peer_inventory.js +0 -48
  897. package/node_modules/remote-pi/dist/session/peer_inventory.js.map +0 -1
  898. package/node_modules/remote-pi/dist/session/peer_limits.d.ts +0 -9
  899. package/node_modules/remote-pi/dist/session/peer_limits.js +0 -21
  900. package/node_modules/remote-pi/dist/session/peer_limits.js.map +0 -1
  901. package/node_modules/remote-pi/dist/session/setup_wizard.d.ts +0 -39
  902. package/node_modules/remote-pi/dist/session/setup_wizard.js +0 -63
  903. package/node_modules/remote-pi/dist/session/setup_wizard.js.map +0 -1
  904. package/node_modules/remote-pi/dist/session/tool_gate.d.ts +0 -5
  905. package/node_modules/remote-pi/dist/session/tool_gate.js +0 -11
  906. package/node_modules/remote-pi/dist/session/tool_gate.js.map +0 -1
  907. package/node_modules/remote-pi/dist/session/tools.d.ts +0 -24
  908. package/node_modules/remote-pi/dist/session/tools.js +0 -252
  909. package/node_modules/remote-pi/dist/session/tools.js.map +0 -1
  910. package/node_modules/remote-pi/dist/session/wizard.d.ts +0 -13
  911. package/node_modules/remote-pi/dist/session/wizard.js +0 -20
  912. package/node_modules/remote-pi/dist/session/wizard.js.map +0 -1
  913. package/node_modules/remote-pi/dist/settings.d.ts +0 -15
  914. package/node_modules/remote-pi/dist/settings.js +0 -52
  915. package/node_modules/remote-pi/dist/settings.js.map +0 -1
  916. package/node_modules/remote-pi/dist/transport/peer_channel.d.ts +0 -37
  917. package/node_modules/remote-pi/dist/transport/peer_channel.js +0 -98
  918. package/node_modules/remote-pi/dist/transport/peer_channel.js.map +0 -1
  919. package/node_modules/remote-pi/dist/transport/pi_forward_client.d.ts +0 -29
  920. package/node_modules/remote-pi/dist/transport/pi_forward_client.js +0 -62
  921. package/node_modules/remote-pi/dist/transport/pi_forward_client.js.map +0 -1
  922. package/node_modules/remote-pi/dist/transport/relay_client.d.ts +0 -91
  923. package/node_modules/remote-pi/dist/transport/relay_client.js +0 -206
  924. package/node_modules/remote-pi/dist/transport/relay_client.js.map +0 -1
  925. package/node_modules/remote-pi/dist/ui/footer.d.ts +0 -32
  926. package/node_modules/remote-pi/dist/ui/footer.js +0 -34
  927. package/node_modules/remote-pi/dist/ui/footer.js.map +0 -1
  928. package/node_modules/remote-pi/docs/daemon.md +0 -289
  929. package/node_modules/remote-pi/package.json +0 -92
  930. package/node_modules/remote-pi/service-templates/launchd.plist.template +0 -35
  931. package/node_modules/remote-pi/service-templates/systemd.service.template +0 -19
  932. package/node_modules/remote-pi/service-templates/task-launcher.vbs.template +0 -10
  933. package/node_modules/remote-pi/service-templates/task-scheduler.xml.template +0 -38
  934. package/node_modules/remote-pi/skills/agent-network/SKILL.md +0 -310
@@ -1,3767 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { buildActionableWarningsReport, formatActionableWarningsAdvisory, publishActionableWarningsReport, writeDeferredActionableWarningsReport, } from "./actionable-warnings.js";
4
- import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
5
- import { appendCodeQualityWarningsHistory, buildCodeQualityWarningsReport, formatCodeQualityWarningsAdvisory, writeCodeQualityWarningsReport, } from "./code-quality-warnings.js";
6
- import { clearGitGuardTestFailure, mergeGitGuardTestFailure, resyncGitGuardAfterInlinePolicy, writeGitGuardRecord, } from "./git-guard.js";
7
- import { cascadeSettleWaitMs } from "./cascade-budget.js";
8
- import { logCascade } from "./cascade-logger.js";
9
- import { normalizeMapKey } from "./path-utils.js";
10
- import { compareOrdinal } from "./string-utils.js";
11
- import { resolveRunnerPath, toRunnerDisplayPath, } from "./dispatch/runner-context.js";
12
- import { getKnipIgnorePatterns } from "./file-utils.js";
13
- import { formatCacheAgeLabel } from "./finding-delivery-gate.js";
14
- import { getFullScanWallClockMs, isWorkspaceSweepActive, runWhenWorkspaceSweepIdle, SWEEP_IDLE_SAFETY_MARGIN_MS, } from "./lsp/workspace-sweep-hold.js";
15
- import { isTestRoleCollateral } from "./collateral-test-role.js";
16
- import { isSecretWarning, secretLocationKey } from "./secret-findings.js";
17
- import { govulncheckLane } from "./turn-end/lanes/govulncheck.js";
18
- import { secretsLane } from "./turn-end/lanes/secrets.js";
19
- import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, stableFindingKey, } from "./dead-code-client.js";
20
- import { logDeadCodeScan } from "./dead-code-logger.js";
21
- import { PROJECT_DIAGNOSTICS_CACHE_VERSION, writeProjectDiagnosticsDeltaReport, } from "./project-diagnostics/cache.js";
22
- import { deadCodeIssueToProjectDiagnostic } from "./project-diagnostics/runner-adapters/dead-code.js";
23
- import { trivyFindingToProjectDiagnostic } from "./project-diagnostics/runner-adapters/trivy.js";
24
- import { knipIssuesToProjectDiagnostics } from "./project-diagnostics/runner-adapters/knip.js";
25
- import { logLatency } from "./latency-logger.js";
26
- import { getLspBudgetIdleTimeoutMs, shouldShortenLspIdleTimeout, } from "./lsp-budget.js";
27
- import { updateHeartbeat } from "./instance-registry.js";
28
- import { emitLensTurnFindings } from "./lens-events.js";
29
- import { RUNTIME_CONFIG } from "./runtime-config.js";
30
- import { isSubagentSession } from "./subagent-mode.js";
31
- import { formatRunDurationMs } from "./run-duration.js";
32
- import { isExcludedTestTarget, isRunnerErrorResult, RUNNERS, } from "./test-runner-client.js";
33
- import { MAX_ADVISORY_AFFECTED_FILES, gateFindingsByPathFreshness, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
34
- import { DEPENDENCY_DRIFT_MAX_DELIVERIES, sweepInlineBlockerFreshness, } from "./blocker-freshness.js";
35
- import { sweepInlineBlockerPastEof } from "./blocker-past-eof.js";
36
- // #2001/#2002: collect-later delivery for slow auxiliary LSP servers.
37
- import { getLSPService } from "./lsp/index.js";
38
- import { drainPendingAuxCapEvictedCount, drainPendingAuxiliaryCoverage, isPendingAuxiliaryPastRearmTtl, rearmPendingAuxiliaryCoverage, MAX_LATE_AUX_REARMS, pendingAuxiliaryCoverageSize, } from "./lsp/pending-aux-coverage.js";
39
- import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
40
- import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
41
- import { runPersistentReverify, } from "./persistent-reverify.js";
42
- import { cascadeCarrySuffix } from "./cascade-format.js";
43
- import { applyPushedFindingPolicy, filterFindingsByDisposition, } from "./dispatch/finding-policy.js";
44
- import { detectFileRole } from "./file-role.js";
45
- import { applyInlineBlockerPolicy, summarizeInlineBlockerPolicy, } from "./inline-blocker-dispositions.js";
46
- import { drainPendingRunnerFindings, dropStaleRunnerFindings, pendingRunnerFindingsSize, } from "./dispatch/pending-runner-findings.js";
47
- // #1631 review V2: moved to its own leaf module so a low-level store
48
- // (widget-state.ts) can use the marker without importing this orchestrator —
49
- // see clients/stale-marker.ts's doc comment.
50
- import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
51
- import { mapWithConcurrency } from "./map-with-concurrency.js";
52
- import { getAmbientAbortSignal } from "./safe-spawn.js";
53
- import { emitBounded } from "./bounded-telemetry.js";
54
- import { degradeDemotedFindingBody, formatDeliveryCapNote, formatRetirementNote, } from "./demoted-finding-render.js";
55
- import { STALE_LINE_MARKER } from "./stale-marker.js";
56
- import { getActiveSessionId } from "./session-lifecycle.js";
57
- import { bounded } from "./deadline-utils.js";
58
- import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
59
- import { drainRenderedDependencyDriftFilePaths, getWidgetBlockingFilesForSweep, incrementWidgetDependencyDriftDelivery, markWidgetFileBlockersStale, recordRunner, retireWidgetDependencyDriftBlockers, } from "./widget-state.js";
60
- /** Maximum detailed notify-stall coverage-gap rows emitted in one turn. */
61
- const LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN = 20;
62
- /**
63
- * #2504 — bounds on the per-turn test-runner fan-out.
64
- *
65
- * The reported turn fired 59 `vitest.cmd` spawns at once from a bare
66
- * `Promise.allSettled(targets.map(...))`. Each spawn carries its OWN 60 s
67
- * timeout (`test-runner-client.ts`), which bounds one target and says nothing
68
- * about the batch: 59 of them starved the event loop for the whole turn
69
- * (`cpuCoverageRatio 0.56`, 20 orphan-backstop escalations inside the storm).
70
- *
71
- * Three separate bounds, because they fail differently:
72
- * - CONCURRENCY caps how much CPU the batch can hold at one instant;
73
- * - TARGET COUNT caps how much work one turn may enqueue at all;
74
- * - the WALL BUDGET caps how long the batch may keep spawning, and is raced
75
- * against the ambient abort signal so a cancelled turn stops dispatching
76
- * immediately rather than at the next natural boundary.
77
- *
78
- * #2522: the batch budget shipped at 90 s in #2509 — well over the turn
79
- * itself, so a slow/hung batch could still hold up the agent for most of a
80
- * turn before the bound even engaged. Reconciled down to 20 s (one constant,
81
- * not a second budget alongside it): well under the turn, and still an order
82
- * of magnitude below the PER-TARGET 60 s cap in `test-runner-client.ts`, so
83
- * the batch bound is the one that actually fires first for a stuck target.
84
- *
85
- * #2522 review round 2: lowering the budget on its own made things WORSE,
86
- * because the "deferred to the next turn" this comment used to claim did not
87
- * exist. `runTestTargetsBounded` returned a skipped COUNT and threw the
88
- * identity of the unrun targets away, so a target slower than the budget
89
- * produced no result, no cache record and no output — every turn, forever —
90
- * while a PARTIAL batch fell through to the clean branch and was reported as
91
- * "all tests passed". The deferral is now a real mechanism:
92
- *
93
- * - the batch owns an `AbortController` that is handed to each target's spawn
94
- * (`TestRunRequest.signal` → `safeSpawnAsync`), so reaching the bound KILLS
95
- * what is in flight instead of leaving it to burn its own 60 s timeout and
96
- * then mutate a results array the caller has already consumed;
97
- * - the killed set plus the never-dispatched set is returned BY IDENTITY as
98
- * `deferred`, persisted on the `test-runner-findings` record as
99
- * `deferredTargets`, and dispatched FIRST on the next turn — ahead of
100
- * failed-first/related/self — under the same `TEST_RUNNER_MAX_TARGETS` cap;
101
- * - a batch whose `deferred` set is non-empty can never take the clean
102
- * branch, so an unfinished run is never reported as a green one and never
103
- * relaxes the `--lens-guard` commit gate.
104
- */
105
- export const TEST_RUNNER_BATCH_CONCURRENCY = 4;
106
- export const TEST_RUNNER_MAX_TARGETS = 12;
107
- export const TEST_RUNNER_BATCH_BUDGET_MS = 20_000;
108
- /**
109
- * How many consecutive turn-end batches a target may be cut out of before it
110
- * is retired from turn-end selection altogether.
111
- *
112
- * Without this the deferral is a LIVELOCK, and not hypothetically: this
113
- * repo's own `tests/index-integration.test.ts` — which turn-end resolves
114
- * through the `related` strategy whenever `clients/runtime-turn.ts` is
115
- * edited — takes 26.4 s, MORE than the entire 20 s batch budget on its own.
116
- * Deferring it puts it FIRST in the next batch, where it is cut again at
117
- * 20 s, and again, every turn, forever. A target that cannot fit the budget
118
- * is not a scheduling problem retrying can solve; it is reported once per
119
- * occurrence and dropped, so the turn stops paying 20 s for it.
120
- */
121
- export const TEST_RUNNER_MAX_DEFERRALS = 2;
122
- /**
123
- * Ceiling on how many entries either persisted target list may carry.
124
- *
125
- * #2522 review round 4, I1: a write may no longer destroy another session's
126
- * entries, so nothing in the write path prunes them any more — each session
127
- * that ever cut or retired a target in this project leaves its rows behind, on
128
- * a record read at every single turn_end. The bound is applied at the one
129
- * writer, on the whole list, and sheds FOREIGN rows first (the writer orders
130
- * this session's entries last), so it can never evict the entries this turn
131
- * depends on. Generous on purpose: it is a backstop against unbounded growth,
132
- * not a scheduling policy.
133
- */
134
- export const TEST_RUNNER_MAX_PERSISTED_TARGETS = 64;
135
- /**
136
- * Union two deferral sets by target identity, keeping the HIGHER attempt count
137
- * (#2522 review round 3, F3). Two overlapping batches can both be cut on the
138
- * same target; taking the lower count would let a target trade an attempt for
139
- * every overlap and never converge on `TEST_RUNNER_MAX_DEFERRALS`.
140
- *
141
- * Identity is (session, path), not path alone (#2522 review round 4, I1): two
142
- * sessions can each owe a run of the same file, and collapsing those into one
143
- * row makes the surviving row's `sessionId` decide whose deferral is honoured
144
- * and whose is silently dropped. The path half is keyed through
145
- * `normalizeMapKey` so `/`- and `\`-separated spellings are one entry
146
- * (AGENTS.md cross-form-path screen).
147
- */
148
- function deferralEntryKey(entry) {
149
- return `${entry.sessionId ?? ""}\u0000${normalizeMapKey(path.resolve(entry.testFile))}`;
150
- }
151
- function mergeDeferredTargets(existing, incoming) {
152
- const byKey = new Map();
153
- for (const entry of [...existing, ...incoming]) {
154
- const key = deferralEntryKey(entry);
155
- const prior = byKey.get(key);
156
- if (prior && (prior.attempts ?? 0) >= (entry.attempts ?? 0))
157
- continue;
158
- byKey.set(key, entry);
159
- }
160
- return [...byKey.values()];
161
- }
162
- /**
163
- * Run `run` over `targets` with a concurrency cap, a batch-wide wall budget
164
- * and an abort-signal race. Reuses `mapWithConcurrency` (the repo's existing
165
- * worker-pool shape) rather than hand-rolling a second pool.
166
- *
167
- * Both bounds are enforced twice on purpose: cooperatively, before each
168
- * dispatch, so no NEW work starts past the bound; and as a real race against
169
- * the pool, so an in-flight target cannot hold the batch past its budget.
170
- * Whatever has settled by then is returned — a partial batch of real results
171
- * is strictly better than none, and the caller reports the shortfall.
172
- */
173
- export async function runTestTargetsBounded(args) {
174
- const results = [];
175
- if (args.targets.length === 0)
176
- return { results, deferred: [] };
177
- let stopReason;
178
- // One controller for the whole batch. Aborting it tree-kills every child the
179
- // runner has in flight (`safeSpawnAsync` honours the signal it is handed),
180
- // which is what turns "the bound fired" into "the work actually stopped".
181
- const batchAbort = new AbortController();
182
- // Latched at the moment this call returns. Nothing may push into `results`
183
- // after that: the caller has already consumed the array, and a late push
184
- // would silently change a batch it has finished reasoning about.
185
- let closed = false;
186
- /**
187
- * #2522 review round 4, I4: "was this target CUT, or did it FINISH?" is a
188
- * semantic question, and this set is the answer — stamped by the `.then`
189
- * attached to `run`'s promise AT DISPATCH, which is that promise's FIRST
190
- * continuation. Nothing but a microtask can run between the promise settling
191
- * and this stamp, and every path that closes the batch (`setTimeout`, the
192
- * ambient signal's `abort` event, the pool settling after every mapper has
193
- * returned) reaches `finish()` from a macrotask or later. So `closed` read
194
- * here is exactly "the batch was already cut when this value existed".
195
- *
196
- * Round 3 read the same flag from the mapper's own `await` continuation,
197
- * which is several continuations downstream of the settle — a macrotask CAN
198
- * interleave there — and papered over the resulting misclassification with a
199
- * `setImmediate` hop plus a `latchedOut` parking array. Both are gone: the
200
- * hop made the answer depend on queue ordering, which is the thing that kept
201
- * producing findings (a spawn aborted BEFORE it started resolves
202
- * synchronously, so the hop folded it back in as a completed run — round 4
203
- * P3c).
204
- */
205
- const settledBeforeClose = new Set();
206
- const budgetMs = Math.max(0, args.budgetMs);
207
- const deadline = Date.now() + budgetMs;
208
- const shouldStop = () => {
209
- if (stopReason !== undefined)
210
- return true;
211
- if (args.signal?.aborted) {
212
- stopReason = "abort";
213
- return true;
214
- }
215
- if (Date.now() >= deadline) {
216
- stopReason = "budget";
217
- return true;
218
- }
219
- return false;
220
- };
221
- const pool = mapWithConcurrency(args.targets.map((target, index) => ({ target, index })), Math.max(1, args.concurrency), async ({ target, index }) => {
222
- if (shouldStop())
223
- return;
224
- const running = args.run(target, batchAbort.signal);
225
- // Attached HERE, at dispatch, so it is the promise's first
226
- // continuation — see `settledBeforeClose`. A value that only exists
227
- // because the batch was cut (a killed spawn's runner error, or
228
- // `safeSpawnAsync`'s synchronous "Spawn aborted before start") settles
229
- // strictly after the close and is therefore never recorded, so the
230
- // caller defers that target instead of reading it as a completed run.
231
- const stamp = running.then((value) => {
232
- if (closed)
233
- return;
234
- settledBeforeClose.add(index);
235
- results.push({ status: "fulfilled", value });
236
- }, (reason) => {
237
- if (closed)
238
- return;
239
- settledBeforeClose.add(index);
240
- results.push({ status: "rejected", reason });
241
- });
242
- // The stamp swallows both settlements, so this only keeps the pool's
243
- // concurrency slot held for as long as the work actually runs.
244
- await stamp;
245
- });
246
- // The pool's mapper swallows every throw from an async `run`; a `run` that throws
247
- // SYNCHRONOUSLY still rejects the pool and closes the whole batch (everything defers,
248
- // nothing publishes clean). Production runners are async, so this is fail-safe, not reachable.
249
- void pool.catch(() => { });
250
- await new Promise((resolve) => {
251
- let settled = false;
252
- const finish = (reason) => {
253
- if (settled)
254
- return;
255
- settled = true;
256
- if (reason !== undefined)
257
- stopReason ??= reason;
258
- // Latch BEFORE resolving, so no continuation scheduled by the kill
259
- // below can slip a result in behind the caller's back.
260
- closed = true;
261
- // Kill whatever is still in flight. Without this the pool ran on past
262
- // the bound: a stuck vitest spawn kept a core busy to its own 60s
263
- // timeout, long after the turn had moved on.
264
- batchAbort.abort();
265
- clearTimeout(timer);
266
- args.signal?.removeEventListener("abort", onAbort);
267
- resolve();
268
- };
269
- const onAbort = () => finish("abort");
270
- const timer = setTimeout(() => finish("budget"), budgetMs);
271
- // A pending batch timer must never hold the process open.
272
- timer.unref?.();
273
- if (args.signal?.aborted)
274
- finish("abort");
275
- else
276
- args.signal?.addEventListener("abort", onAbort, { once: true });
277
- void pool.then(() => finish(), () => finish());
278
- });
279
- return {
280
- results,
281
- deferred: args.targets.filter((_, index) => !settledBeforeClose.has(index)),
282
- stopReason,
283
- };
284
- }
285
- /**
286
- * Would writing `next` over `prev` throw away a good scan for a failed one?
287
- *
288
- * A failed run carries no findings. Writing it evicts the last good result, and
289
- * every later reader then serves the failure as the answer — a 194-byte "not
290
- * available" record replaced 149 KB of real findings in every dogfood project
291
- * (#925, #1467). Callers keep the previous cache when this returns true.
292
- */
293
- function wouldPoisonCache(prev, next) {
294
- return !next.success && prev?.data.success === true;
295
- }
296
- // LSP idle reset scheduling — prevents thrashing by delaying shutdown
297
- let lspIdleResetTimeout = null;
298
- // #1618: set while this timer's fire is deferred behind an in-flight
299
- // workspace sweep (see `scheduleLSPIdleReset`'s `isWorkspaceSweepActive`
300
- // branch). `cancelLSPIdleReset` must be able to cancel THIS too — otherwise
301
- // an active-editing turn that cancels idle reset while a sweep is still
302
- // running would have it silently resurrected once the sweep finishes, even
303
- // though the session is no longer idle.
304
- let pendingSweepRearm = null;
305
- function emitIdleResetReporterWarning(reportErr) {
306
- try {
307
- process.emitWarning(`pi-lens LSP idle reset error reporter failed: ${reportErr}`, { code: "PI_LENS_LSP_IDLE_RESET_REPORTER_FAILED" });
308
- }
309
- catch {
310
- // Preserve the detached-timer invariant: this path must never crash.
311
- void reportErr;
312
- }
313
- }
314
- function reportIdleResetError(onError, err) {
315
- try {
316
- onError?.(err);
317
- }
318
- catch (reportErr) {
319
- emitIdleResetReporterWarning(reportErr);
320
- }
321
- }
322
- function scheduleLSPIdleReset(resetFn, delayMs, options = {}) {
323
- // Clear any pending reset to avoid multiple timers. #1618: also cancel a
324
- // rearm still waiting on a prior sweep's hold — otherwise re-scheduling
325
- // here (this call) leaves that OLD waiter armed too, and the sweep's
326
- // eventual release would fire a SECOND, independent `scheduleLSPIdleReset`
327
- // alongside this fresh one.
328
- if (lspIdleResetTimeout) {
329
- clearTimeout(lspIdleResetTimeout);
330
- }
331
- if (pendingSweepRearm) {
332
- pendingSweepRearm.cancelled = true;
333
- pendingSweepRearm = null;
334
- }
335
- lspIdleResetTimeout = setTimeout(() => {
336
- lspIdleResetTimeout = null;
337
- // #1618: a full workspace sweep (`lens_diagnostics mode=full`) grants
338
- // itself a wall-clock ceiling that can outlive this timer's delay — this
339
- // used to fire straight into an in-flight sweep and destroy the very
340
- // service the sweep was actively touching, mislabeling every file the
341
- // sweep had not yet reached as budget exhaustion. Defer instead of
342
- // firing: re-arm a FRESH `delayMs` timer once the sweep releases its
343
- // hold, rather than resuming a countdown that's already elapsed (which
344
- // would fire the instant the hold releases) or destroying mid-sweep.
345
- if (isWorkspaceSweepActive()) {
346
- const rearmToken = { cancelled: false };
347
- pendingSweepRearm = rearmToken;
348
- runWhenWorkspaceSweepIdle(() => {
349
- if (rearmToken.cancelled)
350
- return;
351
- if (pendingSweepRearm === rearmToken)
352
- pendingSweepRearm = null;
353
- scheduleLSPIdleReset(resetFn, delayMs, options);
354
- });
355
- return;
356
- }
357
- try {
358
- if (options.isCurrentSession && !options.isCurrentSession()) {
359
- return;
360
- }
361
- if (options.isPrimarySession && !options.isPrimarySession()) {
362
- return;
363
- }
364
- resetFn();
365
- }
366
- catch (err) {
367
- // Detached timers run outside a pi event boundary. They must never crash
368
- // the extension process (for example if a host UI object was invalidated
369
- // by session replacement before the timer fired).
370
- reportIdleResetError(options.onError, err);
371
- }
372
- }, delayMs);
373
- // unref so this timer does not prevent the process from exiting naturally
374
- // (critical for subagent / --mode json -p usage where the process should
375
- // exit after completing its work, not wait 240 seconds for this to fire)
376
- lspIdleResetTimeout.unref();
377
- }
378
- // #1618 acceptance criterion 6: FULL_SCAN_WALL_CLOCK_MS (the full-sweep wall
379
- // clock ceiling, `tools/lens-diagnostics.ts`) must stay under EVERY idle
380
- // reset delay this module can arm — derived, not asserted, so the constants
381
- // can't drift back into a relationship where a still-running sweep can
382
- // outlive the timer. The AC1 hold above already makes a mid-sweep fire
383
- // impossible regardless of this margin; this is defense in depth against a
384
- // future caller that touches the LSP service outside
385
- // `runWorkspaceDiagnostics`' hold. `SWEEP_IDLE_SAFETY_MARGIN_MS` is
386
- // single-sourced from `workspace-sweep-hold.ts`, which also uses it for its
387
- // own max-hold-age failsafe — one tunable, not two.
388
- const DEFAULT_LSP_IDLE_RESET_MS = 240_000;
389
- function sweepDerivedFloorMs() {
390
- return getFullScanWallClockMs() + SWEEP_IDLE_SAFETY_MARGIN_MS;
391
- }
392
- /** The normal (non-subagent, non-budget-pressured) idle-reset delay. */
393
- function getBaseLspIdleResetMs() {
394
- return Math.max(DEFAULT_LSP_IDLE_RESET_MS, sweepDerivedFloorMs());
395
- }
396
- /**
397
- * #1618 (R4): the subagent-light (#713) and cross-process-budget-pressured
398
- * (#449) paths used to arm a flat, much SHORTER delay (60s default) than the
399
- * sweep's own 300s ceiling — a 5:1 inversion covered only by the AC1 hold.
400
- * Deriving this path too means AC6 ("the sweep's ceiling stays under every
401
- * idle-reset delay") holds universally, not just for the common path, and an
402
- * env override to either constant can never invert it (`Math.max` floors at
403
- * the derived value no matter how small the override pushes the other side).
404
- *
405
- * Accepted cost (deliberate, not incidental — see R6 in the PR body): under
406
- * default settings this now ALSO arms the ~360s derived floor rather than a
407
- * true 60s teardown, trading some of #713's "release a short-lived
408
- * subagent's fleet fast" benefit for AC6 holding without exceptions.
409
- */
410
- function getShortenedLspIdleResetMs() {
411
- return Math.max(getLspBudgetIdleTimeoutMs(), sweepDerivedFloorMs());
412
- }
413
- /** The idle-reset delay `handleTurnEnd` actually arms on a file-less turn —
414
- * exported so tests assert against the REAL computed value instead of a
415
- * hand-derived literal that can silently drift from this function. */
416
- export function getEffectiveLspIdleResetMs() {
417
- return isSubagentSession() || shouldShortenLspIdleTimeout()
418
- ? getShortenedLspIdleResetMs()
419
- : getBaseLspIdleResetMs();
420
- }
421
- export function cancelLSPIdleReset() {
422
- if (lspIdleResetTimeout) {
423
- clearTimeout(lspIdleResetTimeout);
424
- lspIdleResetTimeout = null;
425
- }
426
- if (pendingSweepRearm) {
427
- pendingSweepRearm.cancelled = true;
428
- pendingSweepRearm = null;
429
- }
430
- }
431
- function capTurnEndMessage(content) {
432
- const maxLines = RUNTIME_CONFIG.turnEnd.maxLines;
433
- const maxChars = RUNTIME_CONFIG.turnEnd.maxChars;
434
- let out = content;
435
- const lines = out.split("\n");
436
- if (lines.length > maxLines) {
437
- out = `${lines.slice(0, maxLines).join("\n")}\n... (truncated)`;
438
- }
439
- if (out.length > maxChars) {
440
- out = `${out.slice(0, maxChars)}\n... (truncated)`;
441
- }
442
- return out;
443
- }
444
- export async function handleTurnEnd(deps) {
445
- const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, host = "pi", owner, resetLSPService, resetFormatService, } = deps;
446
- const turnIndexAtDispatch = runtime.turnIndex;
447
- const clearOwnedTurnState = () => {
448
- if (runtime.turnIndex !== turnIndexAtDispatch) {
449
- dbg(`turn_end: retaining newer turn state (dispatch=${turnIndexAtDispatch}, current=${runtime.turnIndex})`);
450
- return;
451
- }
452
- cacheManager.clearTurnState(cwd, currentOwner);
453
- };
454
- // #449 slice 1: piggyback the instance-registry heartbeat on this existing
455
- // per-turn touchpoint rather than adding a new timer/interval. Cheap (reads
456
- // process.memoryUsage().rss, one read-modify-write of instances.json) and
457
- // fire-and-forget — the kill-switch check + no-op behavior live inside
458
- // updateHeartbeat itself, so this call site doesn't need to know about it.
459
- //
460
- // #620: intentionally RSS-only here — CPU%/LSP-child sampling (which shells
461
- // out to `pidusage`, and a full CIM query on Windows for a spawn's process
462
- // tree) is left to the quiet-window "instance_registry_heartbeat" task
463
- // (clients/quiet-window.ts's `buildHeartbeatResourcePatch`), which fires on
464
- // the idle `agent_settled` window rather than every single turn end. Every
465
- // turn end is a much hotter path than an idle window, and the issue's own
466
- // guardrail is not to let the measurement itself become a new source of
467
- // per-turn overhead worth investigating.
468
- void updateHeartbeat().catch(() => {
469
- // best-effort observability — never fail turn_end over this
470
- });
471
- const cwd = ctxCwd ?? process.cwd();
472
- let turnState = cacheManager.readTurnState(cwd);
473
- // A live foreign writer owns this worklist. Do not clear or consume another
474
- // pi/MCP session's files; a dead/aged owner is safely evicted instead.
475
- // #2504: `sessionStartedAt` dates the persisted worklist against THIS
476
- // session. Without it an ownerless turn-state.json — the resting shape
477
- // before #2504 — read back as "owned" no matter how old it was.
478
- const currentOwner = {
479
- ...(owner ?? {
480
- kind: "pi",
481
- id: runtime.telemetrySessionId,
482
- pid: process.pid,
483
- lastSeen: new Date().toISOString(),
484
- }),
485
- sessionStartedAt: owner?.sessionStartedAt ?? runtime.sessionStartedAt,
486
- };
487
- const access = cacheManager.getTurnStateAccess(cwd, currentOwner);
488
- // Captured BEFORE the eviction below rewrites the file: the owner the gate
489
- // actually judged. This pair is what would have settled #2504 from the
490
- // debug log alone.
491
- const gateOwnerLabel = turnState.owner
492
- ? `${turnState.owner.kind}:${turnState.owner.id}`
493
- : turnState.sessionId
494
- ? `legacy:${turnState.sessionId}`
495
- : "none";
496
- const sameProcessPiSessionHandoff = access === "foreign-live" &&
497
- currentOwner.kind === "pi" &&
498
- turnState.owner?.kind === "pi" &&
499
- turnState.owner.pid === process.pid &&
500
- turnState.owner.id !== currentOwner.id;
501
- if (access === "foreign-live" && !sameProcessPiSessionHandoff) {
502
- dbg(`turn_end: foreign live owner retained (${turnState.owner?.kind ?? "legacy"}:${turnState.owner?.id ?? turnState.sessionId})`);
503
- return;
504
- }
505
- if (access === "available" &&
506
- (turnState.files || turnState.owner || turnState.sessionId)) {
507
- dbg("turn_end: evicting stale turn-state owner");
508
- clearOwnedTurnState();
509
- turnState = cacheManager.readTurnState(cwd);
510
- }
511
- const files = Object.keys(turnState.files);
512
- /**
513
- * #2275: widget-footer sibling of #1950's inline-blocker cap, for the
514
- * widget store's OWN dependency-drift demotion
515
- * (`markWidgetFileBlockersStale`, driven by the freshness sweep further
516
- * down this function) — a completely separate store from
517
- * `RuntimeCoordinator`'s inline-blocker map, so it needed its own
518
- * delivery count (`WidgetDiagnostic.staleDeliveryCount`) rather than
519
- * inheriting one.
520
- *
521
- * Review F1: the population is what the footer RENDERED since the last
522
- * turn end, drained here — not every file that merely holds a demoted
523
- * row. The footer draws one record per pass (`withBlocking[0]`, its top
524
- * five entries) and may not be drawn at all, so a per-turn walk of the
525
- * whole store charged deliveries the agent never received and retired a
526
- * delivery early. This is the widget-surface analogue of the inline
527
- * loop's own `pendingDependencyDriftDeliveries` deferral below: both
528
- * commit a delivery only once the surface has actually served it. Every
529
- * `deliveryCount` reported to the ledger is therefore a count of RENDERS.
530
- *
531
- * Fix-round 3 (#2275 review F1): this drain/charge MUST run before the
532
- * `files.length === 0` early return below — a read-only turn (no
533
- * modified files) still repaints the footer and can draw a demoted row,
534
- * so a cap that only charged deliveries below the early return silently
535
- * starved on quiet turns: the footer re-rendered the same demoted row
536
- * every turn while the delivery count never advanced. The drain is a
537
- * Set.take() plus one map lookup per drained file — cheap enough to run
538
- * unconditionally on every turn end.
539
- */
540
- let widgetDemotedFindingsRetired = 0;
541
- for (const wPath of drainRenderedDependencyDriftFilePaths()) {
542
- const deliveryCount = incrementWidgetDependencyDriftDelivery(wPath);
543
- if (deliveryCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
544
- const capRetired = retireWidgetDependencyDriftBlockers(wPath);
545
- if (capRetired) {
546
- widgetDemotedFindingsRetired += 1;
547
- incrementDegradationCount({
548
- kind: "demoted-finding-retired",
549
- subject: `widget-blocker:${toRunnerDisplayPath(cwd, wPath)}`,
550
- reason: `capped after ${deliveryCount} deliveries with no re-run; hidden from the pi-lens footer, still listed by lens_diagnostics mode=all — re-run can still confirm`,
551
- });
552
- }
553
- }
554
- }
555
- // R1 (#1443 follow-up): a read-only turn (no files touched) must not take
556
- // the fast idle-reset path while a carried cascade run — or one still
557
- // settling — is waiting for its delivery opportunity. Falling through to
558
- // the normal pipeline lets the settle/drain/merge logic below run exactly
559
- // as it does for an edit turn, so a carried finding reaches the agent
560
- // instead of dying unrendered. `hasCascadeRuns()` is a cheap peek (no
561
- // pending work almost every turn), so the common read-only turn still
562
- // takes the early return below.
563
- // A foreign live owner must not deliver another session's pending findings.
564
- // A no-file turn falls through when this process has pending runner work so
565
- // the ordinary freshness gate and delivery cache can run. Max-cycle cleanup
566
- // below intentionally remains a terminal reset; its pending work stays in
567
- // the bounded handoff store for the next eligible turn.
568
- if (files.length === 0 && !runtime.hasCascadeRuns()) {
569
- // A genuinely clean session must invalidate the persisted guard record.
570
- // Blocker records are retained only while the runtime still reports one.
571
- if (getFlag("lens-guard") && !runtime.gitGuardHasBlockers) {
572
- const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
573
- if (guardRecord?.sessionId === runtime.telemetrySessionId &&
574
- guardRecord.testFailures !== true) {
575
- cacheManager.clearCache("turn-end-findings", cwd);
576
- }
577
- }
578
- // #713: subagent sessions use a shorter idle reset (nominally 60s) — a
579
- // short-lived task agent holding a warm fleet for 4 minutes after its
580
- // last turn is pure waste under fan-out. Classify ONCE here so every
581
- // tick in this call path shares the same answer. PI_LENS_SUBAGENT_FULL=1
582
- // restores the base delay via isSubagentSession() returning false.
583
- // #1618: both branches route through `getEffectiveLspIdleResetMs` so
584
- // AC6's derivation applies universally — see that function's doc for
585
- // why the "shorter" path is not always literally 60s anymore.
586
- const idleResetMs = getEffectiveLspIdleResetMs();
587
- dbg(`turn_end: no modified files, scheduling LSP idle reset (${idleResetMs / 1000}s)`);
588
- if (!getFlag("no-lsp")) {
589
- const sessionGeneration = runtime.sessionGeneration;
590
- scheduleLSPIdleReset(resetLSPService, idleResetMs, {
591
- isCurrentSession: () => runtime.isCurrentSession(sessionGeneration),
592
- // #2157 fix round 2: a secondary (subagent) evaluation's own timer
593
- // must not release the primary's shared fleet — see the option's
594
- // doc comment on `scheduleLSPIdleReset`.
595
- isPrimarySession: () => {
596
- const activePrimarySessionId = getActiveSessionId();
597
- return (activePrimarySessionId === undefined ||
598
- activePrimarySessionId === runtime.telemetrySessionId);
599
- },
600
- onError: (err) => dbg(`lsp idle reset failed: ${err}`),
601
- });
602
- }
603
- resetFormatService();
604
- if (pendingRunnerFindingsSize() === 0)
605
- return;
606
- }
607
- // Cancel any pending idle reset since we're actively working. #1618: also
608
- // checks `pendingSweepRearm` — a timer deferred behind an in-flight
609
- // workspace sweep already nulled `lspIdleResetTimeout` (the setTimeout
610
- // callback clears it before checking the hold), so this guard used to
611
- // read "nothing pending" and skip the cancel while a rearm was still
612
- // queued to fire the instant the sweep released its hold — resurrecting
613
- // idle reset on a session that had since gone back to active editing.
614
- if (files.length > 0 && (lspIdleResetTimeout || pendingSweepRearm)) {
615
- cancelLSPIdleReset();
616
- dbg("turn_end: cancelled pending LSP idle reset (active editing)");
617
- }
618
- dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}, access: ${access}, owner: ${gateOwnerLabel}`);
619
- if (cacheManager.isMaxCyclesExceeded(cwd)) {
620
- dbg("turn_end: max cycles exceeded, clearing state and forcing through");
621
- clearOwnedTurnState();
622
- runtime.fixedThisTurn.clear();
623
- resetFormatService();
624
- return;
625
- }
626
- const turnEndStart = Date.now();
627
- const blockerParts = [];
628
- /**
629
- * #1622 review M2: findings the freshness gate demoted. A third tier between
630
- * blockers and advisories — not a blocker, because the cached coordinate is
631
- * untrustworthy; not an advisory, because the advisory label reads "no action
632
- * required this turn" and these DO require a re-scan. Each part carries its
633
- * own imperative preamble rather than inheriting that label.
634
- */
635
- const staleSecretParts = [];
636
- const advisoryParts = [];
637
- const projectDiagnosticsDelta = [];
638
- const projectDiagnosticsSources = new Set();
639
- // #1641: past-EOF gate. Runs BEFORE the dependency-drift sweep below — a
640
- // cheap statSync per cited file is worth paying first so the pricier
641
- // import-parsing sweep can skip anything already taken out of the
642
- // authoritative channel this turn (see blocker-past-eof.ts's module doc
643
- // for the full composition rule with #1631's gate).
644
- const blockerPastEofStart = Date.now();
645
- const blockerPastEof = sweepInlineBlockerPastEof(runtime, cwd);
646
- logLatency({
647
- type: "phase",
648
- toolName: "turn_end",
649
- filePath: cwd,
650
- phase: "blocker_past_eof_sweep",
651
- durationMs: Date.now() - blockerPastEofStart,
652
- metadata: {
653
- total: blockerPastEof.total,
654
- checked: blockerPastEof.checked,
655
- demoted: blockerPastEof.demoted,
656
- // #1944 review F3: `healed` is gone. Retirement makes the falling edge
657
- // unreachable on this store, so the field could only ever log zero —
658
- // see `BlockerPastEofCounts`.
659
- },
660
- });
661
- // #1631: freshness gate. A cached blocker is a verdict about the file AND
662
- // everything it imports; before re-serving it, sweep for out-of-band drift of
663
- // the file or its forward imports and demote drifted entries to a
664
- // `[stale — re-run to confirm]` advisory instead of re-asserting them at full
665
- // authority (#1419 demote-not-drop).
666
- const blockerFreshnessStart = Date.now();
667
- // #1790: widen the sweep's population with widget-store rows a cache-served
668
- // replay populated without ever touching RuntimeCoordinator's inline-blocker
669
- // map — see blocker-freshness.ts's `WidgetSweepBlockerEntry` doc for why this
670
- // is injected here rather than imported by blocker-freshness.ts itself.
671
- const blockerFreshness = await sweepInlineBlockerFreshness(runtime, cwd, {
672
- // #2982: the hook's own signal, so the self axis's filesystem work is
673
- // bounded by the same abort everything else in this handler honours.
674
- ...(deps.signal === undefined ? {} : { signal: deps.signal }),
675
- additionalEntries: getWidgetBlockingFilesForSweep().map((row) => ({
676
- filePath: row.filePath,
677
- recordedAtMs: row.recordedAtMs,
678
- demote: () => markWidgetFileBlockersStale(row.filePath, "dependency-drift"),
679
- })),
680
- });
681
- logLatency({
682
- type: "phase",
683
- toolName: "turn_end",
684
- filePath: cwd,
685
- phase: "blocker_freshness_sweep",
686
- durationMs: Date.now() - blockerFreshnessStart,
687
- metadata: {
688
- total: blockerFreshness.total,
689
- kept: blockerFreshness.kept,
690
- revalidated: blockerFreshness.revalidated,
691
- alreadyStale: blockerFreshness.alreadyStale,
692
- truncatedImports: blockerFreshness.truncatedImports,
693
- selfHealed: blockerFreshness.selfHealed,
694
- selfUnverifiable: blockerFreshness.selfUnverifiable,
695
- hashBudgetExhausted: blockerFreshness.hashBudgetExhausted,
696
- },
697
- });
698
- // Re-surface inline blockers from this turn that the agent didn't fix.
699
- // These were shown inline during write/edit but the agent moved on without resolving them.
700
- const unresolvedBlockers = runtime.getInlineBlockersSnapshot();
701
- /** #1944/#1950: demotions retired after their delivery limit. */
702
- let demotedFindingsRetired = 0;
703
- /**
704
- * #1950 fix-round F1: dependency-drift delivery-count commits, deferred
705
- * until this turn's content is confirmed NOT suppressed by the
706
- * `turn-end-findings-last` signature dedupe further down. That dedupe
707
- * silences a turn whose rendered content is byte-identical to the last
708
- * one actually delivered — the agent never sees a suppressed turn, so
709
- * committing the counter for it would count a delivery that didn't
710
- * happen. Each entry here is invoked only from the "not suppressed"
711
- * branch below.
712
- */
713
- const pendingDependencyDriftDeliveries = [];
714
- /** #3246: one bounded record per TURN for the policy pass, never per finding. */
715
- const inlinePolicyEntries = [];
716
- /**
717
- * #3248: files whose EVERY blocker this pass suppressed — the post-policy
718
- * survivor set, emptied per file. The commit gate is recomputed from it
719
- * below, once, after the loop.
720
- */
721
- const policySuppressedPaths = [];
722
- const inlinePolicyStart = Date.now();
723
- for (const record of unresolvedBlockers) {
724
- const { filePath: bPath, summary, stale, staleReason } = record;
725
- const displayPath = toRunnerDisplayPath(cwd, bPath);
726
- const tally = { displayPath, sources: record.sources };
727
- if (stale) {
728
- inlinePolicyEntries.push({ ...tally, stale: true });
729
- // #1631: demoted — out of the authoritative blocker channel and into the
730
- // advisory channel with a stale marker, so the agent is told to re-run
731
- // rather than pressured by a verdict that may already be resolved.
732
- //
733
- // #1944: the CHANNEL change is not enough. Until this call the advisory
734
- // embedded the blocker body verbatim, so the agent read "🔴 STOP — 11
735
- // issue(s) must be fixed" with dead line numbers under a hedge line it
736
- // ignored. Degrade the body itself, and — when the file shrank past the
737
- // cited lines, so no re-run can ever confirm it — retire the record
738
- // after this ONE delivery instead of re-serving it for the rest of the
739
- // session.
740
- const deadLines = blockerPastEof.deadLinesByPath.get(bPath) ?? [];
741
- const degraded = degradeDemotedFindingBody(summary, { deadLines });
742
- const retired = runtime.retireDemotedPastEofBlocker(bPath, deadLines);
743
- let retirementNote;
744
- if (retired) {
745
- demotedFindingsRetired += 1;
746
- // Bounded by the ledger's own per-kind/subject tally, and the subject
747
- // keeps the discriminating identity (which store, which file).
748
- incrementDegradationCount({
749
- kind: "demoted-finding-retired",
750
- subject: `inline-blocker:${displayPath}`,
751
- reason: `file shrank past cited line(s) ${deadLines.join(", ")}; retired after one degraded delivery`,
752
- });
753
- retirementNote = formatRetirementNote(deadLines);
754
- }
755
- else if (staleReason === "dependency-drift") {
756
- // #1950: a dependency-drift demotion is recoverable (its coordinates
757
- // are still in bounds), so it does NOT retire after one delivery like
758
- // the past-EOF case above — but nothing capped how many times the
759
- // SAME demoted-but-unconfirmed record re-serves, and incident data
760
- // showed repeat deliveries carrying near-zero information after the
761
- // first. Cap it at DEPENDENCY_DRIFT_MAX_DELIVERIES instead.
762
- //
763
- // The count driving THIS render is a peek (fix-round F1): the actual
764
- // increment is deferred to `pendingDependencyDriftDeliveries` below,
765
- // committed only once this turn's content is known to reach the
766
- // agent, so a suppressed turn's tentative render never advances the
767
- // stored count.
768
- const tentativeCount = runtime.peekInlineBlockerStaleDeliveryCount(bPath) + 1;
769
- if (tentativeCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
770
- retirementNote = formatDeliveryCapNote(tentativeCount);
771
- }
772
- pendingDependencyDriftDeliveries.push(() => {
773
- const deliveryCount = runtime.incrementInlineBlockerStaleDelivery(bPath);
774
- if (deliveryCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
775
- const capRetired = runtime.retireDemotedDependencyDriftBlocker(bPath);
776
- if (capRetired) {
777
- demotedFindingsRetired += 1;
778
- incrementDegradationCount({
779
- kind: "demoted-finding-retired",
780
- subject: `inline-blocker:${displayPath}`,
781
- reason: `capped after ${deliveryCount} deliveries with no re-run; re-run can still confirm`,
782
- });
783
- }
784
- }
785
- });
786
- }
787
- // @delivery-surface: runtime-turn:unresolved-inline-blocker
788
- advisoryParts.push(`${STALE_LINE_MARKER} ${displayPath}:\n${degraded.body}` +
789
- (retirementNote ? `\n${retirementNote}` : ""));
790
- }
791
- else {
792
- // #3246: the agent may have marked one of these blockers
793
- // `false-positive` AFTER the record was written, via
794
- // `lens_diagnostic_mark` — which every other findings surface honors.
795
- // Re-derive the body from the record's structured diagnostics through
796
- // the shared `dispatch/finding-policy.ts` stack against the file's
797
- // CURRENT bytes, exactly as the late-auxiliary lane below does.
798
- const policy = applyInlineBlockerPolicy(record, cwd);
799
- inlinePolicyEntries.push({ ...tally, stale: false, outcome: policy });
800
- if (policy.body === undefined) {
801
- // Every blocker on this file was suppressed. This is a PUSH
802
- // surface: silence after a mark is the mark working, not a clean
803
- // verdict, so the count rides the bounded per-turn record below
804
- // instead of announcing the suppression on every later turn.
805
- policySuppressedPaths.push(bPath);
806
- continue;
807
- }
808
- // #1616 suppressed-bucket rule: a delivery that still has something to
809
- // say states what it dropped, once per delivery.
810
- const suppressedNote = policy.suppressed > 0
811
- ? ` (suppressed by disposition: ${policy.suppressed} finding(s))`
812
- : "";
813
- // @delivery-surface: runtime-turn:unresolved-inline-blocker
814
- blockerParts.push(`Unresolved from this turn — ${displayPath}${suppressedNote}:\n${policy.body}`);
815
- }
816
- }
817
- // #3248: the post-policy survivor set per file is FINAL here. The commit
818
- // gate reads a latch (`gitGuardHasBlockers`) before it ever reads the
819
- // persisted record, and the policy wrote neither — so a file whose every
820
- // blocker was just suppressed kept blocking `git commit` while the banner
821
- // above said nothing about it. Recompute the latch from this set; the
822
- // persisted record is rewritten or cleared further down by the writer that
823
- // already owns it (`:4287` / `:4379`), from the same survivor set, and that
824
- // clear is gated on the latch recomputed here.
825
- const guardLatchBefore = runtime.gitGuardHasBlockers;
826
- resyncGitGuardAfterInlinePolicy({
827
- runtime,
828
- suppressedFilePaths: policySuppressedPaths,
829
- });
830
- if (inlinePolicyEntries.length > 0) {
831
- logLatency({
832
- type: "phase",
833
- toolName: "turn_end",
834
- filePath: cwd,
835
- phase: "inline_blocker_policy",
836
- durationMs: Date.now() - inlinePolicyStart,
837
- metadata: {
838
- ...summarizeInlineBlockerPolicy(inlinePolicyEntries),
839
- // #3248: one row per TURN for the gate outcome, never per finding
840
- // — the flip is the event a reader needs to explain why a commit
841
- // that was blocked is now allowed.
842
- guardFilesSuppressed: policySuppressedPaths.length,
843
- guardLatchCleared: guardLatchBefore && !runtime.gitGuardHasBlockers,
844
- },
845
- });
846
- }
847
- // Drain the deferred cascade computes kicked off this turn (#450). They ran
848
- // concurrently off the write hot path; wait a bounded time for them here so
849
- // their runs are available to the merge below. A compute still in flight at
850
- // the cap is carried over to the next turn_end (never dropped).
851
- const cascadeSettleStart = Date.now();
852
- const { settled, timedOut } = await runtime.settleCascadeRuns(cascadeSettleWaitMs(), { trackTurnEndClock: true });
853
- logLatency({
854
- type: "phase",
855
- toolName: "turn_end",
856
- filePath: cwd,
857
- phase: "cascade_settle_wait",
858
- durationMs: Date.now() - cascadeSettleStart,
859
- metadata: { settled, timedOut },
860
- });
861
- // Merge accumulated cascade results from all pipeline runs this turn.
862
- // Two-pass dedup:
863
- // 1. Primary-level: dedup by primary file (last writer wins).
864
- // 2. Neighbor-level: each neighbor is claimed by the latest cascade result
865
- // that covers it — suppresses stale neighbor state from earlier writes.
866
- const t0 = Date.now();
867
- const cascadeRuns = runtime.consumeCascadeRuns().filter((run) => {
868
- const originSeq = run.origin?.projectSeq;
869
- const originTurn = run.origin?.turnSeq;
870
- // A deferred result from AFTER a later write is not current state. Old test
871
- // fixtures without provenance remain accepted for compatibility.
872
- //
873
- // #1443: `turnSeq` alone is NOT a supersede signal, and it used to be an
874
- // unconditional reject. Every LATE run — one whose compute missed the
875
- // settle cap and was re-parked by `settleCascadeRuns`, and one the
876
- // quiet-window reconcile appended after this turn's predecessor already
877
- // consumed (carried across turn_start by `beginTurn`) — is BY DEFINITION
878
- // from an earlier turn, so `originTurn === runtime.turnIndex` was always
879
- // false for exactly the runs the carry-over was built to preserve. Both
880
- // producers' contracts were dead code: the measured cases were the two
881
- // highest-fan-out cascades of the day (38 and 40 neighbours).
882
- //
883
- // R2 (#1443 follow-up): `projectSeq` alone is NOT a per-file supersede
884
- // signal — it is GLOBAL, advancing on every pi-observed write anywhere in
885
- // the project. Rejecting on any mismatch meant an edit to an unrelated
886
- // file superseded a run that had nothing to do with it, reintroducing the
887
- // exact 38/40-neighbour loss #1443 was written to fix, one filter down.
888
- // `getFilesChangedSince` (#451) is the honest per-file signal: a run is
889
- // superseded only if its own primary file or one of its neighbours was
890
- // actually rewritten since it launched. A late-but-not-superseded run is
891
- // surfaced; a superseded one is dropped with a RECORD (never silently),
892
- // so the loss stays countable.
893
- if (originSeq !== undefined) {
894
- const changedSince = runtime.getFilesChangedSince(originSeq);
895
- if (changedSince.length > 0) {
896
- const changedSet = new Set(changedSince);
897
- const primaryKey = normalizeMapKey(path.resolve(run.filePath));
898
- const neighborKeys = [
899
- ...(run.result?.neighbors ?? []).map((n) => n.filePath),
900
- ...(run.selectedNeighborPaths ?? []),
901
- ].map((filePath) => normalizeMapKey(path.resolve(filePath)));
902
- const supersededByOwnFile = changedSet.has(primaryKey) ||
903
- neighborKeys.some((k) => changedSet.has(k));
904
- if (supersededByOwnFile) {
905
- logCascade({
906
- phase: "cascade_carry_over_drop",
907
- filePath: run.filePath,
908
- neighborCount: run.neighborCount,
909
- diagnosticCount: run.diagnosticCount,
910
- reason: "superseded_by_later_write",
911
- metadata: {
912
- originProjectSeq: originSeq,
913
- projectSeq: runtime.projectSeq,
914
- originTurnSeq: originTurn,
915
- turnIndex: runtime.turnIndex,
916
- carriedTurns: run.carriedTurns,
917
- changedFiles: changedSince,
918
- },
919
- });
920
- return false;
921
- }
922
- }
923
- }
924
- return true;
925
- });
926
- const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
927
- // Fix B (#3167): which results belong to a CARRIED run — the label is
928
- // per-result at render time, so the run→result pairing must survive the
929
- // flatMap above (which discards the wrapper).
930
- // Fix B (#3168 F6): the success-path record — latency.log could not show
931
- // the feature ever rendered without it (the only phases were the drop/
932
- // settle records). One bounded record per turn, emitted after both render
933
- // sections below.
934
- let carriedRunsRendered = 0;
935
- let labeledAdvisories = 0;
936
- const carriedMetaByResult = new Map();
937
- for (const r of cascadeRuns) {
938
- if (r.result && (r.carriedTurns ?? 0) > 0 && r.carriedTurns !== undefined) {
939
- carriedMetaByResult.set(r.result, {
940
- carriedTurns: r.carriedTurns,
941
- observedAt: r.observedAt,
942
- });
943
- }
944
- }
945
- // #1550 class sweep: every cascade record below summarises `cascadeResults`
946
- // — runs, which carry their own paths and can be carried across turns
947
- // (#1443) — so labelling them with the turn's first EDITED file is the same
948
- // mis-attribution the `cascade_indeterminate` fix removes. On a read-only
949
- // drain turn `files` is empty and the old `?? cwd` fallback stamped a bare
950
- // DIRECTORY as the record's file. These three are turn-level AGGREGATES (no
951
- // per-file cause is claimed), so one label suffices; the edited file and cwd
952
- // stay as fallbacks.
953
- const cascadeLogFilePath = cascadeResults[0]?.filePath ?? files[0] ?? cwd;
954
- if (cascadeResults.length > 0) {
955
- const seen = new Map();
956
- for (const result of cascadeResults) {
957
- seen.set(normalizeMapKey(result.filePath), result);
958
- }
959
- // Iterate in reverse so the latest result claims each neighbor first.
960
- const neighborOwner = new Map();
961
- for (const result of [...seen.values()].reverse()) {
962
- const pk = normalizeMapKey(result.filePath);
963
- for (const n of result.neighbors) {
964
- const nk = normalizeMapKey(n.filePath);
965
- if (!neighborOwner.has(nk))
966
- neighborOwner.set(nk, pk);
967
- }
968
- }
969
- const parts = [];
970
- // #1446 item 1: track what actually gets injected — a suppressed result
971
- // (real formatted cascade text, but every one of its neighbors was claimed
972
- // by a LATER result — see the reverse-iteration ownership pass above) was
973
- // previously indistinguishable from "no output"; this counts it explicitly
974
- // instead of letting it vanish.
975
- let injectedNeighborCount = 0;
976
- let injectedDiagnosticCount = 0;
977
- let suppressedByOwnership = 0;
978
- for (const result of seen.values()) {
979
- const pk = normalizeMapKey(result.filePath);
980
- const ownsAny = result.neighbors.some((n) => neighborOwner.get(normalizeMapKey(n.filePath)) === pk);
981
- if (ownsAny && result.formatted) {
982
- // Fix B (#3167/#3168): a carried run's re-rendered blocker is labeled
983
- // so the agent can tell it from a fresh observation — with the
984
- // run's own observation age (#3168 F3).
985
- const carryMeta = carriedMetaByResult.get(result);
986
- const carrySuffix = cascadeCarrySuffix(carryMeta?.carriedTurns, carryMeta?.observedAt);
987
- parts.push(carrySuffix
988
- ? `${result.formatted}\n${carrySuffix}`
989
- : result.formatted);
990
- if (carrySuffix)
991
- carriedRunsRendered += 1;
992
- injectedNeighborCount += result.neighbors.length;
993
- injectedDiagnosticCount += result.neighbors.reduce((s, n) => s + n.diagnostics.length, 0);
994
- }
995
- else if (!ownsAny && result.formatted) {
996
- suppressedByOwnership++;
997
- }
998
- }
999
- // Suggest tests for cascade neighbors (files with diagnostics)
1000
- const neighborFilesWithErrors = cascadeResults
1001
- .flatMap((r) => r.neighbors)
1002
- .filter((n) => n.diagnostics.length > 0)
1003
- .map((n) => n.filePath);
1004
- const uniqueNeighborFiles = [...new Set(neighborFilesWithErrors)];
1005
- let testSuggestionCount = 0;
1006
- if (uniqueNeighborFiles.length > 0 &&
1007
- typeof testRunnerClient.suggestTestFiles === "function") {
1008
- const testSuggestions = testRunnerClient.suggestTestFiles(uniqueNeighborFiles, cwd);
1009
- testSuggestionCount = testSuggestions.length;
1010
- // #1446 item 2: this path previously emitted nothing to any log — a
1011
- // zero-suggestion outcome (neighbors had errors but no test file
1012
- // resolved for any of them) is the more interesting case, so it is
1013
- // recorded on the same phase rather than only logging on a hit.
1014
- logCascade({
1015
- phase: "cascade_test_targets",
1016
- filePath: cascadeLogFilePath,
1017
- neighborCount: uniqueNeighborFiles.length,
1018
- metadata: {
1019
- neighborFiles: uniqueNeighborFiles.slice(0, 10),
1020
- suggestedTestFiles: testSuggestions
1021
- .slice(0, 10)
1022
- .map((s) => s.testFile),
1023
- runner: testSuggestions[0]?.runner,
1024
- truncated: testSuggestions.length > 10,
1025
- zeroSuggestions: testSuggestions.length === 0,
1026
- },
1027
- });
1028
- if (testSuggestions.length > 0) {
1029
- const testLines = testSuggestions
1030
- .slice(0, 5)
1031
- .map((s) => ` ${toRunnerDisplayPath(cwd, s.testFile)} (${s.runner})`);
1032
- let testSection = `🧪 Likely tests for affected neighbors:\n${testLines.join("\n")}`;
1033
- if (testSuggestions.length > 5) {
1034
- testSection += `\n ... and ${testSuggestions.length - 5} more`;
1035
- }
1036
- parts.push(testSection);
1037
- }
1038
- }
1039
- if (parts.length > 0) {
1040
- const section = parts.join("\n\n");
1041
- // @delivery-surface: runtime-turn:cascade-blocker
1042
- blockerParts.push(section);
1043
- // #1446 item 1: proves the cascade section reached `blockerParts` —
1044
- // i.e. it was QUEUED for persistence into the turn-end advisory — not
1045
- // that it reached the agent. The counters alone (cascade_result,
1046
- // cascade_turn_end) never confirmed even that much, only computation.
1047
- // Actual delivery happens later, via consumeTurnEndFindings/
1048
- // peekTurnEndFindings, and can still be suppressed after this point
1049
- // (e.g. allFilesDeleted, cross-turn dedup, or the session ending
1050
- // before the next turn_end drains it) — this record does not prove
1051
- // the agent ever saw the text.
1052
- logCascade({
1053
- phase: "cascade_injected",
1054
- filePath: cascadeLogFilePath,
1055
- neighborCount: injectedNeighborCount,
1056
- diagnosticCount: injectedDiagnosticCount,
1057
- metadata: {
1058
- sectionChars: section.length,
1059
- testSuggestionCount,
1060
- suppressedByOwnership,
1061
- },
1062
- });
1063
- }
1064
- logCascade({
1065
- phase: "cascade_turn_end",
1066
- filePath: cascadeLogFilePath,
1067
- neighborCount: cascadeResults.reduce((s, r) => s + r.neighbors.length, 0),
1068
- diagnosticCount: cascadeResults.reduce((s, r) => s + r.neighbors.reduce((ns, n) => ns + n.diagnostics.length, 0), 0),
1069
- metadata: {
1070
- fileCount: cascadeResults.length,
1071
- mergedResults: seen.size,
1072
- },
1073
- });
1074
- }
1075
- // #1023: surface an HONEST note whenever a cascade run could not compute
1076
- // downstream impact (degraded/over-cap graph, missing node, a thrown compute,
1077
- // or a deliberately budget-truncated neighbor set) — never a silent all-clear
1078
- // (#533). This goes to the ADVISORY tier,
1079
- // NOT the blocker tier: in an over-cap monorepo the graph is `skipped` on
1080
- // every edit, so a blocker would fire hard and never clear turn state every
1081
- // turn (over-escalation — the mirror of the silent-all-clear bug). Advisory
1082
- // still reaches the agent, just without the blocker mechanics. Keyed strictly
1083
- // off the `indeterminate` marker threaded by the compute; a healthy build
1084
- // with a genuinely empty dependent set carries no marker and stays silent
1085
- // (over-correction guard).
1086
- const indeterminateRuns = cascadeRuns.filter((r) => r.indeterminate);
1087
- if (indeterminateRuns.length > 0) {
1088
- // #1104 (review P3 on PR #1143, rides with the resultId main body): this
1089
- // preamble used to hardcode a graph-unavailability frame for EVERY
1090
- // indeterminate reason. That's accurate for `graph_degraded`/
1091
- // `missing_node`/`error` (the graph really couldn't produce a dependent
1092
- // set), but `lsp_binding_rejected` is a DIFFERENT failure shape — the
1093
- // graph WAS available and dependents WERE derived; only their LSP
1094
- // diagnostics display was withheld because a fallback snapshot's content
1095
- // binding didn't match current disk. Saying "the review graph was
1096
- // unavailable" for that case mis-attributes the cause. Bucket by reason
1097
- // family so each gets its own accurate frame.
1098
- const buildAdvisory = (runs, frame) => {
1099
- if (runs.length === 0)
1100
- return undefined;
1101
- const byDetail = new Map();
1102
- for (const r of runs) {
1103
- const detail = r.indeterminate?.detail ?? frame.fallbackDetail(r);
1104
- const files = byDetail.get(detail) ?? [];
1105
- files.push(toRunnerDisplayPath(cwd, r.filePath));
1106
- byDetail.set(detail, files);
1107
- }
1108
- const lines = [];
1109
- for (const [detail, filesRaw] of byDetail) {
1110
- const files = [...new Set(filesRaw)];
1111
- const shown = files.slice(0, 5).join(", ");
1112
- const more = files.length > 5 ? ` (+${files.length - 5} more)` : "";
1113
- lines.push(` • ${detail}: ${shown}${more}`);
1114
- }
1115
- const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath)))
1116
- .size;
1117
- const reasons = [...byDetail.keys()].join("; ");
1118
- return `${frame.lead(fileCount, reasons)}\n${lines.join("\n")}`;
1119
- };
1120
- // #1445: `excluded_by_role` (test files excluded from the graph BY DESIGN,
1121
- // #260) is never agent-facing — it is not a graph failure, and #1080
1122
- // already excludes test-role files from every neighbor surface, so "a
1123
- // clean result does not cover them" would itself be a false claim. It
1124
- // stays visible in the `cascade_indeterminate` log below (metadata-only,
1125
- // info-level) so the log can tell an intentional exclusion from a real
1126
- // graph gap, but it never reaches `buildAdvisory`/the agent.
1127
- const graphRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason !== "lsp_binding_rejected" &&
1128
- r.indeterminate?.reason !== "excluded_by_role" &&
1129
- r.indeterminate?.reason !== "budget_truncated" &&
1130
- r.indeterminate?.budget === undefined);
1131
- const bindingRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "lsp_binding_rejected" &&
1132
- r.indeterminate?.budget === undefined);
1133
- // Budget coverage can be merged into a graph or binding marker, so its
1134
- // advisory bucket follows the evidence rather than replacing that reason.
1135
- const budgetRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "budget_truncated" ||
1136
- r.indeterminate?.budget !== undefined);
1137
- // Factual/informational phrasing — the advisory tier wraps this with an
1138
- // "ℹ️ Advisory — no action required this turn:" label, so an imperative
1139
- // ("review dependents manually") would contradict it. The #533 substance
1140
- // stays: a clean cascade result does NOT cover these files' dependents.
1141
- // Fix B (#3167/#3168 F4): a coverage advisory computed from a CARRIED
1142
- // indeterminate run describes the previous turn's evidence — label it so
1143
- // the absence-of-coverage statement is not read as current. A MIXED
1144
- // bucket (some carried, some fresh) is left UNLABELED: a Math.max
1145
- // suffix on the finished multi-line advisory would attach the carry to
1146
- // a file that was not carried.
1147
- const withCarryLabel = (advisory, runs) => {
1148
- if (advisory === undefined)
1149
- return undefined;
1150
- const carried = runs.filter((r) => (r.carriedTurns ?? 0) > 0);
1151
- if (carried.length === 0 || carried.length !== runs.length) {
1152
- return advisory;
1153
- }
1154
- // #3168 F13: the bucket's age is the OLDEST carried observation, and
1155
- // only when EVERY carried run carries one. A `Math.min` sentinel
1156
- // (`?? Number.MAX_SAFE_INTEGER`) silently ignored the unstamped runs
1157
- // and stated a confident age for a bucket that contains an unaged
1158
- // one; one missing stamp collapses the age half to the helper's
1159
- // neutral "scan age unknown" wording instead.
1160
- const stamps = carried.map((r) => r.observedAt);
1161
- const observedAt = stamps.every((s) => s !== undefined)
1162
- ? Math.min(...stamps)
1163
- : undefined;
1164
- const suffix = cascadeCarrySuffix(carried[0]?.carriedTurns, observedAt);
1165
- if (!suffix)
1166
- return advisory;
1167
- labeledAdvisories += 1;
1168
- // #3168 F11: newline, as the blocker path does above. A space join
1169
- // welded the suffix onto the LAST bullet of a multi-bullet advisory,
1170
- // so the carry label read as a property of that one file.
1171
- return `${advisory}\n${suffix}`;
1172
- };
1173
- const graphAdvisory = withCarryLabel(buildAdvisory(graphRuns, {
1174
- lead: (fileCount, reasons) => `Cascade could not compute downstream impact for ${fileCount} edited file(s) this turn — ` +
1175
- `the review graph was unavailable (${reasons}), so their dependents were not ` +
1176
- `cascade-checked and a clean cascade result does not cover them.`,
1177
- fallbackDetail: (r) => r.indeterminate?.reason === "missing_node"
1178
- ? "changed file not in the review graph"
1179
- : "review graph unavailable",
1180
- }), graphRuns);
1181
- // @delivery-surface: runtime-turn:cascade-coverage-advisory
1182
- if (graphAdvisory)
1183
- advisoryParts.push(graphAdvisory);
1184
- const bindingAdvisory = withCarryLabel(buildAdvisory(bindingRuns, {
1185
- lead: (fileCount, reasons) => `Cascade identified dependents for ${fileCount} edited file(s) this turn, but their ` +
1186
- `diagnostics could not be freshly confirmed (${reasons}) and were withheld — a clean ` +
1187
- `cascade result does not cover them.`,
1188
- fallbackDetail: () => "cascade diagnostics withheld (binding rejected)",
1189
- }), bindingRuns);
1190
- // @delivery-surface: runtime-turn:cascade-coverage-advisory
1191
- if (bindingAdvisory)
1192
- advisoryParts.push(bindingAdvisory);
1193
- const budgetAdvisory = withCarryLabel(buildAdvisory(budgetRuns, {
1194
- lead: (fileCount, reasons) => `Cascade checked the selected neighbors for ${fileCount} edited file(s) this turn, ` +
1195
- `but some eligible dependents were not checked because the cascade budget ` +
1196
- `was exhausted (${reasons}); a clean cascade result does not cover them.`,
1197
- fallbackDetail: (r) => {
1198
- const budget = r.indeterminate?.budget;
1199
- if (!budget)
1200
- return "cascade budget omitted eligible dependents";
1201
- const detail = `cascade budget checked ${budget.selectedCount} of ${budget.eligibleCount} eligible dependents (${budget.truncatedCount} omitted)`;
1202
- return budget.transitiveTruncated
1203
- ? `${detail}; transitive expansion was capped before all eligible dependents were enumerated`
1204
- : detail;
1205
- },
1206
- }), budgetRuns);
1207
- // @delivery-surface: runtime-turn:cascade-coverage-advisory
1208
- if (budgetAdvisory)
1209
- advisoryParts.push(budgetAdvisory);
1210
- const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
1211
- // #1550: attribute each reason to the file that PRODUCED it. This record
1212
- // used to stamp `filePath: files[0] ?? cwd` — the turn's first EDITED file
1213
- // — and a bare `reasons` array with no file association. The two sets are
1214
- // disjoint: a run can be carried across turns (#1443), and an edited file
1215
- // can skip the graph entirely (markdown/JSON return `non_code` before
1216
- // computeImpactCascade ever runs). So the log blamed a file that could not
1217
- // have produced the reason — a markdown file credited with `missing_node`,
1218
- // a non-test source file credited with `excluded_by_role` — and the defect
1219
- // read as "concentrated on test files" only because the first edited file
1220
- // of a turn usually is one. `fileCount` and the agent-facing advisory
1221
- // already keyed off `r.filePath`; only this record's labels did not.
1222
- const byFile = indeterminateRuns.map((r) => ({
1223
- file: toRunnerDisplayPath(cwd, r.filePath),
1224
- reason: r.indeterminate?.reason,
1225
- ...(r.indeterminate?.detail && { detail: r.indeterminate.detail }),
1226
- ...(r.indeterminate?.budget && { budget: r.indeterminate.budget }),
1227
- ...(r.indeterminate?.diagnostic && {
1228
- diagnostic: r.indeterminate.diagnostic,
1229
- }),
1230
- }));
1231
- logCascade({
1232
- phase: "cascade_indeterminate",
1233
- // The first indeterminate run's own file. `files[0] ?? cwd` survives only
1234
- // as a last resort for a run with no path at all.
1235
- filePath: indeterminateRuns[0]?.filePath ?? files[0] ?? cwd,
1236
- metadata: {
1237
- fileCount,
1238
- reasons: indeterminateRuns.map((r) => r.indeterminate?.reason),
1239
- byFile: byFile.slice(0, 20),
1240
- ...(byFile.length > 20 && { byFileTruncated: byFile.length - 20 }),
1241
- },
1242
- });
1243
- }
1244
- // Fix B (#3168 F6): the success-path record — see the counters above.
1245
- if (carriedRunsRendered > 0 || labeledAdvisories > 0) {
1246
- logCascade({
1247
- phase: "cascade_carry_rendered",
1248
- filePath: cascadeLogFilePath,
1249
- metadata: { carriedRunsRendered, labeledAdvisories },
1250
- });
1251
- }
1252
- const cascadeSkipped = {
1253
- blockers: 0,
1254
- non_code: 0,
1255
- no_neighbors: 0,
1256
- clean: 0,
1257
- indeterminate: 0,
1258
- error: 0,
1259
- };
1260
- for (const r of cascadeRuns) {
1261
- if (r.skipReason)
1262
- cascadeSkipped[r.skipReason] = (cascadeSkipped[r.skipReason] ?? 0) + 1;
1263
- }
1264
- logLatency({
1265
- type: "phase",
1266
- toolName: "turn_end",
1267
- filePath: cwd,
1268
- phase: "cascade_merge",
1269
- durationMs: Date.now() - t0,
1270
- metadata: {
1271
- runsTotal: cascadeRuns.length,
1272
- resultCount: cascadeResults.length,
1273
- neighborCount: cascadeRuns.reduce((s, r) => s + r.neighborCount, 0),
1274
- diagnosticCount: cascadeRuns.reduce((s, r) => s + r.diagnosticCount, 0),
1275
- skipped: cascadeSkipped,
1276
- },
1277
- });
1278
- const t2 = Date.now();
1279
- let knipMeta = {};
1280
- if (runtime.isStartupScanInFlight("knip")) {
1281
- dbg("turn_end: skipping knip (startup scan still in flight)");
1282
- knipMeta = { skipped: true };
1283
- }
1284
- else {
1285
- // Let KnipClient resolve/validate a real JS project root before probing or
1286
- // auto-installing knip. Non-JS repos (for example Unity projects) should not
1287
- // run tool checks every turn. Also back off after a timeout/kill so every
1288
- // agent turn does not spend 30s launching another heavyweight knip process.
1289
- const prevKnip = cacheManager.readCache("knip", cwd);
1290
- // An availability failure is NOT a hard knip failure: knip never ran, so
1291
- // there is nothing to back off from, and backing off would make an
1292
- // expiring probe verdict permanent again (#1467).
1293
- const previousFailedHard = prevKnip &&
1294
- !prevKnip.data.success &&
1295
- !prevKnip.data.failureKind &&
1296
- /(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prevKnip.data.summary);
1297
- if (previousFailedHard) {
1298
- dbg(`turn_end: skipping knip after recent failure: ${prevKnip.data.summary}`);
1299
- knipMeta = { skipped: true, reason: prevKnip.data.summary };
1300
- }
1301
- else {
1302
- const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns(), {
1303
- projectSeq: runtime.projectSeq,
1304
- });
1305
- // Never overwrite a good scan with a failure (#925, #1467): the last
1306
- // good result stays until a new successful scan replaces it.
1307
- const knipWouldPoison = wouldPoisonCache(prevKnip, knipResult);
1308
- if (knipWouldPoison) {
1309
- dbg(`turn_end: keeping last good knip cache; this run failed: ${knipResult.summary}`);
1310
- }
1311
- else {
1312
- cacheManager.writeCache("knip", knipResult, cwd);
1313
- }
1314
- knipMeta = {
1315
- execution: knipResult.execution ?? "executed",
1316
- success: knipResult.success,
1317
- totalIssues: knipResult.issues.length,
1318
- newIssues: 0,
1319
- blockerIssues: 0,
1320
- // #3248: bounded per-turn, on the row this lane already writes —
1321
- // never one record per finding.
1322
- dispositionSuppressed: 0,
1323
- ...(!knipResult.success && { reason: knipResult.summary }),
1324
- ...(knipResult.failureKind && { failureKind: knipResult.failureKind }),
1325
- ...(knipWouldPoison && { cacheKept: true }),
1326
- };
1327
- if (knipResult.success && knipResult.issues.length > 0) {
1328
- // Deliberately excludes the line number — see stableFindingKey's
1329
- // doc comment (#1483: mirrors the dead-code fix in #1477).
1330
- const issueKey = (i) => stableFindingKey(i.type, i.file, i.name, i.package);
1331
- const prevKeys = new Set((prevKnip?.data?.issues ?? []).map(issueKey));
1332
- const modifiedSet = new Set(files.map((f) => resolveRunnerPath(cwd, f)));
1333
- const newIssues = knipResult.issues.filter((issue) => {
1334
- if (prevKeys.has(issueKey(issue)))
1335
- return false;
1336
- if (!issue.file)
1337
- return false;
1338
- const abs = resolveRunnerPath(cwd, issue.file);
1339
- return modifiedSet.has(abs);
1340
- });
1341
- knipMeta.newIssues = newIssues.length;
1342
- if (newIssues.length > 0) {
1343
- projectDiagnosticsDelta.push(...knipIssuesToProjectDiagnostics(cwd, newIssues));
1344
- projectDiagnosticsSources.add("knip");
1345
- }
1346
- // #3248: what the agent READS goes through the same stored-
1347
- // disposition filter every other findings surface applies, keyed off
1348
- // knip's OWN `ProjectDiagnostic` adapter — the identity
1349
- // `lens_diagnostics` surfaces and `lens_diagnostic_mark` anchors
1350
- // against — so a marked finding stops re-reporting here. The
1351
- // `projectDiagnosticsDelta` push above deliberately keeps the
1352
- // UNFILTERED set: that record is what the scan found, and its reader
1353
- // (`lens_diagnostics`) applies dispositions on read, so filtering it
1354
- // here would apply the same policy twice on one lane.
1355
- // Paired through `flatMap` rather than indexing the adapter's array:
1356
- // `knipIssuesToProjectDiagnostics` is a straight `issues.map(...)`
1357
- // (one diagnostic per issue, never empty), so this keeps the pairing
1358
- // total while staying honest under `noUncheckedIndexedAccess`.
1359
- const knipPaired = newIssues.flatMap((issue) => knipIssuesToProjectDiagnostics(cwd, [issue]).map((diagnostic) => ({
1360
- issue,
1361
- diagnostic,
1362
- })));
1363
- const knipFiltered = filterFindingsByDisposition(knipPaired, cwd, (pair) => pair.diagnostic);
1364
- const knipDeliverable = {
1365
- kept: knipFiltered.kept.map((pair) => pair.issue),
1366
- suppressed: knipFiltered.suppressed,
1367
- };
1368
- knipMeta.dispositionSuppressed = knipDeliverable.suppressed;
1369
- const blockerIssues = knipDeliverable.kept.filter((i) => i.type === "unlisted" || i.type === "bin");
1370
- knipMeta.blockerIssues = blockerIssues.length;
1371
- if (blockerIssues.length > 0) {
1372
- let report = "🔴 New unresolved imports/deps in modified code (Knip):\n";
1373
- let firstPath = null;
1374
- for (const issue of blockerIssues.slice(0, 5)) {
1375
- const display = issue.file
1376
- ? toRunnerDisplayPath(cwd, issue.file)
1377
- : "(unknown)";
1378
- if (!firstPath && display !== "(unknown)")
1379
- firstPath = display;
1380
- report += ` ${display}${issue.line ? `:${issue.line}` : ""} — ${issue.type}: ${issue.name}\n`;
1381
- }
1382
- if (firstPath) {
1383
- report += ` First location: ${firstPath}\n`;
1384
- }
1385
- // @delivery-surface: runtime-turn:knip-blocker
1386
- blockerParts.push(report);
1387
- }
1388
- // Turn-end injects only this turn's HIGH-CONFIDENCE, ATTRIBUTABLE
1389
- // delta: symbols in files the agent just edited that became unused
1390
- // (weren't flagged in the previous scan) — low-volume and actionable
1391
- // now. The FULL project-wide dead-code picture is deliberately NOT
1392
- // injected per turn (hundreds of mostly-pre-existing findings would
1393
- // drown the blockers and burn context every turn); it's available
1394
- // on demand via lens_diagnostics. The delta also feeds the session-slop
1395
- // record (`projectDiagnosticsDelta`) above.
1396
- const unusedExportDelta = knipDeliverable.kept.filter((i) => i.type === "export" || i.type === "enumMember");
1397
- if (unusedExportDelta.length > 0) {
1398
- let report = "⚠️ Newly unused exports in files you edited — check if callers need updating (Knip):\n";
1399
- for (const issue of unusedExportDelta.slice(0, 5)) {
1400
- const display = issue.file
1401
- ? toRunnerDisplayPath(cwd, issue.file)
1402
- : "(unknown)";
1403
- report += ` ${display}${issue.line ? `:${issue.line}` : ""} — ${issue.name}\n`;
1404
- }
1405
- // @delivery-surface: runtime-turn:knip-advisory
1406
- advisoryParts.push(report);
1407
- }
1408
- }
1409
- }
1410
- }
1411
- logLatency({
1412
- type: "phase",
1413
- toolName: "turn_end",
1414
- filePath: cwd,
1415
- phase: "knip",
1416
- durationMs: Date.now() - t2,
1417
- metadata: knipMeta,
1418
- });
1419
- // Cross-file dead-code (#127) for non-JS/TS languages, on knip's contract:
1420
- // re-scan only when this turn touched a file the client owns, then inject the
1421
- // ATTRIBUTABLE delta — symbols in those files that became unused because of
1422
- // the edit. The project-wide list is deliberately NOT injected per turn (the
1423
- // same reasoning as knip above) and stays available via lens_diagnostics.
1424
- // MUST run before the projectDiagnosticsDelta write below, or a dead-code-only
1425
- // turn would persist nothing.
1426
- const tDeadCode = Date.now();
1427
- const deadCodeMeta = {};
1428
- if (runtime.isStartupScanInFlight("dead-code")) {
1429
- dbg("turn_end: skipping dead-code (startup scan still in flight)");
1430
- deadCodeMeta.skipped = true;
1431
- deadCodeMeta.reason = "startup_scan_in_flight";
1432
- }
1433
- else if (deadCodeClients.length === 0) {
1434
- deadCodeMeta.reason = "no_clients";
1435
- }
1436
- else {
1437
- // The modified-file set costs a resolveRunnerPath per file, and that walks
1438
- // every ancestor to the filesystem root on a miss. Build it lazily, only
1439
- // once a client has actually claimed this project, so an all-JS repo with
1440
- // no dead-code client pays nothing per turn. Knip does the same.
1441
- let modifiedSet = null;
1442
- const modifiedFiles = () => (modifiedSet ??= new Set(files.map((f) => resolveRunnerPath(cwd, f))));
1443
- let newIssueTotal = 0;
1444
- /** #3248: bounded per-turn on this lane's own row, never per finding. */
1445
- let deadCodeDispositionSuppressed = 0;
1446
- const reasons = [];
1447
- // A malformed client or deps object must never abort turn_end. Before the
1448
- // per-turn delta this block only read a cache; now it iterates and awaits,
1449
- // so the whole thing needs the guard, not just `client.analyze`.
1450
- try {
1451
- for (const client of deadCodeClients) {
1452
- if (!client.detect(cwd)) {
1453
- reasons.push(`${client.id}:not_detected`);
1454
- continue;
1455
- }
1456
- if (![...modifiedFiles()].some((f) => client.owns(f))) {
1457
- reasons.push(`${client.id}:no_owned_files`);
1458
- continue;
1459
- }
1460
- const cacheKey = `dead-code-${client.id}`;
1461
- const prev = cacheManager.readCache(cacheKey, cwd);
1462
- // Back off after a timeout/kill so an unresponsive scanner cannot cost
1463
- // every later turn its full analysis budget (mirrors knip).
1464
- if (prev &&
1465
- !prev.data.success &&
1466
- /(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prev.data.summary)) {
1467
- dbg(`turn_end: skipping dead-code after failure: ${prev.data.summary}`);
1468
- deadCodeMeta.skipped = true;
1469
- reasons.push(`${client.id}:backoff:${prev.data.summary}`);
1470
- continue;
1471
- }
1472
- const startMs = Date.now();
1473
- try {
1474
- const result = await client.analyze(cwd);
1475
- const durationMs = Date.now() - startMs;
1476
- // Never overwrite a good scan with a failure (#925, #1467): a
1477
- // vulture timeout on one .py turn would otherwise evict the
1478
- // session_start scan, and the backoff above would then latch
1479
- // off the poisoned record.
1480
- if (wouldPoisonCache(prev, result)) {
1481
- dbg(`turn_end: keeping last good dead-code(${client.id}) cache; this run failed: ${result.summary}`);
1482
- deadCodeMeta.cacheKept = true;
1483
- }
1484
- else {
1485
- cacheManager.writeCache(cacheKey, result, cwd, {
1486
- scanDurationMs: durationMs,
1487
- });
1488
- }
1489
- // One event per cross-file scan (AGENTS.md) — the per-turn scan is
1490
- // now the primary path, so dead-code.log must see it too.
1491
- logDeadCodeScan({
1492
- language: client.language,
1493
- success: result.success,
1494
- cached: false,
1495
- unusedExports: result.unusedExports.length,
1496
- unusedFiles: result.unusedFiles.length,
1497
- unusedDeps: result.unusedDeps.length,
1498
- unlistedDeps: result.unlistedDeps.length,
1499
- durationMs: result.durationMs ?? durationMs,
1500
- ...(!result.success && { reason: result.summary }),
1501
- });
1502
- deadCodeMeta.success = result.success;
1503
- if (!result.success) {
1504
- reasons.push(`${client.id}:scan_failed:${result.summary}`);
1505
- continue;
1506
- }
1507
- deadCodeMeta.totalIssues =
1508
- (deadCodeMeta.totalIssues ?? 0) + deadCodeIssues(result).length;
1509
- // No baseline means every finding looks new. Report nothing rather
1510
- // than blame the edit for the whole project's pre-existing debt.
1511
- if (!prev?.data.success) {
1512
- reasons.push(`${client.id}:no_previous_scan`);
1513
- continue;
1514
- }
1515
- const prevKeys = new Set(deadCodeIssues(prev.data).map(deadCodeIssueKey));
1516
- const modified = modifiedFiles();
1517
- const newIssues = deadCodeIssues(result).filter((issue) => {
1518
- if (prevKeys.has(deadCodeIssueKey(issue)))
1519
- return false;
1520
- if (!issue.file)
1521
- return false;
1522
- return modified.has(resolveRunnerPath(cwd, issue.file));
1523
- });
1524
- if (newIssues.length === 0) {
1525
- reasons.push(`${client.id}:clean`);
1526
- continue;
1527
- }
1528
- newIssueTotal += newIssues.length;
1529
- projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
1530
- projectDiagnosticsSources.add("dead-code");
1531
- // #3248: the rendered advisory takes the stored-disposition
1532
- // filter, keyed off this lane's OWN adapter. The delta record
1533
- // above keeps the unfiltered set — its reader applies the policy
1534
- // on read, so filtering both would double-apply on one lane.
1535
- const deadCodeDeliverable = filterFindingsByDisposition(newIssues, cwd, (issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language));
1536
- deadCodeDispositionSuppressed += deadCodeDeliverable.suppressed;
1537
- // Every finding on this scan was marked: a PUSH surface stays
1538
- // silent rather than re-announcing that the mark is working; the
1539
- // count rides this lane's bounded per-turn row.
1540
- if (deadCodeDeliverable.kept.length === 0) {
1541
- reasons.push(`${client.id}:all_disposed`);
1542
- continue;
1543
- }
1544
- // @delivery-surface: runtime-turn:dead-code-advisory
1545
- advisoryParts.push(formatDeadCodeDelta(deadCodeDeliverable.kept, result.language));
1546
- }
1547
- catch (err) {
1548
- dbg(`turn_end: dead-code(${client.id}) failed: ${err}`);
1549
- reasons.push(`${client.id}:threw`);
1550
- }
1551
- }
1552
- }
1553
- catch (err) {
1554
- dbg(`turn_end: dead-code block failed: ${err}`);
1555
- reasons.push("block_threw");
1556
- }
1557
- deadCodeMeta.newIssues = newIssueTotal;
1558
- deadCodeMeta.dispositionSuppressed = deadCodeDispositionSuppressed;
1559
- if (reasons.length > 0)
1560
- deadCodeMeta.reason = reasons.join(",");
1561
- }
1562
- logLatency({
1563
- type: "phase",
1564
- toolName: "turn_end",
1565
- filePath: cwd,
1566
- phase: "dead-code",
1567
- durationMs: Date.now() - tDeadCode,
1568
- metadata: deadCodeMeta,
1569
- });
1570
- // #1617: running total of findings this turn's advisory/blocker sections
1571
- // dropped because an agent/user marked them false-positive/won't-fix —
1572
- // the #1616 suppressed-bucket rule applied to turn_end's own reporting
1573
- // lanes, so a mark's effect stays visible even though the finding itself
1574
- // no longer appears above. Review-round F4 (#1625): kept per-lane, not
1575
- // just a bare total, so the eventual trace says WHICH lane's marks did
1576
- // the suppressing.
1577
- let dispositionSuppressedTotal = 0;
1578
- const dispositionSuppressedByLane = {};
1579
- function recordDispositionSuppressed(lane, count) {
1580
- if (count <= 0)
1581
- return;
1582
- dispositionSuppressedTotal += count;
1583
- dispositionSuppressedByLane[lane] =
1584
- (dispositionSuppressedByLane[lane] ?? 0) + count;
1585
- }
1586
- // #1892: ONE read per scanner store per turn_end. The inline lanes got this
1587
- // for free by reading each cache into a local; now that a lane reads its own
1588
- // stores (`TurnEndLaneContext.readScannerCache`), the composer and the lanes
1589
- // must still share one envelope per store — two reads of one store inside
1590
- // one delivery is the parallel-store shape this umbrella exists to kill, and
1591
- // the cache's TTL boundary can fall between them, so the secrets tier and
1592
- // the CVE tier would disagree about the trivy store they both read.
1593
- //
1594
- // #3274: the memo holds the PROMISE of each store's envelope, not the
1595
- // envelope. The read itself moved to `CacheManager.readCacheAsync`, which
1596
- // suspends on `fs.promises`, so `bounded()` finally has something to bound:
1597
- // the sync `readCache` completed during ARGUMENT EVALUATION, before
1598
- // `bounded()` was ever handed a promise, and a wrapper around it registered
1599
- // a turn_end budget that could never be spent (probed on #3274 with an
1600
- // already-aborted signal and `ms: 0` — bounded returned undefined and the
1601
- // read had already parsed). Memoizing the promise is also what keeps the
1602
- // one-envelope-per-store rule under concurrency: two lanes that await the
1603
- // same store share ONE read and ONE TTL boundary even when neither has
1604
- // settled yet, which a value memo could not do.
1605
- //
1606
- // An abandoned read yields null, never a throw and never a late envelope:
1607
- // `bounded()` resolves `undefined` on the budget or the hook's signal and
1608
- // records ONE `hook-await-exceeded` row per (hook, label) for the deadline
1609
- // arm, and the memoized promise is already settled by the time the
1610
- // abandoned read resolves, so nothing mutates after the delivery composed.
1611
- // A lane that receives null behaves exactly as it does for a cold cache.
1612
- const scannerCacheReads = new Map();
1613
- // Stores this delivery composed WITHOUT because a bound fired, in read
1614
- // order. `bounded()` records the AWAIT's own row (`hook-await-exceeded`),
1615
- // but only for the deadline arm and only about the await; the agent-facing
1616
- // consequence — the secrets tier went out with no gitleaks store behind it,
1617
- // which looks exactly like a clean scan (AGENTS.md defect shape 10) — has no
1618
- // record otherwise, on either arm. One row per DELIVERY, below, never one
1619
- // per store and never on a healthy turn.
1620
- const scannerStoresUnread = [];
1621
- function readScannerCache(scanner) {
1622
- let pending = scannerCacheReads.get(scanner);
1623
- if (pending === undefined) {
1624
- // An already-aborted turn does not start the read at all. `bounded()`
1625
- // abandons the await but cannot cancel work already dispatched, so
1626
- // without this the cancelled turn still pays six file reads whose
1627
- // result nothing can use. Checked AFTER the memo lookup: a signal that
1628
- // fires mid-delivery must not give the second lane a different answer
1629
- // from the first (that is the TTL-boundary split this memo exists for).
1630
- // It records nothing, on purpose: an already-cancelled turn is Escape,
1631
- // which `bounded()` also keeps off the ledger — the row below is for a
1632
- // delivery that went out degraded, not one the user stopped.
1633
- pending = deps.signal?.aborted
1634
- ? Promise.resolve(null)
1635
- : bounded(cacheManager.readCacheAsync(scanner, cwd), {
1636
- ms: HOOK_WALL_BUDGET_MS.turn_end,
1637
- signal: deps.signal,
1638
- hook: "turn_end",
1639
- label: `readScannerCache:${scanner}`,
1640
- }).then((entry) => {
1641
- if (entry === undefined)
1642
- scannerStoresUnread.push(scanner);
1643
- return entry ?? null;
1644
- });
1645
- scannerCacheReads.set(scanner, pending);
1646
- }
1647
- return pending;
1648
- }
1649
- const laneCtx = {
1650
- cwd,
1651
- signal: deps.signal,
1652
- readScannerCache,
1653
- peekActionableWarnings: () => runtime.peekActionableWarnings(),
1654
- };
1655
- // govulncheck — the session_start-cached Go CVE store, delivered as ONE
1656
- // advisory tier by the govulncheck LANE
1657
- // (`clients/turn-end/lanes/govulncheck.ts`), which owns every rule this block
1658
- // used to state inline: the `onMissing: "demote"` freshness declaration and
1659
- // its first-filename-frame `citedPath` (#1622 H1), the disposition anchor
1660
- // over BOTH freshness arms (#1694 F1), the stale-line withholding and marker,
1661
- // the module/package fallback, the fix hint and the display cap. No per-turn
1662
- // re-run in this slice; the cache refreshes at next session_start. Like every
1663
- // lane it does NOT gate itself — the freshness pass below is shared.
1664
- const govSources = await govulncheckLane.collect(laneCtx);
1665
- const trivyCacheEntry = await readScannerCache("trivy");
1666
- // The secrets lane (`clients/turn-end/lanes/secrets.ts`) reads the gitleaks
1667
- // and trivy stores, classifies, and states the freshness policy its rows
1668
- // need; every rendering and disposition rule for the two secrets tiers lives
1669
- // there. It does NOT gate itself either.
1670
- const secretsSources = await secretsLane.collect(laneCtx);
1671
- // #1892: ONE freshness pass for the three cached scanner stores that cite a
1672
- // file. Each store keeps its own `scannedAt` and its own `onMissing` — the
1673
- // gate carries source identity, so gitleaks' older scan cannot demote a
1674
- // trivy secret its newer scan covers, and govulncheck's `demote` verdict for
1675
- // a deleted path cannot reach gitleaks, which must drop it. What IS shared
1676
- // is the filesystem: one `statSync` per unique cited path per delivery, one
1677
- // stat budget, and one bounded drop/demote record instead of up to six.
1678
- //
1679
- // #1461 slice 1 (#1460) on gitleaks: the cache is TTL-only, so a finding for
1680
- // a file deleted after the scan was served as a 🔴 blocker for the rest of
1681
- // the 30-minute window — 119 of 126 findings in pi-lens's own cache. This
1682
- // read is the single agent-facing consumer of that store (session_start's
1683
- // read only decides whether to re-scan; the project-diagnostics path
1684
- // re-scans fresh and reconciles at load), so the drop belongs here, before
1685
- // the findings enter the shared secret pipeline. #1622 extends the gate from
1686
- // existence to freshness, and adds trivy secrets — the sibling store with
1687
- // the identical shape. A cited file edited after the scan keeps its finding
1688
- // but loses its line number: the credential may still be there, just not
1689
- // where the snapshot says. Dropping instead would let any edit — malicious
1690
- // or accidental — mute a real secret.
1691
- if (scannerStoresUnread.length > 0) {
1692
- logLatency({
1693
- type: "phase",
1694
- toolName: "turn_end",
1695
- filePath: cwd,
1696
- phase: "scanner_cache_read_abandoned",
1697
- durationMs: 0,
1698
- metadata: {
1699
- stores: scannerStoresUnread.join("+"),
1700
- aborted: deps.signal?.aborted === true,
1701
- },
1702
- });
1703
- }
1704
- const scannerGates = gateFindingsByPathFreshness({
1705
- cwd,
1706
- sources: {
1707
- ...govSources,
1708
- ...secretsSources,
1709
- },
1710
- });
1711
- const govGate = scannerGates.govulncheck;
1712
- const gitleaksGate = scannerGates.gitleaks;
1713
- const trivySecretsGate = scannerGates["trivy-secrets"];
1714
- const govDelivery = govulncheckLane.render(govulncheckLane.gate({ govulncheck: govGate }, laneCtx), laneCtx);
1715
- for (const [store, count] of Object.entries(govDelivery.dispositionSuppressed ?? {})) {
1716
- recordDispositionSuppressed(store, count);
1717
- }
1718
- // @delivery-surface: runtime-turn:govulncheck-advisory
1719
- advisoryParts.push(...(govDelivery.advisoryParts ?? []));
1720
- // Secrets — UNIFIED surfacing (#131 Mode 3). gitleaks, trivy secret, and the
1721
- // ast-grep hardcoded-secret rules can each flag the SAME line with different
1722
- // rule ids, which the rule-keyed diagnostic dedup can't collapse. Collapse by
1723
- // location so a committed/hardcoded secret is reported ONCE (with combined
1724
- // provenance) — a blocker, since credentials need rotation before merge.
1725
- //
1726
- // #1892: both tiers are rendered by the secrets LANE
1727
- // (`clients/turn-end/lanes/secrets.ts`), which owns every rule this block
1728
- // used to state inline — the two stores' disposition anchors over BOTH
1729
- // freshness arms (#1617/#1625/#1628), the location dedupe and ast-grep
1730
- // provenance enrichment, the demoted tier's file+rule+source identity
1731
- // (#1622 M1) and its own-tier placement (#1622 M2). The composer keeps only
1732
- // what is not one lane's rule: the gated arms it hands over, the order the
1733
- // tiers are pushed in, and the per-lane suppression counts that fold into
1734
- // the one notice below.
1735
- const secretsDelivery = secretsLane.render(secretsLane.gate({
1736
- gitleaks: gitleaksGate,
1737
- "trivy-secrets": trivySecretsGate,
1738
- }, laneCtx), laneCtx);
1739
- for (const [store, count] of Object.entries(secretsDelivery.dispositionSuppressed ?? {})) {
1740
- recordDispositionSuppressed(store, count);
1741
- }
1742
- const secretBlockedLocations = secretsDelivery.deliveredLocationKeys ?? new Set();
1743
- // @delivery-surface: runtime-turn:secrets-gitleaks,runtime-turn:secrets-trivy
1744
- blockerParts.push(...(secretsDelivery.blockerParts ?? []));
1745
- // @delivery-surface: runtime-turn:stale-secrets-tier
1746
- staleSecretParts.push(...(secretsDelivery.staleSecretParts ?? []));
1747
- // trivy — surface session_start-cached dependency CVEs (#131, Phase 1).
1748
- // CRITICAL is a blocker (a known-exploitable CVE in a shipped dep is real
1749
- // production risk); HIGH/MEDIUM/LOW are advisory. The agent gets the upgrade
1750
- // target as a hint and decides — we never auto-edit lockfiles.
1751
- //
1752
- // #1634: these three trivy reports (critical blocker, non-critical
1753
- // advisory, license advisory below) name a PACKAGE, not a file:line — there
1754
- // is no cited path for `gateFindingsByPathFreshness` to stat, so unlike the
1755
- // secrets/govulncheck stores above this store cannot be freshness-GATED.
1756
- // It is the delivery gate's explicit-label escape hatch instead
1757
- // (`clients/finding-delivery-gate.ts`, surfaces `runtime-turn:trivy-*`):
1758
- // the session_start cache can be arbitrarily old, so its age is stated
1759
- // plainly rather than presenting a CRITICAL blocker as if it were current.
1760
- // This runs on top of (not instead of) #1625's disposition filter below —
1761
- // a suppressed finding never reaches this render at all, so the two only
1762
- // ever compose.
1763
- const trivyAgeLabel = formatCacheAgeLabel(trivyCacheEntry?.data?.scannedAt);
1764
- const trivyFindingsFiltered = filterFindingsByDisposition(trivyCacheEntry?.data?.findings ?? [], cwd, (f) => trivyFindingToProjectDiagnostic(cwd, f));
1765
- recordDispositionSuppressed("trivy", trivyFindingsFiltered.suppressed);
1766
- if (trivyFindingsFiltered.kept.length) {
1767
- const all = trivyFindingsFiltered.kept;
1768
- const critical = all.filter((f) => f.severity === "CRITICAL");
1769
- const advisory = all.filter((f) => f.severity !== "CRITICAL");
1770
- const fmt = (f) => {
1771
- const pkg = f.installedVersion
1772
- ? `${f.pkgName}@${f.installedVersion}`
1773
- : f.pkgName;
1774
- const fix = f.fixedVersion
1775
- ? ` — upgrade to ${f.fixedVersion} or later`
1776
- : " — no fix yet, track upstream";
1777
- return ` ${f.vulnerabilityId} (${pkg})${fix}\n`;
1778
- };
1779
- if (critical.length) {
1780
- const shown = critical.slice(0, 5);
1781
- let report = `🔴 STOP — CRITICAL dependency CVEs (trivy, ${trivyAgeLabel}). Upgrade before shipping:\n`;
1782
- for (const f of shown)
1783
- report += fmt(f);
1784
- if (critical.length > shown.length) {
1785
- report += ` … and ${critical.length - shown.length} more\n`;
1786
- }
1787
- // @delivery-surface: runtime-turn:trivy-critical-blocker
1788
- blockerParts.push(report);
1789
- }
1790
- if (advisory.length) {
1791
- const shown = advisory.slice(0, 5);
1792
- let report = `🛡️ Dependency CVEs (trivy, ${trivyAgeLabel}) — upgrade where possible:\n`;
1793
- for (const f of shown)
1794
- report += fmt(f);
1795
- if (advisory.length > shown.length) {
1796
- report += ` … and ${advisory.length - shown.length} more\n`;
1797
- }
1798
- // @delivery-surface: runtime-turn:trivy-cve-advisory
1799
- advisoryParts.push(report);
1800
- }
1801
- }
1802
- // trivy — dependency license risk (#131 Mode 4). Advisory only: a copyleft /
1803
- // restricted license in a proprietary tree is a compliance signal, not a
1804
- // build break. Surfaced from the same cached `trivy fs` pass — same #1634
1805
- // explicit-label rationale as the CVE reports above (no cited path to gate).
1806
- const licenses = trivyCacheEntry?.data?.licenses ?? [];
1807
- if (licenses.length) {
1808
- const shown = licenses.slice(0, 5);
1809
- let report = `📜 Dependency license risk (trivy, ${trivyAgeLabel}) — review for compliance:\n`;
1810
- for (const l of shown) {
1811
- const cat = l.category ? `, ${l.category}` : "";
1812
- report += ` ${l.pkgName} — ${l.license} (${l.severity}${cat})\n`;
1813
- }
1814
- if (licenses.length > shown.length) {
1815
- report += ` … and ${licenses.length - shown.length} more\n`;
1816
- }
1817
- // @delivery-surface: runtime-turn:trivy-license-advisory
1818
- advisoryParts.push(report);
1819
- }
1820
- // #1616 suppressed-bucket rule: surface the running disposition-drop total
1821
- // as its own advisory line so a mark's effect is visible, not a silent
1822
- // absence — trace, not a vanish.
1823
- if (dispositionSuppressedTotal > 0) {
1824
- // Review-round F4 (#1625): per-lane attribution, e.g.
1825
- // "gitleaks 2, govulncheck 1" — not just a bare total.
1826
- const byLane = Object.entries(dispositionSuppressedByLane)
1827
- .map(([lane, count]) => `${lane} ${count}`)
1828
- .join(", ");
1829
- // @delivery-surface: runtime-turn:disposition-suppressed-notice
1830
- advisoryParts.push(`suppressed by disposition: ${dispositionSuppressedTotal} finding(s) ` +
1831
- `dropped from this turn's gitleaks/govulncheck/trivy sections (${byLane}) ` +
1832
- "(marked false-positive or won't-fix).");
1833
- }
1834
- const t3 = Date.now();
1835
- let madgeStats;
1836
- // Off by default (#766): this pass only writes debug output, and user-facing
1837
- // madge diagnostics come from the session-start `madge` cache + the
1838
- // `lens_diagnostics` extractor. Enabled with `--lens-turn-end-madge` /
1839
- // `turnEnd.madge.enabled=true` for those who want the per-edit circular note.
1840
- if (getFlag("lens-turn-end-madge") && (await depChecker.ensureAvailable())) {
1841
- const madgeFiles = cacheManager.getFilesForMadge(cwd);
1842
- if (madgeFiles.length > 0) {
1843
- dbg(`turn_end: madge checking ${madgeFiles.length} file(s) for circular deps`);
1844
- // Checked concurrently (bounded) rather than one `await` per file —
1845
- // the shared circular-dep state update is deferred/folded inside
1846
- // checkFilesBatch so concurrent spawns can't clobber each other (#766).
1847
- const absFiles = madgeFiles.map((file) => path.resolve(cwd, file));
1848
- const batch = await depChecker.checkFilesBatch(absFiles, cwd);
1849
- const depResults = batch.results;
1850
- madgeStats = batch.stats;
1851
- for (const file of madgeFiles) {
1852
- const absPath = path.resolve(cwd, file);
1853
- const depResult = depResults.get(absPath);
1854
- if (!depResult)
1855
- continue;
1856
- if (depResult.hasCircular && depResult.circular.length > 0) {
1857
- // Whole-project circular deps are surfaced in lens_diagnostics via the
1858
- // session-start `madge` cache + extractor; this per-file turn-end pass
1859
- // only logs (blockers-only mode suppresses circular-dep notes).
1860
- dbg(`turn_end: circular dependency note for ${file} (suppressed in blockers-only mode)`);
1861
- }
1862
- }
1863
- }
1864
- }
1865
- logLatency({
1866
- type: "phase",
1867
- toolName: "turn_end",
1868
- filePath: cwd,
1869
- phase: "madge",
1870
- durationMs: Date.now() - t3,
1871
- // A ~0ms entry with no metadata is indistinguishable from "ran and was
1872
- // fast" when re-analyzing the #766 tail — mark the skipped case.
1873
- metadata: madgeStats ?? { skipped: true },
1874
- });
1875
- // --- Test runner: fire once per turn after all edits are done ---
1876
- // Runs for each unique test target across modified files; results remain in
1877
- // the pull-diagnostics cache and are delivered after the agent settles.
1878
- if (!getFlag("no-tests") && files.length > 0) {
1879
- const seen = new Set();
1880
- // `strategy` is widened by one value the resolver itself cannot produce:
1881
- // a target carried over from the previous turn's cut batch (#2522 R2 F1).
1882
- const targets = [];
1883
- // #628: also target the test companions of this turn's cascade neighbors
1884
- // (files that import an edited file) — a neighbor's own tests can break
1885
- // even though the neighbor's source wasn't touched. Reuses `cascadeResults`,
1886
- // already computed above (from the same #450 deferred-cascade drain) for the
1887
- // LSP cascade-diagnostics merge — no second reverse-dependency walk, and the
1888
- // neighbor set inherits whatever budget the cascade compute already applied
1889
- // (CASCADE_NEIGHBOUR_BUDGET), so this can't turn into unbounded per-edit work.
1890
- const candidates = [];
1891
- const seenCandidateKeys = new Set();
1892
- for (const file of files) {
1893
- const abs = resolveRunnerPath(cwd, file);
1894
- const key = normalizeMapKey(abs);
1895
- if (seenCandidateKeys.has(key))
1896
- continue;
1897
- seenCandidateKeys.add(key);
1898
- candidates.push({ display: file, abs, isNeighbor: false });
1899
- }
1900
- for (const result of cascadeResults) {
1901
- for (const neighbor of result.neighbors) {
1902
- const abs = path.isAbsolute(neighbor.filePath)
1903
- ? neighbor.filePath
1904
- : resolveRunnerPath(cwd, neighbor.filePath);
1905
- const key = normalizeMapKey(abs);
1906
- if (seenCandidateKeys.has(key))
1907
- continue;
1908
- seenCandidateKeys.add(key);
1909
- candidates.push({ display: neighbor.filePath, abs, isNeighbor: true });
1910
- }
1911
- }
1912
- // #2522 review round 2, F1: the previous turn's unfinished targets go
1913
- // FIRST — ahead of failed-first/related/self — under the same
1914
- // TEST_RUNNER_MAX_TARGETS cap, so a batch that keeps getting cut makes
1915
- // progress instead of re-running whatever this turn happened to touch
1916
- // and dropping the remainder again. The list is consumed here (written
1917
- // back empty below) so a target can never be carried forever.
1918
- const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
1919
- const carriedDeferred = priorTestCache?.deferredTargets ?? [];
1920
- // #2522 review round 3, F5: a deferral list belongs to the session that
1921
- // cut it. `runtime.telemetrySessionId` is the same identity the batch
1922
- // stamps its own entries with below (`firedSessionId`), so this is a
1923
- // single comparison against one source of truth.
1924
- const turnSessionId = sessionId ?? runtime.telemetrySessionId;
1925
- const isThisSession = (entry) => entry.sessionId === turnSessionId;
1926
- const entryPathKey = (entry) => normalizeMapKey(path.resolve(cwd, entry.testFile));
1927
- /**
1928
- * #2522 review round 3, F1: the retirement, carried forward.
1929
- *
1930
- * Round 2 announced the retirement and dropped it on the floor, so the
1931
- * candidate loop below re-resolved the very same file through
1932
- * `related`/`self` in the SAME turn and ran it anyway with a fresh
1933
- * (undefined) attempt count. Steady state was a 3-turn cycle that spawned
1934
- * and cut the slow suite on every turn. The retired set is persisted on
1935
- * the record instead.
1936
- *
1937
- * Round 4, I1: this is a SELECTION filter and nothing more. Round 3 also
1938
- * wrote the filtered list straight back, so a turn taken by the other
1939
- * entry point into the same project — `clients/mcp/session.ts` passes no
1940
- * `sessionId` and falls back to `telemetrySessionId`, while `index.ts`
1941
- * passes pi's stable id — erased the entries belonging to the other
1942
- * route. Both lists survive every write now; see `writeTestFindings`.
1943
- */
1944
- const retiredCarry = (priorTestCache?.retiredTargets ?? []).filter(isThisSession);
1945
- const retiredKeys = new Set(retiredCarry.map(entryPathKey));
1946
- /**
1947
- * Deferral entries of THIS session that this turn has settled: dispatched
1948
- * into `targets`, retired at the cap, or dropped as unrunnable. Only
1949
- * these may leave the persisted list, and only when this turn's own write
1950
- * is not re-asserting them (a cut target comes straight back with
1951
- * `attempts + 1`).
1952
- */
1953
- const resolvedDeferralKeys = new Set();
1954
- /**
1955
- * #2522 review round 4, I5: entries this turn could not fit under
1956
- * `TEST_RUNNER_MAX_TARGETS`. They were not run, so they keep their attempt
1957
- * count and stay on the list — round 3 folded the over-cap case in with
1958
- * "no longer resolves" and dropped them outright, so a carry larger than
1959
- * the cap lost its tail every turn under a dbg line that said the files
1960
- * were gone.
1961
- */
1962
- const heldDeferred = [];
1963
- /**
1964
- * The ONE writer of `test-runner-findings` in this block, so the rules
1965
- * about what survives a write are stated once instead of at six call sites
1966
- * that a seventh would silently not join (#2522 review round 3, F1; same
1967
- * net-count rule as `retireTestFindings` in `runtime-context.ts`).
1968
- *
1969
- * Three rules, all of them round-4 invariants:
1970
- * - I1: the LIVE record is re-read here and merged into, never replaced.
1971
- * A row belonging to another session is untouchable — this turn may
1972
- * ignore it, never delete it.
1973
- * - I2: every write merges. A batch that publishes clean, a batch that
1974
- * publishes failures and a superseded batch all go through this, so
1975
- * none of them can overwrite a concurrent batch's cut set. Removal is
1976
- * narrow by construction: an entry leaves only if it is this session's,
1977
- * this turn settled it, and this write is not re-asserting it.
1978
- * - the persisted lists are bounded (`TEST_RUNNER_MAX_PERSISTED_TARGETS`)
1979
- * with this session's rows ordered LAST, so the bound sheds foreign
1980
- * rows first and can never evict what this turn just recorded.
1981
- */
1982
- const boundPersisted = (entries, label) => {
1983
- const ordered = [
1984
- ...entries.filter((entry) => !isThisSession(entry)),
1985
- ...entries.filter(isThisSession),
1986
- ];
1987
- if (ordered.length <= TEST_RUNNER_MAX_PERSISTED_TARGETS)
1988
- return ordered;
1989
- dbg(`turn_end: ${label} test target list held ${ordered.length} entries, bounded to the newest ${TEST_RUNNER_MAX_PERSISTED_TARGETS} (oldest foreign-session rows dropped)`);
1990
- return ordered.slice(-TEST_RUNNER_MAX_PERSISTED_TARGETS);
1991
- };
1992
- const writeTestFindings = (record, ownDeferred) => {
1993
- const live = cacheManager.readCache("test-runner-findings", cwd)?.data;
1994
- const own = mergeDeferredTargets(heldDeferred, ownDeferred);
1995
- const ownKeys = new Set(own.map(entryPathKey));
1996
- const merged = mergeDeferredTargets(live?.deferredTargets ?? [], own).filter((entry) => {
1997
- if (!isThisSession(entry))
1998
- return true;
1999
- const key = entryPathKey(entry);
2000
- return !resolvedDeferralKeys.has(key) || ownKeys.has(key);
2001
- });
2002
- cacheManager.writeCache("test-runner-findings", {
2003
- ...record,
2004
- deferredTargets: boundPersisted(merged, "deferred"),
2005
- retiredTargets: boundPersisted(mergeDeferredTargets(live?.retiredTargets ?? [], retiredCarry), "retired"),
2006
- }, cwd);
2007
- };
2008
- let deadDeferred = 0;
2009
- let heldOverCap = 0;
2010
- let foreignDeferred = 0;
2011
- let redispatchedDeferred = 0;
2012
- let retiredThisTurn = 0;
2013
- for (const carried of carriedDeferred) {
2014
- const testFile = path.resolve(cwd, carried.testFile);
2015
- const carriedKey = normalizeMapKey(testFile);
2016
- // I1: another session still owes this run. Skip it, leave it on the
2017
- // record — `writeTestFindings` never removes a row this turn does not
2018
- // own.
2019
- if (!isThisSession(carried)) {
2020
- foreignDeferred++;
2021
- continue;
2022
- }
2023
- if (seen.has(carriedKey)) {
2024
- resolvedDeferralKeys.add(carriedKey);
2025
- continue;
2026
- }
2027
- const attempts = carried.attempts ?? 0;
2028
- if (attempts >= TEST_RUNNER_MAX_DEFERRALS) {
2029
- resolvedDeferralKeys.add(carriedKey);
2030
- if (!retiredKeys.has(carriedKey)) {
2031
- retiredKeys.add(carriedKey);
2032
- retiredThisTurn++;
2033
- retiredCarry.push({
2034
- testFile,
2035
- runner: carried.runner,
2036
- attempts,
2037
- sessionId: turnSessionId,
2038
- });
2039
- // Never silent, and counted rather than once-per-subject: this is
2040
- // the ledger entry that names WHICH suite is too slow to belong in
2041
- // a per-turn batch at all.
2042
- incrementDegradationCount({
2043
- kind: "test-runner-batch-capped",
2044
- subject: `${cwd}:deferral-exhausted`,
2045
- reason: `test target ${path.relative(cwd, testFile)} was cut at the turn-end batch budget ${attempts} turn(s) running and is retired from turn-end selection for the rest of this session — too slow for a per-turn batch, run it explicitly`,
2046
- });
2047
- dbg(`turn_end: retiring deferred test target ${path.relative(cwd, testFile)} after ${attempts} cut batch(es) — too slow for the turn-end budget, run it explicitly`);
2048
- }
2049
- continue;
2050
- }
2051
- // A RunnerConfig carries functions, so the cache stores the runner KEY
2052
- // and the config is re-resolved here from the single registry.
2053
- const config = RUNNERS[carried.runner];
2054
- if (!config ||
2055
- isExcludedTestTarget(testFile, cwd) ||
2056
- !fs.existsSync(testFile)) {
2057
- resolvedDeferralKeys.add(carriedKey);
2058
- deadDeferred++;
2059
- continue;
2060
- }
2061
- if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
2062
- // I5: not settled — held, with `attempts` UNCHANGED. It was never
2063
- // dispatched, so charging it toward retirement would retire a suite
2064
- // this turn simply had no room for.
2065
- heldOverCap++;
2066
- heldDeferred.push({
2067
- testFile,
2068
- runner: carried.runner,
2069
- sourceFile: carried.sourceFile ?? testFile,
2070
- attempts,
2071
- sessionId: turnSessionId,
2072
- });
2073
- continue;
2074
- }
2075
- resolvedDeferralKeys.add(carriedKey);
2076
- seen.add(carriedKey);
2077
- redispatchedDeferred++;
2078
- targets.push({
2079
- testFile,
2080
- sourceFile: carried.sourceFile ?? testFile,
2081
- runner: carried.runner,
2082
- config,
2083
- strategy: "deferred",
2084
- deferralAttempts: attempts,
2085
- });
2086
- dbg(`turn_end: re-running deferred test target ${path.relative(cwd, testFile)} (${carried.runner}) from the previous turn's cut batch`);
2087
- }
2088
- if (deadDeferred > 0) {
2089
- dbg(`turn_end: dropped ${deadDeferred} deferred test target(s) that no longer resolve (missing file, excluded, or unknown runner)`);
2090
- }
2091
- if (heldOverCap > 0) {
2092
- dbg(`turn_end: held ${heldOverCap} deferred test target(s) over the per-turn cap of ${TEST_RUNNER_MAX_TARGETS} — kept on the deferral list for the next turn, attempts unchanged`);
2093
- }
2094
- if (foreignDeferred > 0) {
2095
- dbg(`turn_end: ignored ${foreignDeferred} deferred test target(s) belonging to another session — left on the record for the session that cut them`);
2096
- }
2097
- if (carriedDeferred.length > 0) {
2098
- // #2522 review round 4, S1: the ONE pushed record for this path. The
2099
- // MCP / Stop-hook route calls `handleTurnEnd` with `dbg: noop`, so every
2100
- // line above is invisible there and the deferral machinery — the part
2101
- // that decides whether a suite runs at all — left no trace whatsoever
2102
- // on the route that fires it most.
2103
- logLatency({
2104
- type: "phase",
2105
- toolName: "turn_end",
2106
- filePath: cwd,
2107
- phase: "test_runner_deferral",
2108
- durationMs: 0,
2109
- metadata: {
2110
- carried: carriedDeferred.length,
2111
- redispatched: redispatchedDeferred,
2112
- heldOverCap,
2113
- retired: retiredThisTurn,
2114
- dropped: deadDeferred,
2115
- foreignSession: foreignDeferred,
2116
- sessionId: turnSessionId,
2117
- },
2118
- });
2119
- }
2120
- let overCapTargets = 0;
2121
- let missingTargetFiles = 0;
2122
- let excludedTargets = 0;
2123
- let retiredSkips = 0;
2124
- for (const { display, abs, isNeighbor } of candidates) {
2125
- const target = testRunnerClient.getTestRunTarget(abs, cwd, runtime.turnIndex);
2126
- const targetKey = target ? normalizeMapKey(target.testFile) : "";
2127
- if (target && !seen.has(targetKey)) {
2128
- seen.add(targetKey);
2129
- // #2522 review round 3, F1: THE gate that makes the deferral cap a
2130
- // cap. Whichever strategy produced this target — related, self,
2131
- // failed-first — a target already retired this session for
2132
- // outrunning the whole batch budget is not fired again. Without
2133
- // this the retire branch above was decorative: it logged, and the
2134
- // candidate loop three lines down re-resolved the same file and
2135
- // spawned it anyway, with its attempt counter reset to zero.
2136
- if (retiredKeys.has(targetKey)) {
2137
- retiredSkips++;
2138
- dbg(`turn_end: ${display} → test target retired earlier this session (outran the turn-end batch budget), skipping spawn (${path.relative(cwd, target.testFile)})`);
2139
- continue;
2140
- }
2141
- // #2522: built-in exclusion for turn-end SELECTION — a resolved
2142
- // target under an integration/e2e directory or naming convention
2143
- // is never auto-fired, whichever strategy (failed-first/related/
2144
- // self) produced it. No per-project config knob (maintainer
2145
- // decision); see `TURN_END_EXCLUDED_TEST_GLOBS`.
2146
- // LATENT HAZARD, deliberately left as-is: this `continue` skips the
2147
- // candidate entirely, including `retireMissingFailedTargets`. If an
2148
- // excluded target were ever seeded into the persisted failed set
2149
- // (it cannot be today — nothing writes that set except a runner
2150
- // RESULT, and an excluded target is never run, so it can never
2151
- // produce one), it would sit there unretired forever, chosen by
2152
- // the failed-first strategy on every turn and dropped here on
2153
- // every turn. Any future writer of the failed set must retire
2154
- // excluded entries at the write site, not here.
2155
- if (isExcludedTestTarget(target.testFile, cwd)) {
2156
- excludedTargets++;
2157
- dbg(`turn_end: ${display} → test target excluded (integration/e2e), skipping spawn (${path.relative(cwd, target.testFile)})`);
2158
- continue;
2159
- }
2160
- // #2504: a conventional target that no longer exists on disk still
2161
- // cost a full runner spawn, which came back "Test file not found"
2162
- // and was then dropped as an expected skip further down. 9 of the
2163
- // reported turn's 59 spawns were this. One statSync is cheaper
2164
- // than a vitest process by four orders of magnitude.
2165
- if (!fs.existsSync(target.testFile)) {
2166
- missingTargetFiles++;
2167
- dbg(`turn_end: ${display} → test file missing, skipping spawn (${path.relative(cwd, target.testFile)})`);
2168
- continue;
2169
- }
2170
- if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
2171
- overCapTargets++;
2172
- continue;
2173
- }
2174
- targets.push({ ...target, sourceFile: abs });
2175
- dbg(`turn_end: ${display} → test ${target.runner} ${path.relative(cwd, target.testFile)} (${target.strategy}${isNeighbor ? ", cascade-neighbor" : ""})`);
2176
- }
2177
- else if (!target) {
2178
- dbg(`turn_end: ${display} → no test file found${isNeighbor ? " (cascade-neighbor)" : ""}`);
2179
- }
2180
- }
2181
- if (excludedTargets > 0) {
2182
- dbg(`turn_end: excluded ${excludedTargets} test target(s) under the built-in integration/e2e exclusion list`);
2183
- }
2184
- if (retiredSkips > 0) {
2185
- dbg(`turn_end: skipped ${retiredSkips} test target(s) retired earlier this session for outrunning the turn-end batch budget`);
2186
- }
2187
- if (missingTargetFiles > 0) {
2188
- dbg(`turn_end: skipped ${missingTargetFiles} test target(s) whose file no longer exists`);
2189
- }
2190
- if (overCapTargets > 0) {
2191
- // Never silent: the agent is told that some of this turn's tests were
2192
- // not run, rather than reading an all-green batch that covered part
2193
- // of the edit set.
2194
- recordDegradationOnce({
2195
- kind: "test-runner-batch-capped",
2196
- subject: cwd,
2197
- reason: `turn touched more test targets than one turn may fire; ran ${TEST_RUNNER_MAX_TARGETS}, skipped ${overCapTargets} — re-run the remainder with lens_diagnostics or edit them in a smaller batch`,
2198
- });
2199
- dbg(`turn_end: test target count capped at ${TEST_RUNNER_MAX_TARGETS}, ${overCapTargets} skipped`);
2200
- }
2201
- if (targets.length > 0) {
2202
- for (const target of targets) {
2203
- target.fileSeqAtRun = runtime.getFileSeq(target.sourceFile);
2204
- }
2205
- dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking, max ${TEST_RUNNER_BATCH_CONCURRENCY} concurrent)`);
2206
- const firedAtTurn = runtime.turnIndex;
2207
- const firedSessionId = turnSessionId;
2208
- const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
2209
- const provenanceFiles = [
2210
- ...candidates.map((candidate) => ({
2211
- path: candidate.abs,
2212
- role: "source",
2213
- })),
2214
- ...targets.map((target) => ({
2215
- path: target.testFile,
2216
- role: "test",
2217
- })),
2218
- ];
2219
- const launchedFrom = snapshotAdvisoryProvenance({
2220
- cwd,
2221
- runtime,
2222
- generation: testRunGeneration,
2223
- files: provenanceFiles,
2224
- });
2225
- writeTestFindings({
2226
- ...(priorTestCache ?? { content: "" }),
2227
- testRunGeneration,
2228
- },
2229
- // Consumed above into `targets` (or retired, or dropped). Anything
2230
- // this turn HELD is re-asserted by the writer itself; the batch's own
2231
- // outcome adds the NEW cut set below.
2232
- []);
2233
- runTestTargetsBounded({
2234
- targets,
2235
- concurrency: TEST_RUNNER_BATCH_CONCURRENCY,
2236
- budgetMs: TEST_RUNNER_BATCH_BUDGET_MS,
2237
- // Both bounds, per AGENTS.md: a wall budget AND the ambient
2238
- // abort signal the rest of the spawn layer already honours.
2239
- signal: getAmbientAbortSignal(),
2240
- run: (t, batchSignal) => testRunnerClient.runTestFileAsync(t.testFile, cwd, {
2241
- runner: t.runner,
2242
- config: t.config,
2243
- turnIndex: firedAtTurn,
2244
- // #2522 R2 F1: the BATCH's signal, so spending the 20s wall
2245
- // budget kills this spawn rather than letting it run out its
2246
- // own 60s timeout behind a batch that has already returned.
2247
- signal: batchSignal,
2248
- }),
2249
- })
2250
- .then(({ results, deferred, stopReason }) => {
2251
- const settledResults = results;
2252
- const verdicts = settledResults.flatMap((result) => {
2253
- if (result.status === "rejected")
2254
- return [];
2255
- const target = targets.find((candidate) => candidate.testFile === result.value.file);
2256
- return target
2257
- ? [
2258
- {
2259
- file: result.value.file,
2260
- sourceFile: target.sourceFile,
2261
- fileSeq: target.fileSeqAtRun === undefined
2262
- ? {
2263
- state: "unknown",
2264
- reason: "sequence-unavailable",
2265
- }
2266
- : {
2267
- state: "known",
2268
- value: target.fileSeqAtRun,
2269
- },
2270
- },
2271
- ]
2272
- : [];
2273
- });
2274
- const deferredTargets = deferred.map((t) => ({
2275
- testFile: t.testFile,
2276
- sourceFile: t.sourceFile,
2277
- runner: t.runner,
2278
- // One more cut batch for this target. Read back by the
2279
- // selection loop above, which retires it at
2280
- // TEST_RUNNER_MAX_DEFERRALS rather than carrying it forever.
2281
- attempts: (t.deferralAttempts ?? 0) + 1,
2282
- // #2522 R3 F5: stamped with the session that cut it, so the
2283
- // next session re-arms this target instead of inheriting an
2284
- // attempt count measured under a load it never saw.
2285
- sessionId: firedSessionId,
2286
- }));
2287
- if (deferred.length > 0) {
2288
- // #2522 review round 2, F4: `incrementDegradationCount`, not
2289
- // `recordDegradationOnce`. At a 20s budget this is a
2290
- // RECURRING event, and a once-per-subject entry reports the
2291
- // tenth cut batch of a session exactly like the first.
2292
- incrementDegradationCount({
2293
- kind: "test-runner-batch-capped",
2294
- subject: `${cwd}:${stopReason ?? "incomplete"}`,
2295
- reason: `test batch stopped early (${stopReason ?? "incomplete"}); ${deferred.length} target(s) killed or never dispatched, deferred to the next turn`,
2296
- });
2297
- dbg(`turn_end: test batch stopped early (${stopReason ?? "incomplete"}), ${deferred.length} target(s) deferred to the next turn`);
2298
- }
2299
- const deferralNote = deferred.length > 0
2300
- ? `${deferred.length} test target(s) did not finish within the turn-end batch budget and are deferred to the next turn (they run first): ${deferredTargets
2301
- .map((t) => path.relative(cwd, t.testFile))
2302
- .join(", ")}`
2303
- : "";
2304
- /**
2305
- * A newer batch has already published for this project — this
2306
- * one's results are stale by construction and must not overwrite
2307
- * it. Was inlined three times; extracted once round 2 added a
2308
- * fourth call site.
2309
- */
2310
- const supersededByNewerGeneration = (label) => {
2311
- const current = cacheManager.readCache("test-runner-findings", cwd)?.data;
2312
- const currentGeneration = current?.testRunGeneration;
2313
- if (currentGeneration !== undefined &&
2314
- currentGeneration > testRunGeneration) {
2315
- dbg(`turn_end: ${label}test generation ${testRunGeneration} superseded by ${currentGeneration}`);
2316
- // #2522 review round 3, F3: returning here USED to drop this
2317
- // batch's cut targets entirely. The newer batch's own pre-run
2318
- // write already cleared them, and it never saw this set — so
2319
- // with two overlapping batches the targets the older one was
2320
- // cut on were never deferred, never re-selected, and the suite
2321
- // they belong to simply never ran again. Hand them over
2322
- // instead. Round 4, I2: the merge is `writeTestFindings`'s own
2323
- // rule now, not a second copy of it here — every branch below
2324
- // merges the same way, so a NEWER batch publishing after this
2325
- // one no longer flattens what this hand-over just recorded.
2326
- if (deferredTargets.length > 0) {
2327
- writeTestFindings({ ...(current ?? { content: "" }) }, deferredTargets);
2328
- dbg(`turn_end: ${label}carried ${deferredTargets.length} cut target(s) into the newer generation's deferral set`);
2329
- }
2330
- return true;
2331
- }
2332
- return false;
2333
- };
2334
- const publishedAgainst = snapshotAdvisoryProvenance({
2335
- cwd,
2336
- runtime,
2337
- generation: testRunGeneration,
2338
- files: provenanceFiles,
2339
- });
2340
- const superseded = launchedFrom.revision.sessionId !==
2341
- publishedAgainst.revision.sessionId ||
2342
- launchedFrom.revision.projectSeq !==
2343
- publishedAgainst.revision.projectSeq ||
2344
- launchedFrom.revision.turnIndex !==
2345
- publishedAgainst.revision.turnIndex ||
2346
- launchedFrom.files.some((file, index) => publishedAgainst.files[index]?.sha256 !== file.sha256 ||
2347
- publishedAgainst.files[index]?.path !== file.path);
2348
- // #628: the turn advancing while tests ran no longer means the
2349
- // results are thrown away — a late result is still real
2350
- // information about what's currently broken. It's tagged `stale`
2351
- // so a downstream consumer can distinguish it from a result that
2352
- // arrived in time, but it's cached either way.
2353
- const stale = runtime.turnIndex !== firedAtTurn;
2354
- const failures = [];
2355
- const resultValues = [];
2356
- let rejectedCount = 0;
2357
- // #2522: whether ANY reported item is a genuine failing test
2358
- // (`failed > 0`) as opposed to the runner itself never
2359
- // completing (timeout, missing provider/binary, a rejected
2360
- // promise). Drives the delivery framing below — a batch made
2361
- // up ENTIRELY of runner errors is not something the agent
2362
- // introduced, so it must not read as "fix before continuing".
2363
- let hasRealFailure = false;
2364
- for (const r of results) {
2365
- if (r.status === "rejected") {
2366
- rejectedCount++;
2367
- emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:rejected`, {
2368
- filePath: cwd,
2369
- durationMs: 0,
2370
- metadata: {
2371
- outcome: "runner-promise-rejected",
2372
- sessionId: firedSessionId,
2373
- generation: testRunGeneration,
2374
- targetCount: targets.length,
2375
- droppedDetailCount: 0,
2376
- reason: String(r.reason).slice(0, 500),
2377
- },
2378
- }, {
2379
- ledgerKind: "test-runner-delivery",
2380
- reason: "test runner promise rejected",
2381
- capPerTurn: { limit: 8, turnIndex: firedAtTurn },
2382
- });
2383
- dbg(`turn_end: test run rejected — ${r.reason}`);
2384
- continue;
2385
- }
2386
- resultValues.push(r.value);
2387
- const { file, runner, passed, failed, duration, error } = r.value;
2388
- if (failed > 0)
2389
- hasRealFailure = true;
2390
- const shortFile = path.basename(file);
2391
- // #1479: `(0ms)` used to be printed for a run nobody
2392
- // timed — a payload with no suite timestamps, an
2393
- // unrecognised summary line, or an empty result — and
2394
- // that is the same string a genuinely sub-millisecond
2395
- // run produces. A reader could not tell "measured 0"
2396
- // from "not measured", which is the confusion #1452 was
2397
- // reported for. `duration` is now absent when it was
2398
- // never measured, and this line says which one it has.
2399
- //
2400
- // #1480: the test is `formatRunDurationMs`, not an
2401
- // inline comparison. The "absent = unmeasured" contract
2402
- // was being re-derived at every site that read a
2403
- // duration, and a site that gets it slightly wrong —
2404
- // treating a measured `0` as absent — puts the bug back
2405
- // without touching this comment.
2406
- const elapsed = formatRunDurationMs(duration);
2407
- // Lifted out of the template below for the same reason
2408
- // `elapsed` is: the pair read as a nested ternary, which
2409
- // this line only got flagged for because #1479 touched it.
2410
- const verdict = failed > 0 ? "FAIL" : "PASS";
2411
- // #2532 review S1: folded onto `isRunnerErrorResult` instead of
2412
- // re-deriving `error && passed === 0 && failed === 0` here — that
2413
- // local spelling missed a runner error reported alongside partial
2414
- // passes (pytest `Interrupted` after some tests already passed),
2415
- // which read as a clean "PASS Np/0f" line with the error silently
2416
- // dropped. A partial run says so explicitly rather than reading
2417
- // as a clean pass.
2418
- const summary = isRunnerErrorResult(r.value)
2419
- ? passed > 0
2420
- ? `error: ${error} (${passed} passed before)`
2421
- : `error: ${error}`
2422
- : `${verdict} ${passed}p/${failed}f (${elapsed})`;
2423
- dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} → ${summary}`);
2424
- // #1524: also fires on `error` alone, not just `failed > 0`.
2425
- // A runner error (the suite never started, or was
2426
- // interrupted before finishing — spawn/config/timeout
2427
- // failure) can arrive with `failed === 0` even when tests
2428
- // DID pass before it (#2532 review S2 — not "by
2429
- // construction": `parsePytestOutput` sets `error` from the
2430
- // exit code independently of the parsed counts, so
2431
- // `isRunnerErrorResult` is `failed === 0 && !!error`, not an
2432
- // invariant elsewhere). Gating on `failed > 0` alone dropped
2433
- // it silently: the agent got no context at all, and the
2434
- // empty `failures` array below sent this result down the
2435
- // "all tests passed" branch, clearing any prior real
2436
- // test-failure git-guard blocker. `formatResult` already
2437
- // renders the error-only case as "Could not run tests: ...".
2438
- if (failed > 0 || error) {
2439
- // #2028: "Test file not found" is an expected skip
2440
- // (conventional test path without an actual file),
2441
- // not an actionable failure. Don't surface it.
2442
- if (error &&
2443
- String(r.value?.error ?? "").includes("Test file not found")) {
2444
- continue;
2445
- }
2446
- const formatted = testRunnerClient.formatResult(r.value);
2447
- if (formatted)
2448
- failures.push(formatted);
2449
- }
2450
- }
2451
- if (rejectedCount > 0) {
2452
- failures.push(`Test runner rejected ${rejectedCount} promise(s) before producing a structured result.`);
2453
- }
2454
- if (failures.length > 0) {
2455
- if (supersededByNewerGeneration(""))
2456
- return;
2457
- const content = [
2458
- stale
2459
- ? "[from a prior turn — the edit that triggered this run had already been superseded by the time results came back]"
2460
- : "",
2461
- failures.join("\n\n"),
2462
- // A batch can BOTH find a real failure and be cut short.
2463
- // The agent needs to see the failure AND that the run was
2464
- // incomplete, or it reads a partial list as the full one.
2465
- deferralNote,
2466
- ]
2467
- .filter(Boolean)
2468
- .join("\n\n");
2469
- // #2522: nothing in this batch is a genuine failing test —
2470
- // every entry is a runner error (timeout, missing
2471
- // provider/binary, a rejected promise). `peekTestFindings`
2472
- // reads this to deliver advisory framing instead of
2473
- // "fix before continuing", since the agent introduced
2474
- // nothing here to fix.
2475
- const runnerErrorOnly = !hasRealFailure;
2476
- writeTestFindings({
2477
- content,
2478
- stale,
2479
- results: resultValues,
2480
- verdicts,
2481
- testRunGeneration,
2482
- launchedFrom,
2483
- publishedAgainst,
2484
- provenance: publishedAgainst,
2485
- superseded,
2486
- runnerErrorOnly,
2487
- }, deferredTargets);
2488
- try {
2489
- deps.onTestRunnerComplete?.({
2490
- cwd,
2491
- sessionId: firedSessionId,
2492
- generation: testRunGeneration,
2493
- targetCount: targets.length,
2494
- hasFindings: true,
2495
- });
2496
- }
2497
- catch (deliveryErr) {
2498
- dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
2499
- }
2500
- if (getFlag("lens-guard") &&
2501
- firedSessionId === runtime.telemetrySessionId) {
2502
- // #1524: `&& !value.error` — a runner-error result has
2503
- // `failed === 0` (the suite never ran, so nothing could
2504
- // fail), but it is not a pass. Without the filter it
2505
- // would clear a prior real test-failure git-guard
2506
- // blocker on the strength of a suite that never
2507
- // started. And the call itself is skipped when this
2508
- // list is empty rather than passed as `[]`:
2509
- // `clearGitGuardTestFailure`'s own empty-array
2510
- // fallback treats "no files named" as "clear every
2511
- // blocked file", so an all-error batch (one go file,
2512
- // runner-error, zero clean files) would otherwise
2513
- // clear every blocker through that fallback instead
2514
- // of clearing none.
2515
- const cleanFiles = resultValues
2516
- .filter((value) => value.failed === 0 && !value.error)
2517
- .map((value) => value.file);
2518
- if (cleanFiles.length > 0) {
2519
- clearGitGuardTestFailure(cacheManager, cwd, runtime, cleanFiles);
2520
- }
2521
- // #2532: `runnerErrorOnly` (computed above for the turn-end
2522
- // delivery framing) is true exactly when nothing in this
2523
- // batch is a genuine failing test — every entry is a
2524
- // runner error the agent did not introduce. Without this
2525
- // gate, an all-runner-error batch still called
2526
- // `mergeGitGuardTestFailure` with an EMPTY failed-files
2527
- // list, which unconditionally sets `hasBlockers: true`:
2528
- // the identical event the turn-end message reports as
2529
- // advisory read as "COMMIT BLOCKED" under --lens-guard.
2530
- if (!runnerErrorOnly) {
2531
- mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, resultValues
2532
- .filter((value) => value.failed > 0)
2533
- .map((value) => value.file));
2534
- }
2535
- else {
2536
- // #2532 review T2: the skip above is otherwise pull-only —
2537
- // nothing records that a batch with real content
2538
- // (`failures.length > 0`) was deliberately kept OFF the
2539
- // `--lens-guard` blocker because every entry was a runner
2540
- // error. Same phase/ledger as the rejected-promise event
2541
- // above, so both `--lens-guard` demotions land in one
2542
- // queryable place. Reached only inside the enclosing
2543
- // `getFlag("lens-guard") && firedSessionId === …` check —
2544
- // no point recording a demotion the flag can't act on.
2545
- emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:runner-error-only`, {
2546
- filePath: cwd,
2547
- durationMs: 0,
2548
- metadata: {
2549
- outcome: "runner-error-only-not-blocking",
2550
- sessionId: firedSessionId,
2551
- generation: testRunGeneration,
2552
- targetCount: targets.length,
2553
- droppedDetailCount: 0,
2554
- },
2555
- }, {
2556
- ledgerKind: "test-runner-delivery",
2557
- reason: "runner-error-only batch kept off the --lens-guard blocker",
2558
- capPerTurn: { limit: 8, turnIndex: firedAtTurn },
2559
- });
2560
- }
2561
- }
2562
- dbg(`turn_end: ${failures.length} test failure(s) cached for pull diagnostics and post-agent delivery${stale ? " (stale — turn advanced while tests ran)" : ""}`);
2563
- }
2564
- else if (deferred.length > 0) {
2565
- // #2522 review round 2, F1: a PARTIAL batch is not a clean
2566
- // batch. Pre-round-2 this fell through to the branch below —
2567
- // `content: ""`, "all tests passed", and a `--lens-guard`
2568
- // clear — on the strength of a run that never finished. The
2569
- // record names the deferral instead; the commit gate is left
2570
- // exactly as it was, because nothing here proves anything
2571
- // passed that was not already proven.
2572
- if (supersededByNewerGeneration("deferred "))
2573
- return;
2574
- writeTestFindings({
2575
- content: deferralNote,
2576
- stale,
2577
- results: resultValues,
2578
- verdicts,
2579
- testRunGeneration,
2580
- launchedFrom,
2581
- publishedAgainst,
2582
- provenance: publishedAgainst,
2583
- superseded,
2584
- // Advisory framing: an unfinished batch is not a
2585
- // failure the agent introduced, exactly like a runner
2586
- // error. See `TestRunnerFindingsCache.runnerErrorOnly`.
2587
- runnerErrorOnly: true,
2588
- }, deferredTargets);
2589
- try {
2590
- deps.onTestRunnerComplete?.({
2591
- cwd,
2592
- sessionId: firedSessionId,
2593
- generation: testRunGeneration,
2594
- targetCount: targets.length,
2595
- hasFindings: true,
2596
- });
2597
- }
2598
- catch (deliveryErr) {
2599
- dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
2600
- }
2601
- dbg(`turn_end: partial test batch — ${results.length} target(s) settled, ${deferred.length} deferred to the next turn; NOT recorded as a clean run`);
2602
- }
2603
- else if (results.length > 0) {
2604
- if (supersededByNewerGeneration("clean "))
2605
- return;
2606
- writeTestFindings({
2607
- ...(priorTestCache ?? { content: "" }),
2608
- content: "",
2609
- stale: false,
2610
- results: resultValues,
2611
- testRunGeneration,
2612
- launchedFrom,
2613
- publishedAgainst,
2614
- provenance: publishedAgainst,
2615
- superseded,
2616
- },
2617
- // Nothing was cut. This turn's own settled entries drop out;
2618
- // I2 keeps every row this batch does not own — including a
2619
- // concurrent older batch's hand-over.
2620
- []);
2621
- try {
2622
- deps.onTestRunnerComplete?.({
2623
- cwd,
2624
- sessionId: firedSessionId,
2625
- generation: testRunGeneration,
2626
- targetCount: targets.length,
2627
- hasFindings: false,
2628
- });
2629
- }
2630
- catch (deliveryErr) {
2631
- dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
2632
- }
2633
- if (getFlag("lens-guard") &&
2634
- firedSessionId === runtime.telemetrySessionId) {
2635
- clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues.map((value) => value.file));
2636
- }
2637
- dbg(`turn_end: all tests passed${stale ? " (stale — turn advanced while tests ran)" : ""}`);
2638
- }
2639
- })
2640
- .catch(() => { });
2641
- }
2642
- else if (carriedDeferred.length > 0) {
2643
- // Nothing fired, so no batch outcome will write the list back, but this
2644
- // turn still settled part of it (targets retired at the cap, or dropped
2645
- // because their file is gone). Persist that here rather than re-reading
2646
- // a set of dead paths on every future turn; `writeTestFindings` carries
2647
- // this turn's retirements alongside, which is what the deferral-cap
2648
- // branch above depends on when it retires the LAST carried target and
2649
- // fires nothing. Rows this turn only HELD (over the cap) or does not
2650
- // own (another session's) are re-asserted by the writer itself.
2651
- writeTestFindings({ ...(priorTestCache ?? { content: "" }) }, []);
2652
- }
2653
- }
2654
- if (runtime.errorDebtBaseline && files.length > 0) {
2655
- dbg("turn_end: marking error debt check for next session");
2656
- cacheManager.writeCache("errorDebt", {
2657
- pendingCheck: true,
2658
- baselineTestsPassed: runtime.errorDebtBaseline.testsPassed,
2659
- }, cwd);
2660
- }
2661
- // Session summaries are intentionally suppressed at turn_end to avoid
2662
- // distracting the agent with non-blocking telemetry.
2663
- // Call-graph impact analysis — surface WillBreak/MayBreak callers for modified
2664
- // symbols. MUST run BEFORE the writeProjectDiagnosticsDeltaReport serialization
2665
- // below: it is a delta contributor (like knip above), pushing into
2666
- // projectDiagnosticsDelta / projectDiagnosticsSources. If it ran after the
2667
- // single write, a call-graph-only turn would persist nothing and a mixed turn
2668
- // would drop the call-graph entries — so lens_diagnostics (which only reads the
2669
- // persisted report) would never surface the findings (#179/#533).
2670
- if (runtime.callGraph && files.length > 0) {
2671
- const coverage = runtime.callGraph.coverage;
2672
- if (!coverage || coverage.complete !== true) {
2673
- // An incomplete graph can still contain useful edges, but emitting them
2674
- // as ordinary impact findings would turn unsupported/partial extraction
2675
- // into an authoritative-looking clean result for the rest of the file.
2676
- // Keep the limitation visible and require a complete graph for this
2677
- // user-facing impact surface (#1070).
2678
- // @delivery-surface: runtime-turn:call-graph-advisory
2679
- advisoryParts.push("Call-graph impact was not emitted because call-graph extraction coverage is incomplete; " +
2680
- "the affected files may have unreported callers.");
2681
- }
2682
- else {
2683
- const callGraphStart = Date.now();
2684
- try {
2685
- const { impact, formatImpact, parseSymbolKey } = await import("./call-graph.js");
2686
- const { callGraphImpactToProjectDiagnostics } = await import("./project-diagnostics/runner-adapters/call-graph-impact.js");
2687
- const impactLines = [];
2688
- /** #3248: bounded per-turn, never per finding. */
2689
- let callGraphDispositionSuppressed = 0;
2690
- const impactFindings = [];
2691
- for (const filePath of files.slice(0, 5)) {
2692
- // Turn-state files may be cwd-relative while graph keys are absolute,
2693
- // and persisted graphs can contain either slash style/casing. Compare
2694
- // through the shared normalized path seam; keep the original filePath
2695
- // only for display and diagnostics.
2696
- const changedFileKey = normalizeMapKey(resolveRunnerPath(cwd, filePath));
2697
- const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => {
2698
- const graphFilePath = parseSymbolKey(k).filePath;
2699
- return (normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) ===
2700
- changedFileKey);
2701
- });
2702
- for (const calleeKey of fileCallerKeys.slice(0, 3)) {
2703
- // #1080: drop KNOWN test-role callers BEFORE both the human advisory
2704
- // (formatImpact below) and the persisted delta (impactFindings →
2705
- // callGraphImpactToProjectDiagnostics) — the advisory is rendered
2706
- // first, so the filter must reach the shared `results` set that feeds
2707
- // both. A test caller supplied by an old/fixture/expanded graph must
2708
- // appear in neither surface. Fail-open: an unparseable/unclassifiable
2709
- // key is retained (the adapter re-applies the same predicate).
2710
- const results = impact(runtime.callGraph, calleeKey).filter((r) => {
2711
- const callerFile = parseSymbolKey(r.symbolKey).filePath;
2712
- return (!callerFile ||
2713
- !isTestRoleCollateral(resolveRunnerPath(cwd, callerFile)));
2714
- });
2715
- if (results.length > 0) {
2716
- impactFindings.push({ calleeKey, results });
2717
- // #3248: the rendered line takes the stored-disposition
2718
- // filter. A result is mapped to its diagnostic by the
2719
- // lane's OWN adapter, ONE result at a time, so the
2720
- // identity can never diverge from the one
2721
- // `lens_diagnostics` surfaces and a mark anchors against
2722
- // — and a result the adapter does not map (the Review
2723
- // tier, an unattributable key, a test-role caller) is
2724
- // kept unconditionally: it is not markable, so nothing
2725
- // may suppress it. Re-rendered with `formatImpact`, the
2726
- // same renderer, never a second one.
2727
- // `flatMap` rather than a push-under-an-`if`: an entry the
2728
- // adapter does not map simply yields nothing, so the array
2729
- // is well-typed with no branch whose removal changes no
2730
- // behaviour (there is nothing here to mutate).
2731
- const markable = results.flatMap((result) => callGraphImpactToProjectDiagnostics(cwd, [
2732
- { calleeKey, results: [result] },
2733
- ]).map((diagnostic) => ({ result, diagnostic })));
2734
- const filtered = filterFindingsByDisposition(markable, cwd, (entry) => entry.diagnostic);
2735
- callGraphDispositionSuppressed += filtered.suppressed;
2736
- const keptEntries = new Set(filtered.kept);
2737
- const dropped = new Set(markable
2738
- .filter((entry) => !keptEntries.has(entry))
2739
- .map((entry) => entry.result));
2740
- const survivors = dropped.size === 0
2741
- ? results
2742
- : results.filter((r) => !dropped.has(r));
2743
- const summary = survivors.length > 0 ? formatImpact(survivors, cwd) : "";
2744
- if (summary)
2745
- impactLines.push(` ${parseSymbolKey(calleeKey).symbolName ?? calleeKey}: ${summary}`);
2746
- }
2747
- }
2748
- }
2749
- if (impactLines.length > 0) {
2750
- // @delivery-surface: runtime-turn:call-graph-advisory
2751
- advisoryParts.push(`📊 Call-graph impact (changed symbols have callers):\n${impactLines.join("\n")}`);
2752
- }
2753
- if (impactFindings.length > 0) {
2754
- const impactDiagnostics = callGraphImpactToProjectDiagnostics(cwd, impactFindings);
2755
- if (impactDiagnostics.length > 0) {
2756
- projectDiagnosticsDelta.push(...impactDiagnostics);
2757
- projectDiagnosticsSources.add("call-graph");
2758
- }
2759
- }
2760
- // #3248: this lane wrote nothing per turn, so its delivery decision
2761
- // — including how many callers a stored disposition dropped — was
2762
- // unobservable. One bounded row per turn, only when the lane ran.
2763
- if (impactFindings.length > 0 || callGraphDispositionSuppressed > 0) {
2764
- logLatency({
2765
- type: "phase",
2766
- toolName: "turn_end",
2767
- filePath: cwd,
2768
- phase: "call_graph_impact",
2769
- durationMs: Date.now() - callGraphStart,
2770
- metadata: {
2771
- callees: impactFindings.length,
2772
- lines: impactLines.length,
2773
- dispositionSuppressed: callGraphDispositionSuppressed,
2774
- },
2775
- });
2776
- }
2777
- // Non-fatal — call graph is best-effort
2778
- }
2779
- catch {
2780
- // Non-fatal — call graph is best-effort
2781
- }
2782
- }
2783
- }
2784
- if (projectDiagnosticsDelta.length > 0) {
2785
- writeProjectDiagnosticsDeltaReport(cwd, {
2786
- version: PROJECT_DIAGNOSTICS_CACHE_VERSION,
2787
- cwd,
2788
- generatedAt: new Date().toISOString(),
2789
- sessionId: runtime.telemetrySessionId,
2790
- turnIndex: runtime.turnIndex,
2791
- projectSeqStart: runtime.turnStartProjectSeq,
2792
- projectSeqEnd: runtime.projectSeq,
2793
- diagnostics: projectDiagnosticsDelta,
2794
- sources: [...projectDiagnosticsSources].sort((a, b) => a.localeCompare(b)),
2795
- });
2796
- }
2797
- const t4 = Date.now();
2798
- const modifiedRangesByFile = new Map(Object.entries(turnState.files).map(([file, state]) => [
2799
- normalizeMapKey(resolveRunnerPath(cwd, file)),
2800
- state.modifiedRanges,
2801
- ]));
2802
- const getFileSeq = runtime.getFileSeq;
2803
- const fileSeqByPath = new Map();
2804
- if (getFileSeq) {
2805
- for (const file of files) {
2806
- const filePath = normalizeMapKey(resolveRunnerPath(cwd, file));
2807
- fileSeqByPath.set(filePath, getFileSeq.call(runtime, filePath));
2808
- }
2809
- }
2810
- let reverify;
2811
- if (getFlag("lens-actionable-warnings")) {
2812
- // #3170: re-verify carried deferred findings before the advisory
2813
- // assembles — a finding whose file is unchanged re-serves from the
2814
- // persisted report without ever being re-observed; root-cause-fixed-
2815
- // elsewhere findings converge here instead of repeating. Bounded in the
2816
- // module (≤4 files, wall budget, abort signal). #3176 F1: the
2817
- // replacement entries fold into THIS turn's single in-band publish
2818
- // below — a separate replacement publish spends the carry marker and
2819
- // this publish then drops the entries at the scope guard (the blocker
2820
- // the review caught).
2821
- const persistedReport = cacheManager.readCache("actionable-warnings", cwd, Number.MAX_SAFE_INTEGER)?.data;
2822
- if (persistedReport?.files?.some((entry) => entry.origin === "deferred")) {
2823
- const reverifyLspService = getLSPService();
2824
- if (reverifyLspService) {
2825
- // #2523: the hook-path await is bound-wrapped with the hook's own
2826
- // budget and signal; the pass's internal deadline (3s) is the
2827
- // tighter of the two.
2828
- reverify = await bounded(runPersistentReverify({
2829
- report: persistedReport,
2830
- cwd,
2831
- lspService: reverifyLspService,
2832
- signal: getAmbientAbortSignal(),
2833
- }), {
2834
- ms: HOOK_WALL_BUDGET_MS.turn_end,
2835
- signal: getAmbientAbortSignal(),
2836
- hook: "turn_end",
2837
- label: "persistent_reverify",
2838
- });
2839
- }
2840
- }
2841
- try {
2842
- const report = await buildActionableWarningsReport({
2843
- cwd,
2844
- sessionId: runtime.telemetrySessionId,
2845
- turnIndex: runtime.turnIndex,
2846
- files,
2847
- modifiedRangesByFile,
2848
- dispatchWarnings: runtime.peekActionableWarnings(),
2849
- includeLspCodeActions: !!getFlag("lens-actionable-warning-actions"),
2850
- projectSeqStart: runtime.turnStartProjectSeq,
2851
- projectSeqEnd: runtime.projectSeq,
2852
- fileSeqByPath,
2853
- deltaOnly: !getFlag("lens-actionable-warning-all"),
2854
- dbg,
2855
- // #2504: this call is AWAITED on the turn_end hook, so its cost is
2856
- // terminal-blocking time. The file cap and wall budget are its
2857
- // bounds; the abort signal is the second one AGENTS.md requires.
2858
- signal: getAmbientAbortSignal(),
2859
- // A cold-cache turn hands the fresh-pull loop back here, off the
2860
- // hook — it lands in the same cache the in-band report goes to.
2861
- onDeferredReport: (deferred) => {
2862
- try {
2863
- // #2504 review round 2 (F2): GUARDED. This callback fires up
2864
- // to a minute after the turn that armed it, and the report
2865
- // it carries is stamped with THAT turn's
2866
- // turnIndex/projectSeq. Writing it unconditionally
2867
- // overwrote a newer report, which `agent_end` then rejected
2868
- // as `project_seq_mismatch` (silently skipping the autofix
2869
- // pass) and `lens_diagnostics` re-served as a stale delta.
2870
- // #2504 review round 4 (F1): guarded PER FILE, not per
2871
- // report. The round-3 shape (publish, or discard whole
2872
- // when something newer is persisted) composed with
2873
- // incumbent-wins into "publish nothing" — every turn_end
2874
- // with modified files persists an in-band report with a
2875
- // strictly increasing turnIndex, and the decline fires
2876
- // exactly when such a turn runs while a loop is in
2877
- // flight, so a decline always implied a supersede. The
2878
- // merge upserts the entries whose file has not moved and
2879
- // drops only those that have.
2880
- writeDeferredActionableWarningsReport({
2881
- cacheManager,
2882
- cwd,
2883
- report: deferred,
2884
- // The LIVE per-file sequence at WRITE time is the
2885
- // baseline the merge judges each entry against. The
2886
- // runtime is the only thing that knows it: a file
2887
- // edited into cleanliness by a later turn is absent
2888
- // from the persisted report entirely.
2889
- getFileSeq: getFileSeq
2890
- ? (filePath) => getFileSeq.call(runtime, filePath)
2891
- : undefined,
2892
- dbg,
2893
- });
2894
- }
2895
- catch (deferErr) {
2896
- dbg(`turn_end: deferred actionable-warnings write failed — ${deferErr}`);
2897
- }
2898
- },
2899
- });
2900
- // #2504 review round 5 (F1): THE publish choke point. This was a
2901
- // blind `writeActionableWarningsReport` while the deferred callback
2902
- // above read-modify-wrote the SAME cache key. The deferred merge can
2903
- // land anywhere inside this handleTurnEnd -- the cascade settle,
2904
- // knip, madge, the test batch, the in-band LSP enrichment are all
2905
- // awaited between the moment it is armed and the moment we get
2906
- // here -- and the blind write erased it 607 ms later in the
2907
- // reviewer's trace. The publisher now always reads what is persisted
2908
- // and merges per file, so the two writers cannot race. It carries
2909
- // forward only entries a DEFERRAL produced, and only while their
2910
- // file has not moved, so a `turn_delta` report does not accumulate
2911
- // every prior turn's findings.
2912
- // #3176 F1: the re-verify replacements fold into THIS report — one
2913
- // in-band publish total, so the carry marker is spent exactly once and
2914
- // the merged report cannot drop the entries behind the publish (the
2915
- // blocker the review caught).
2916
- if (reverify?.replacementFiles.length) {
2917
- const replacementByPath = new Map(reverify.replacementFiles.map((file) => [
2918
- normalizeMapKey(file.filePath),
2919
- file,
2920
- ]));
2921
- const files = (report.files ?? []).map((file) => replacementByPath.get(normalizeMapKey(file.filePath)) ?? file);
2922
- for (const replacement of reverify.replacementFiles) {
2923
- if (!files.some((file) => normalizeMapKey(file.filePath) ===
2924
- normalizeMapKey(replacement.filePath))) {
2925
- files.push(replacement);
2926
- }
2927
- }
2928
- report.files = files;
2929
- }
2930
- const publishResult = publishActionableWarningsReport(cacheManager, cwd, report, {
2931
- origin: "in-band",
2932
- getFileSeq: getFileSeq
2933
- ? (filePath) => getFileSeq.call(runtime, filePath)
2934
- : undefined,
2935
- dbg,
2936
- });
2937
- // #2504 review round 6 (a): the advisory must read the MERGED report,
2938
- // not the pre-merge `report` this turn assembled -- a rescued deferred
2939
- // entry lives only on `publishResult.report`, and formatting the
2940
- // pre-merge report silently dropped it from the turn_end advisory
2941
- // even though it was correctly persisted to cache.
2942
- // #2521: `cwd` is what resolves the store location the advisory
2943
- // names -- it is the SAME cwd `publishActionableWarningsReport`
2944
- // just wrote through, so the two cannot disagree.
2945
- const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd, host, (report) => {
2946
- let dispositionSuppressed = 0;
2947
- const files = report.files
2948
- .map((file) => {
2949
- const policy = filterFindingsByDisposition(file.warnings, cwd, (warning) => {
2950
- const { line, column, rule, code } = warning;
2951
- return {
2952
- filePath: warning.filePath,
2953
- severity: warning.severity,
2954
- semantic: "warning",
2955
- tool: warning.tool,
2956
- runner: warning.tool,
2957
- message: warning.message,
2958
- source: warning.origin === "lsp" ? "lsp" : "dispatch",
2959
- ...(line === undefined ? {} : { line: warning.line }),
2960
- ...(column === undefined ? {} : { column: warning.column }),
2961
- ...(rule === undefined ? {} : { rule: warning.rule }),
2962
- ...(code === undefined ? {} : { code }),
2963
- };
2964
- });
2965
- dispositionSuppressed += policy.suppressed;
2966
- const kept = policy.kept.filter((warning) => !(isSecretWarning(warning) &&
2967
- typeof warning.line === "number" &&
2968
- secretBlockedLocations.has(secretLocationKey(warning.filePath, warning.line))));
2969
- return kept.length > 0 ? { ...file, warnings: kept } : undefined;
2970
- })
2971
- .filter((file) => file !== undefined);
2972
- recordDispositionSuppressed("actionable-warnings", dispositionSuppressed);
2973
- return { files, suppressed: dispositionSuppressed };
2974
- });
2975
- // @delivery-surface: runtime-turn:actionable-warnings-advisory
2976
- if (advisory)
2977
- advisoryParts.push(advisory);
2978
- logActionableWarningsEvent({
2979
- event: advisory ? "advisory_injected" : "advisory_skipped",
2980
- sessionId: runtime.telemetrySessionId,
2981
- metadata: {
2982
- turnIndex: runtime.turnIndex,
2983
- // #2504 review round 7 (F2): the MERGED report, matching the
2984
- // advisory text above it (round 6, a) -- a rescued deferred
2985
- // entry lives only on `publishResult.report`, and the pre-merge
2986
- // `report` this turn assembled undercounts it. This is the value
2987
- // scripts/analyze-pi-lens-logs.mjs sums, so an undercount here is
2988
- // a silent miscount there too.
2989
- unsuppressed: publishResult.report.summary.unsuppressed,
2990
- },
2991
- });
2992
- logLatency({
2993
- type: "phase",
2994
- toolName: "turn_end",
2995
- filePath: cwd,
2996
- phase: "actionable_warnings_report",
2997
- durationMs: Date.now() - t4,
2998
- metadata: report.summary,
2999
- });
3000
- }
3001
- catch (err) {
3002
- dbg(`turn_end: actionable warning report failed: ${err}`);
3003
- logLatency({
3004
- type: "phase",
3005
- toolName: "turn_end",
3006
- filePath: cwd,
3007
- phase: "actionable_warnings_report",
3008
- durationMs: Date.now() - t4,
3009
- metadata: {
3010
- failed: true,
3011
- error: err instanceof Error ? err.message : String(err),
3012
- },
3013
- });
3014
- }
3015
- }
3016
- const t5 = Date.now();
3017
- try {
3018
- // #3248: the code-quality records were filtered at DISPATCH time, before
3019
- // any `lens_diagnostic_mark`. `lens_diagnostics mode=delta` re-applies
3020
- // dispositions when it re-serves this same cache
3021
- // (`tools/lens-diagnostics.ts`'s `visibleWarningFiles`), so without this
3022
- // the turn-end advisory counted warnings the delta view had already
3023
- // dropped — the two surfaces disagreed about the same records. Filtered
3024
- // at the report INPUT so the advisory, the persisted report and the delta
3025
- // view all agree; the delta view's own filter then re-applies to the same
3026
- // set and is idempotent. Per file, because the anchor is content-bound.
3027
- const qualityWarningsByFile = new Map();
3028
- for (const warning of runtime.peekCodeQualityWarnings()) {
3029
- const group = qualityWarningsByFile.get(warning.filePath);
3030
- if (group)
3031
- group.push(warning);
3032
- else
3033
- qualityWarningsByFile.set(warning.filePath, [warning]);
3034
- }
3035
- const qualityWarnings = [];
3036
- let qualityDispositionSuppressed = 0;
3037
- for (const [filePath, group] of qualityWarningsByFile) {
3038
- let content;
3039
- try {
3040
- content = fs.readFileSync(filePath, "utf-8");
3041
- }
3042
- catch {
3043
- content = undefined;
3044
- }
3045
- const { kept, suppressed } = applyPushedFindingPolicy(group, {
3046
- cwd,
3047
- filePath,
3048
- content,
3049
- });
3050
- qualityWarnings.push(...kept);
3051
- qualityDispositionSuppressed += suppressed;
3052
- }
3053
- const qualityReport = buildCodeQualityWarningsReport({
3054
- cwd,
3055
- sessionId: runtime.telemetrySessionId,
3056
- turnIndex: runtime.turnIndex,
3057
- warnings: qualityWarnings,
3058
- modifiedRangesByFile,
3059
- projectSeqStart: runtime.turnStartProjectSeq,
3060
- projectSeqEnd: runtime.projectSeq,
3061
- fileSeqByPath,
3062
- });
3063
- writeCodeQualityWarningsReport(cacheManager, cwd, qualityReport);
3064
- appendCodeQualityWarningsHistory(cwd, qualityReport);
3065
- const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd, host);
3066
- // @delivery-surface: runtime-turn:code-quality-warnings-advisory
3067
- if (advisory)
3068
- advisoryParts.push(advisory);
3069
- logLatency({
3070
- type: "phase",
3071
- toolName: "turn_end",
3072
- filePath: cwd,
3073
- phase: "code_quality_warnings_report",
3074
- durationMs: Date.now() - t5,
3075
- metadata: {
3076
- ...qualityReport.summary,
3077
- // #3248: bounded per-turn on this lane's own row.
3078
- dispositionSuppressed: qualityDispositionSuppressed,
3079
- },
3080
- });
3081
- }
3082
- catch (err) {
3083
- dbg(`turn_end: code quality warning report failed: ${err}`);
3084
- logLatency({
3085
- type: "phase",
3086
- toolName: "turn_end",
3087
- filePath: cwd,
3088
- phase: "code_quality_warnings_report",
3089
- durationMs: Date.now() - t5,
3090
- metadata: {
3091
- failed: true,
3092
- error: err instanceof Error ? err.message : String(err),
3093
- },
3094
- });
3095
- }
3096
- cacheManager.incrementTurnCycle(cwd, currentOwner);
3097
- // Collect-later CLI runners continue off the write path. Their completed
3098
- // diagnostics use the same freshness gate as late auxiliary findings and
3099
- // enter the ordinary turn-end advisory delivery channel.
3100
- const runnerFindingsStart = Date.now();
3101
- // Turn-end delivery is deliberately non-blocking. Collect already-settled
3102
- // results and requeue the rest; the edit path already paid the deferral
3103
- // decision, so another 2s wait would charge every turn while a runner is
3104
- // still in flight (#2122 F5).
3105
- const pendingRunnerFindings = await drainPendingRunnerFindings(0);
3106
- let runnerFindingsDelivered = 0;
3107
- let runnerFindingsStale = 0;
3108
- let runnerFindingsFailed = 0;
3109
- let runnerFindingsDropped = 0;
3110
- /** #3248: bounded per-turn on this lane's own row, never per finding. */
3111
- let runnerFindingsDispositionSuppressed = 0;
3112
- const runnerFindingsDeliveredIds = [];
3113
- for (const pending of pendingRunnerFindings) {
3114
- const result = pending.result;
3115
- if (!result)
3116
- continue;
3117
- recordRunner(pending.filePath, pending.runnerId, result.status, result.diagnostics.length, Date.now() - pending.markedAtMs, pending.writeIndex);
3118
- if (result.status === "failed") {
3119
- runnerFindingsFailed += 1;
3120
- const detail = result.failureMessage ? `: ${result.failureMessage}` : "";
3121
- // @delivery-surface: runtime-turn:late-runner-findings
3122
- advisoryParts.push(`❌ Deferred runner ${pending.runnerId} failed (${result.failureKind ?? "unknown"})${detail}`);
3123
- continue;
3124
- }
3125
- const findings = result.diagnostics;
3126
- if (findings.length === 0)
3127
- continue;
3128
- const { "late-runner-findings": gate } = gateFindingsByPathFreshness({
3129
- cwd,
3130
- sources: {
3131
- "late-runner-findings": {
3132
- findings,
3133
- scannedAt: pending.markedAtMs,
3134
- citedPath: (finding) => finding.filePath,
3135
- },
3136
- },
3137
- });
3138
- runnerFindingsStale += gate.stale.length;
3139
- if (gate.stale.length > 0) {
3140
- // The runner answered for bytes older than the latest edit. Do not
3141
- // re-arm this completed answer: only a new runner query can restore
3142
- // coverage for the refreshed bytes.
3143
- dropStaleRunnerFindings(pending);
3144
- runnerFindingsDropped += 1;
3145
- }
3146
- if (gate.live.length === 0)
3147
- continue;
3148
- const displayPath = toRunnerDisplayPath(cwd, pending.filePath);
3149
- // #3248: the survivors are what the agent READS, so they take the same
3150
- // policy stack the late-AUXILIARY drain below applies — this lane is its
3151
- // twin (same post-gate `Diagnostic[]`, same rendering) and was the one
3152
- // push surface still re-reporting a finding the agent had marked. AFTER
3153
- // the freshness gate, like every other lane: the anchor is derived from
3154
- // the post-gate identity, never the raw pre-gate set. The file's CURRENT
3155
- // bytes; an unreadable file fails open inside the helper.
3156
- let runnerContent;
3157
- try {
3158
- runnerContent = fs.readFileSync(pending.filePath, "utf-8");
3159
- }
3160
- catch {
3161
- runnerContent = undefined;
3162
- }
3163
- const { kept: runnerKept, suppressed: runnerSuppressedHere } = applyPushedFindingPolicy(gate.live, {
3164
- cwd,
3165
- filePath: pending.filePath,
3166
- content: runnerContent,
3167
- });
3168
- runnerFindingsDispositionSuppressed += runnerSuppressedHere;
3169
- if (runnerKept.length === 0) {
3170
- // Every late finding was marked. A PUSH surface stays silent rather
3171
- // than re-announcing that the mark is working; the count rides this
3172
- // lane's bounded per-turn row below.
3173
- continue;
3174
- }
3175
- const lines = runnerKept.map((finding) => ` ${displayPath}:${finding.line ?? 1}:${finding.column ?? 1} [${finding.rule ?? finding.id}] ${finding.message}`);
3176
- runnerFindingsDelivered += runnerKept.length;
3177
- for (const finding of runnerKept) {
3178
- if (runnerFindingsDeliveredIds.length < 50) {
3179
- runnerFindingsDeliveredIds.push(finding.id);
3180
- }
3181
- }
3182
- // #1616 suppressed-bucket rule: a delivery that still has something to
3183
- // say states what it dropped, once per delivery.
3184
- const runnerSuppressedNote = runnerSuppressedHere > 0
3185
- ? `; suppressed by disposition: ${runnerSuppressedHere} finding(s)`
3186
- : "";
3187
- // @delivery-surface: runtime-turn:late-runner-findings
3188
- advisoryParts.push(`⏱️ Late runner diagnostics (${pending.runnerId} completed after the edit${runnerSuppressedNote}):\n${lines.join("\n")}`);
3189
- }
3190
- logLatency({
3191
- type: "phase",
3192
- toolName: "turn_end",
3193
- filePath: cwd,
3194
- phase: "late_runner_findings",
3195
- durationMs: Date.now() - runnerFindingsStart,
3196
- metadata: {
3197
- pending: pendingRunnerFindings.length,
3198
- delivered: runnerFindingsDelivered,
3199
- stale: runnerFindingsStale,
3200
- failed: runnerFindingsFailed,
3201
- dropped: runnerFindingsDropped,
3202
- dispositionSuppressed: runnerFindingsDispositionSuppressed,
3203
- deliveredIds: runnerFindingsDeliveredIds,
3204
- },
3205
- });
3206
- // #2001/#2002: collect-later delivery for auxiliary LSP servers whose
3207
- // aux-grace window expired without a publication (opengrep on Windows:
3208
- // ~8s per scan against a 2s grace — the scanner's eventual findings sat
3209
- // in its client cache, agent-invisible). Probe each pending pair through
3210
- // the read-only cache seam (never spawns), freshness-gate the result
3211
- // against when the pair was marked, and deliver survivors as an advisory.
3212
- // A pair whose client is alive but has STILL published nothing re-arms
3213
- // (baseline preserved, TTL anchor advanced) until the re-arm TTL; a dead
3214
- // client drops silently.
3215
- const lateAuxStart = Date.now();
3216
- const drainedPairs = drainPendingAuxiliaryCoverage();
3217
- // #2168: cap evictions retire a pair before any drain can observe it — read
3218
- // and reset that count here so it folds into this turn's reconciliation
3219
- // sum instead of the pair vanishing uncounted.
3220
- const lateAuxCapEvicted = drainPendingAuxCapEvictedCount();
3221
- let lateAuxDelivered = 0;
3222
- let lateAuxStale = 0;
3223
- let lateAuxMissing = 0;
3224
- let lateAuxRearmed = 0;
3225
- let lateAuxClientGone = 0;
3226
- let lateAuxProbeFailed = 0;
3227
- let lateAuxCleanConfirmed = 0;
3228
- let lateAuxExpired = 0;
3229
- let lateAuxCeilingExhausted = 0;
3230
- let lateAuxAnswered = 0;
3231
- let lateAuxNotifyStallDemoted = 0;
3232
- // #3102: dropped by the shared finding-policy stack (inline `pi-lens-ignore`
3233
- // / stored disposition / `.pi-lens.json` rule policy) and, separately, by the
3234
- // auxiliary profile's OWN native suppression inside `retagAuxiliaryDiagnostics`.
3235
- // Both are reported in the one bounded per-turn record below — a drop is
3236
- // never silent (shape 10).
3237
- let lateAuxDispositionSuppressed = 0;
3238
- let lateAuxAuxSuppressed = 0;
3239
- const lateAuxCoverageGapPairs = [];
3240
- let lateAuxCoverageGapDetailCount = 0;
3241
- let lateAuxCoverageGapDropCount = 0;
3242
- const lateAuxStuckPairs = [];
3243
- if (drainedPairs.length > 0) {
3244
- const lateObserverDeadline = Date.now() + HOOK_WALL_BUDGET_MS.turn_end;
3245
- const byFile = new Map();
3246
- for (const pair of drainedPairs) {
3247
- const list = byFile.get(pair.filePath);
3248
- if (list)
3249
- list.push(pair);
3250
- else
3251
- byFile.set(pair.filePath, [pair]);
3252
- }
3253
- try {
3254
- const service = getLSPService();
3255
- for (const [lateAuxPath, pairs] of byFile) {
3256
- let cached;
3257
- try {
3258
- cached = await service.readCachedDiagnosticsForServers(lateAuxPath, new Set(pairs.map((p) => p.serverId)));
3259
- }
3260
- catch {
3261
- // #2027 round-1 P3-2 / #2167 R2-2: a transient probe rejection
3262
- // tells us nothing about the pair's content, so treat it like the
3263
- // "still scanning" branch below — re-arm under the SAME
3264
- // ceiling/TTL bound rather than dropping the coverage outright.
3265
- // `probeFailed` stays an honest per-turn failure count; it is
3266
- // informational (like `stale`/`missing`), not a terminal bucket,
3267
- // since the pair itself still resolves through rearmed/
3268
- // ceilingExhausted/expired below.
3269
- lateAuxProbeFailed += pairs.length;
3270
- for (const pair of pairs) {
3271
- if (!isPendingAuxiliaryPastRearmTtl(pair) &&
3272
- (pair.rearmCount ?? 0) < MAX_LATE_AUX_REARMS) {
3273
- rearmPendingAuxiliaryCoverage(pair);
3274
- lateAuxRearmed += 1;
3275
- }
3276
- else if (isPendingAuxiliaryPastRearmTtl(pair)) {
3277
- lateAuxExpired += 1;
3278
- }
3279
- else {
3280
- lateAuxCeilingExhausted += 1;
3281
- }
3282
- }
3283
- continue;
3284
- }
3285
- const displayLateAuxPath = toRunnerDisplayPath(cwd, lateAuxPath);
3286
- // #3102: the file's CURRENT bytes, for the two content-bound halves
3287
- // of the policy stack (inline `pi-lens-ignore` and the STRICT
3288
- // `false-positive` anchor) and for the auxiliary profile's own
3289
- // native suppression. Read at most ONCE per file per drain, lazily:
3290
- // a turn that drains nothing, finds no live client, re-arms, or
3291
- // confirms clean pays no I/O at all, and a file with several pending
3292
- // servers pays one read for all of them. Measured cost on the one
3293
- // file that does publish: 0.10-0.20 ms typical, 4.0 ms worst case
3294
- // (a 180 KB file, 5 findings, a populated disposition store) against
3295
- // the 3000 ms `HOOK_WALL_BUDGET_MS.turn_end`. A read failure yields
3296
- // `undefined`: the content-free half still applies and nothing is
3297
- // hidden on an I/O error (shape 48).
3298
- let lateAuxContentRead = false;
3299
- let lateAuxContentValue;
3300
- const readLateAuxContent = () => {
3301
- if (!lateAuxContentRead) {
3302
- lateAuxContentRead = true;
3303
- try {
3304
- lateAuxContentValue = fs.readFileSync(lateAuxPath, "utf-8");
3305
- }
3306
- catch {
3307
- lateAuxContentValue = undefined;
3308
- }
3309
- }
3310
- return lateAuxContentValue;
3311
- };
3312
- for (const pair of pairs) {
3313
- const cachedEntry = cached.get(pair.serverId);
3314
- if (cachedEntry === undefined) {
3315
- // No live client for this server any more — best-effort probe,
3316
- // drop the pair silently.
3317
- lateAuxClientGone += 1;
3318
- continue;
3319
- }
3320
- if (cachedEntry.notifyStallDemoted) {
3321
- if (cachedEntry.demotedAt === undefined ||
3322
- pair.markedAtMs > cachedEntry.demotedAt) {
3323
- // A pair marked after teardown belongs to the missing
3324
- // generation, so it follows ordinary clientGone handling.
3325
- lateAuxClientGone += 1;
3326
- continue;
3327
- }
3328
- // #2356: notify-stall teardown is a transient absence while the
3329
- // breaker cools down, but only for a pair marked before teardown.
3330
- lateAuxNotifyStallDemoted += 1;
3331
- const pastTtl = isPendingAuxiliaryPastRearmTtl(pair);
3332
- const atCeiling = (pair.rearmCount ?? 0) >= MAX_LATE_AUX_REARMS;
3333
- if (!pastTtl && !atCeiling) {
3334
- rearmPendingAuxiliaryCoverage(pair);
3335
- lateAuxRearmed += 1;
3336
- if (lateAuxStuckPairs.length < 20)
3337
- lateAuxStuckPairs.push({
3338
- filePath: pair.filePath,
3339
- serverId: pair.serverId,
3340
- });
3341
- }
3342
- else {
3343
- if (pastTtl)
3344
- lateAuxExpired += 1;
3345
- else
3346
- lateAuxCeilingExhausted += 1;
3347
- lateAuxCoverageGapPairs.push({
3348
- filePath: pair.filePath,
3349
- serverId: pair.serverId,
3350
- });
3351
- }
3352
- continue;
3353
- }
3354
- const rawDiags = cachedEntry.diags;
3355
- if (cachedEntry.publishedAt === undefined ||
3356
- cachedEntry.publishedAt <= pair.markedAtMs) {
3357
- // Still scanning (or published nothing yet) — keep waiting
3358
- // so a scan finishing before the NEXT turn end still
3359
- // delivers. Two clocks, deliberately decoupled: the
3360
- // freshness baseline (`markedAtMs`) NEVER moves — it is what
3361
- // the delivery gate stats against — while the re-arm TTL is
3362
- // anchored on `lastRearmedAtMs`, advanced by every successful
3363
- // empty probe: the scanner is demonstrably alive, just slow.
3364
- if (!isPendingAuxiliaryPastRearmTtl(pair) &&
3365
- (pair.rearmCount ?? 0) < MAX_LATE_AUX_REARMS) {
3366
- rearmPendingAuxiliaryCoverage(pair);
3367
- lateAuxRearmed += 1;
3368
- if (lateAuxStuckPairs.length < 20)
3369
- lateAuxStuckPairs.push({
3370
- filePath: pair.filePath,
3371
- serverId: pair.serverId,
3372
- });
3373
- }
3374
- else {
3375
- if (isPendingAuxiliaryPastRearmTtl(pair))
3376
- lateAuxExpired += 1;
3377
- else
3378
- lateAuxCeilingExhausted += 1;
3379
- }
3380
- continue;
3381
- }
3382
- // A demoted auxiliary still answers through this late path. Feed the
3383
- // publication-minus-mark interval into the re-promotion streak. This is
3384
- // delivery latency observed by the drain, not the scanner's total scan
3385
- // latency. Cache priming is
3386
- // below the freshness gate so a changed file cannot resurrect stale data.
3387
- if (typeof service.observeLateAuxiliaryAnswer === "function") {
3388
- await bounded(service.observeLateAuxiliaryAnswer(lateAuxPath, pair.serverId, cachedEntry.publishedAt - pair.markedAtMs), {
3389
- ms: Math.max(1, lateObserverDeadline - Date.now()),
3390
- signal: deps.signal /* late observer */,
3391
- hook: "turn_end",
3392
- label: "observeLateAuxiliaryAnswer",
3393
- });
3394
- }
3395
- if (rawDiags.length === 0) {
3396
- lateAuxCleanConfirmed += 1;
3397
- continue;
3398
- }
3399
- // `convertLspDiagnostics` drops entries with no start line, which
3400
- // would break the 1:1 index alignment `retagAuxiliaryDiagnostics`
3401
- // needs. Partition first so `converted[i]` IS `anchored[i]` —
3402
- // the same pre-partition `applyLspFindingPolicy` does.
3403
- const anchored = rawDiags.filter((d) => d.range?.start?.line !== undefined);
3404
- if (anchored.length === 0) {
3405
- lateAuxMissing += rawDiags.length;
3406
- lateAuxAnswered += 1;
3407
- continue;
3408
- }
3409
- const lateAuxContent = readLateAuxContent();
3410
- const converted = convertLspDiagnostics(anchored, lateAuxPath);
3411
- // #3046/#3047: the auxiliary's REAL tool id (and its own native
3412
- // inline suppression — opengrep's `# nosemgrep`, ast-grep's
3413
- // test-file gate), from the ONE shared derivation every other
3414
- // surface anchors a mark against. These diagnostics come straight
3415
- // off the aux client's cache, so nothing upstream applied it.
3416
- const retained = retagAuxiliaryDiagnostics(converted, anchored, lateAuxContent ?? "", { cwd, fileRole: detectFileRole(lateAuxPath, lateAuxContent) });
3417
- lateAuxAuxSuppressed += converted.length - retained.length;
3418
- if (retained.length === 0) {
3419
- lateAuxAnswered += 1;
3420
- continue;
3421
- }
3422
- // Freshness kernel (#1634 gated surface): stat the cited file
3423
- // against the mark timestamp. Missing → drop (no remediation for
3424
- // a deleted file); mtime drifted past the mark → drop too, NOT
3425
- // demote — unlike the cached-blocker gates these findings were
3426
- // NEVER delivered before, and the edit that drifted the file
3427
- // already re-touched it (a fresh pending pair supersedes this
3428
- // one), so a stale-arm replay would double-report old content.
3429
- // Both drops are COUNTED here and in the latency record below —
3430
- // never silent (shape 10).
3431
- const { "late-auxiliary-findings": gate } = gateFindingsByPathFreshness({
3432
- cwd,
3433
- sources: {
3434
- "late-auxiliary-findings": {
3435
- findings: retained,
3436
- scannedAt: pair.markedAtMs,
3437
- citedPath: () => lateAuxPath,
3438
- },
3439
- },
3440
- });
3441
- lateAuxStale += gate.stale.length;
3442
- lateAuxMissing +=
3443
- retained.length - gate.live.length - gate.stale.length;
3444
- if (gate.live.length === 0) {
3445
- if (gate.stale.length > 0) {
3446
- // Stale findings mean the scan predates the last edit. Re-arm
3447
- // with a refreshed baseline and carry the ceiling count.
3448
- if (!isPendingAuxiliaryPastRearmTtl(pair) &&
3449
- (pair.rearmCount ?? 0) < MAX_LATE_AUX_REARMS) {
3450
- rearmPendingAuxiliaryCoverage(pair, Date.now(), true);
3451
- lateAuxRearmed += 1;
3452
- }
3453
- else if (isPendingAuxiliaryPastRearmTtl(pair)) {
3454
- lateAuxExpired += 1;
3455
- }
3456
- else {
3457
- lateAuxCeilingExhausted += 1;
3458
- }
3459
- }
3460
- else {
3461
- lateAuxAnswered += 1;
3462
- }
3463
- continue;
3464
- }
3465
- // #2810 round 4: this drain does NOT write the hash-bound
3466
- // last-known record. The prime it used to call could only fire when
3467
- // a record already existed at the pair's hash — which requires a
3468
- // FULLY covered touch of those exact bytes, the one case where the
3469
- // scanner's findings are already in the record — so it was a no-op
3470
- // in the demoted steady state it was added for, and a #570/#1470
3471
- // hazard everywhere else (an auxiliary-only array replacing the
3472
- // merged one). Late findings reach the agent as the gated advisory
3473
- // below; the turn-end hash-guarded fast path stays cold for a file
3474
- // whose touch was partial, which is exactly what #1470 requires.
3475
- // #3102: the survivors are what the agent READS, so they take the
3476
- // same `clients/dispatch/finding-policy.ts` stack — inline
3477
- // `pi-lens-ignore` → stored dispositions → `.pi-lens.json` rule
3478
- // policy — the per-edit dispatcher, `mode=full` and the
3479
- // `source=lsp` probe lane apply. Without it a finding the agent
3480
- // marked `false-positive` re-reported on every turn that drained
3481
- // a late pair. Runs AFTER the freshness gate, like the #1625
3482
- // govulncheck/secrets filters: the anchor is derived from the
3483
- // post-gate identity, never the raw pre-gate set.
3484
- // #3248: the four arguments moved into
3485
- // `applyPushedFindingPolicy` so the late-RUNNER drain below
3486
- // cannot make a second copy of them. Same stack, same
3487
- // identities, same fail-open content rule.
3488
- const { kept: lateAuxKept, suppressed: lateAuxSuppressedHere } = applyPushedFindingPolicy(gate.live, {
3489
- cwd,
3490
- filePath: lateAuxPath,
3491
- content: lateAuxContent,
3492
- });
3493
- lateAuxDispositionSuppressed += lateAuxSuppressedHere;
3494
- if (lateAuxKept.length === 0) {
3495
- // Every late finding was suppressed. This is a PUSH surface:
3496
- // silence after a mark is the mark working, not a clean
3497
- // verdict, so the count rides the bounded per-turn record below
3498
- // instead of re-announcing the suppression every single turn.
3499
- lateAuxAnswered += 1;
3500
- continue;
3501
- }
3502
- const lines = lateAuxKept.map((f) => ` ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
3503
- lateAuxDelivered += lateAuxKept.length;
3504
- lateAuxAnswered += 1;
3505
- // #1616 suppressed-bucket rule: a delivery that still has
3506
- // something to say states what it dropped, once per delivery.
3507
- const lateAuxSuppressedNote = lateAuxSuppressedHere > 0
3508
- ? `; suppressed by disposition: ${lateAuxSuppressedHere} finding(s)`
3509
- : "";
3510
- // @delivery-surface: runtime-turn:late-auxiliary-findings
3511
- advisoryParts.push(`🕐 Late auxiliary diagnostics (${pair.serverId} answered after its grace window${lateAuxSuppressedNote}):\n${lines.join("\n")}`);
3512
- }
3513
- }
3514
- }
3515
- catch (err) {
3516
- dbg(`turn_end: late-auxiliary probe failed: ${err}`);
3517
- }
3518
- // #2356: a demoted scanner that never gets replaced remains a coverage
3519
- // gap. Re-raise it once when the existing bounded late-pair window closes,
3520
- // preserving the server/file identity in both the ledger and latency row.
3521
- for (const pair of lateAuxCoverageGapPairs) {
3522
- const normalizedPairPath = normalizeMapKey(pair.filePath);
3523
- const emitted = emitBounded("lsp_scanner_coverage_gap", `${pair.serverId}:${normalizedPairPath}`, {
3524
- filePath: normalizedPairPath,
3525
- durationMs: 0,
3526
- metadata: {
3527
- source: "late-auxiliary",
3528
- serverIds: [pair.serverId],
3529
- reason: "notify-stall-replacement-unavailable",
3530
- reRaised: true,
3531
- },
3532
- }, {
3533
- ledgerKind: "lsp-scanner-coverage-gap",
3534
- reason: "notify-stall replacement was not available before late-coverage ceiling",
3535
- capPerTurn: {
3536
- limit: LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN,
3537
- turnIndex: runtime.turnIndex,
3538
- },
3539
- });
3540
- if (emitted)
3541
- lateAuxCoverageGapDetailCount += 1;
3542
- else
3543
- lateAuxCoverageGapDropCount += 1;
3544
- }
3545
- logLatency({
3546
- type: "phase",
3547
- toolName: "turn_end",
3548
- filePath: cwd,
3549
- phase: "late_auxiliary_findings",
3550
- durationMs: Date.now() - lateAuxStart,
3551
- metadata: {
3552
- pending: drainedPairs.length,
3553
- pairCreated: drainedPairs.length + lateAuxCapEvicted,
3554
- pendingAfter: pendingAuxiliaryCoverageSize(),
3555
- delivered: lateAuxDelivered,
3556
- stale: lateAuxStale,
3557
- missing: lateAuxMissing,
3558
- rearmed: lateAuxRearmed,
3559
- clientGone: lateAuxClientGone,
3560
- probeFailed: lateAuxProbeFailed,
3561
- cleanConfirmed: lateAuxCleanConfirmed,
3562
- expired: lateAuxExpired,
3563
- ceilingExhausted: lateAuxCeilingExhausted,
3564
- answered: lateAuxAnswered,
3565
- dispositionSuppressed: lateAuxDispositionSuppressed,
3566
- auxSuppressed: lateAuxAuxSuppressed,
3567
- notifyStallDemoted: lateAuxNotifyStallDemoted,
3568
- coverageGapReRaised: lateAuxCoverageGapPairs.length,
3569
- coverageGapReRaisedDetailed: lateAuxCoverageGapDetailCount,
3570
- coverageGapReRaisedDropped: lateAuxCoverageGapDropCount,
3571
- capEvicted: lateAuxCapEvicted,
3572
- stuckPairs: lateAuxStuckPairs,
3573
- },
3574
- });
3575
- }
3576
- const labeledAdvisoryParts = advisoryParts.map((p) => `ℹ️ Advisory — no action required this turn:\n${p}`);
3577
- // Stale-secret parts sit between the two tiers and are NOT relabelled — they
3578
- // ship the imperative preamble they were built with (#1622 review M2).
3579
- const findingParts = [
3580
- ...blockerParts,
3581
- ...staleSecretParts,
3582
- ...labeledAdvisoryParts,
3583
- ];
3584
- if (findingParts.length > 0) {
3585
- dbg(`turn_end: ${blockerParts.length} blocker section(s), ${advisoryParts.length} advisory section(s) found, persisting for next context`);
3586
- const content = capTurnEndMessage(findingParts.join("\n\n"));
3587
- const signature = `${files
3588
- .slice()
3589
- .sort((a, b) => compareOrdinal(a, b))
3590
- .join("|")}::${content}`;
3591
- const last = cacheManager.readCache("turn-end-findings-last", cwd);
3592
- if (last?.data?.signature === signature &&
3593
- last?.data?.sessionId === runtime.telemetrySessionId) {
3594
- dbg("turn_end: duplicate findings detected (same session), suppressing re-prompt");
3595
- if (getFlag("lens-guard")) {
3596
- const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
3597
- if (existingGuard) {
3598
- writeGitGuardRecord(cacheManager, runtime, cwd, {
3599
- ...existingGuard,
3600
- content,
3601
- blockerContent: blockerParts.length > 0
3602
- ? capTurnEndMessage(blockerParts.join("\n\n"))
3603
- : undefined,
3604
- hasBlockers: blockerParts.length > 0 || existingGuard.testFailures === true,
3605
- blockingFiles: blockerParts.length > 0 ? existingGuard.affectedFiles : undefined,
3606
- projectSeqStart: runtime.turnStartProjectSeq,
3607
- projectSeqEnd: runtime.projectSeq,
3608
- fileSeqByPath: Object.fromEntries(runtime
3609
- .getFileSeqEntries()
3610
- .map(([filePath, seq]) => [
3611
- normalizeMapKey(path.resolve(filePath)),
3612
- seq,
3613
- ])),
3614
- fileContentHashes: {},
3615
- consumed: false,
3616
- });
3617
- }
3618
- }
3619
- clearOwnedTurnState();
3620
- runtime.fixedThisTurn.clear();
3621
- resetFormatService();
3622
- return;
3623
- }
3624
- // #1950 fix-round F1: this turn's content is confirmed NOT suppressed —
3625
- // it is about to reach the agent — so NOW commit the delivery-count
3626
- // increments the per-blocker loop above only tentatively computed.
3627
- for (const commit of pendingDependencyDriftDeliveries)
3628
- commit();
3629
- const fileSeqByPath = {};
3630
- for (const [filePath, seq] of runtime.getFileSeqEntries()) {
3631
- fileSeqByPath[normalizeMapKey(path.resolve(filePath))] = seq;
3632
- }
3633
- if (getFlag("lens-guard")) {
3634
- const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
3635
- const blockingContent = blockerParts.length > 0
3636
- ? capTurnEndMessage(blockerParts.join("\n\n"))
3637
- : undefined;
3638
- const affectedFiles = [
3639
- ...(existingGuard?.affectedFiles ?? []),
3640
- ...files.map((file) => resolveRunnerPath(cwd, file)),
3641
- ...cascadeResults.flatMap((result) => result.neighbors
3642
- .filter((neighbor) => neighbor.diagnostics.length > 0)
3643
- .map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
3644
- ];
3645
- writeGitGuardRecord(cacheManager, runtime, cwd, {
3646
- content: [content, existingGuard?.testFailureContent]
3647
- .filter((value) => !!value)
3648
- .join("\n\n"),
3649
- blockerContent: blockingContent,
3650
- blockingFiles: blockerParts.length > 0 ? affectedFiles : undefined,
3651
- hasBlockers: !!blockingContent || existingGuard?.testFailures === true,
3652
- affectedFiles,
3653
- sessionId: runtime.telemetrySessionId,
3654
- projectSeqStart: runtime.turnStartProjectSeq,
3655
- projectSeqEnd: runtime.projectSeq,
3656
- fileSeqByPath,
3657
- fileContentHashes: {},
3658
- consumed: false,
3659
- testFailures: existingGuard?.testFailures,
3660
- testFailureContent: existingGuard?.testFailureContent,
3661
- testFailureFiles: existingGuard?.testFailureFiles,
3662
- });
3663
- }
3664
- else {
3665
- const allAffectedFiles = [
3666
- ...files.map((file) => resolveRunnerPath(cwd, file)),
3667
- ...cascadeResults.flatMap((result) => result.neighbors
3668
- .filter((neighbor) => neighbor.diagnostics.length > 0)
3669
- .map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
3670
- ];
3671
- const affectedFiles = [...new Set(allAffectedFiles)].slice(0, MAX_ADVISORY_AFFECTED_FILES);
3672
- const affectedFilesTruncated = new Set(allAffectedFiles).size > affectedFiles.length;
3673
- cacheManager.writeCache("turn-end-findings", {
3674
- content,
3675
- affectedFiles,
3676
- affectedFilesTruncated,
3677
- provenance: snapshotAdvisoryProvenance({
3678
- cwd,
3679
- runtime,
3680
- generation: 0,
3681
- files: affectedFiles.map((file) => ({
3682
- path: file,
3683
- role: "affected",
3684
- })),
3685
- truncated: affectedFilesTruncated,
3686
- }),
3687
- }, cwd);
3688
- }
3689
- cacheManager.writeCache("turn-end-findings-last", {
3690
- signature,
3691
- sessionId: runtime.telemetrySessionId,
3692
- projectSeqStart: runtime.turnStartProjectSeq,
3693
- projectSeqEnd: runtime.projectSeq,
3694
- }, cwd);
3695
- emitLensTurnFindings({
3696
- cwd,
3697
- filePaths: files.map((file) => resolveRunnerPath(cwd, file)),
3698
- sessionId: runtime.telemetrySessionId,
3699
- turnIndex: runtime.turnIndex,
3700
- blockerSections: blockerParts.length,
3701
- advisorySections: advisoryParts.length,
3702
- content,
3703
- });
3704
- }
3705
- if (blockerParts.length === 0) {
3706
- clearOwnedTurnState();
3707
- // `staleSecretParts` counts here too (#1622 review M2): clearing the
3708
- // findings record while a stale secret is still unverified would drop the
3709
- // only surviving trace of it.
3710
- if (getFlag("lens-guard") &&
3711
- advisoryParts.length === 0 &&
3712
- staleSecretParts.length === 0 &&
3713
- !runtime.gitGuardHasBlockers) {
3714
- const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
3715
- if (guardRecord?.sessionId === runtime.telemetrySessionId &&
3716
- guardRecord.testFailures !== true) {
3717
- cacheManager.clearCache("turn-end-findings", cwd);
3718
- }
3719
- }
3720
- }
3721
- runtime.fixedThisTurn.clear();
3722
- runtime.clearActionableWarnings();
3723
- runtime.clearCodeQualityWarnings();
3724
- if (demotedFindingsRetired > 0) {
3725
- // #1944: the retired payload must not survive as a SUPPRESSION key.
3726
- // `turn-end-findings-last` holds a content signature used to silence a
3727
- // duplicate re-prompt; live evidence found the demoted payload still on
3728
- // disk 80+ minutes after the file shrank. It is not a delivery source —
3729
- // `runtime-context.ts` never reads it, and the read at the top of this
3730
- // function is gated on the current `sessionId`, so it cannot resurrect
3731
- // the finding in a later session. It CAN, however, silence a genuinely
3732
- // new report of content the store no longer holds. Drop it with the
3733
- // record it describes.
3734
- cacheManager.clearCache("turn-end-findings-last", cwd);
3735
- }
3736
- logLatency({
3737
- type: "tool_result",
3738
- toolName: "turn_end",
3739
- filePath: cwd,
3740
- durationMs: Date.now() - turnEndStart,
3741
- // #1622 review M2: a pending stale secret is NOT a clean turn. It gets its
3742
- // own result rather than being promoted to `blockers_found`, which would
3743
- // undo the demotion the freshness gate just made.
3744
- result: blockerParts.length > 0
3745
- ? "blockers_found"
3746
- : staleSecretParts.length > 0
3747
- ? "stale_secrets_pending"
3748
- : "clean",
3749
- metadata: {
3750
- fileCount: files.length,
3751
- blockerSections: blockerParts.length,
3752
- staleSecretSections: staleSecretParts.length,
3753
- advisorySections: advisoryParts.length,
3754
- // #1944 AC3: an empty advisory section on its own cannot say whether
3755
- // the turn had nothing to report or dropped something. This counter
3756
- // answers that from latency.log even when the payload is empty, and
3757
- // the payload itself carries the retirement note when it is not.
3758
- demotedFindingsRetired,
3759
- // #2275: the widget-footer store's own dependency-drift retirements —
3760
- // a separate surface/counter from `demotedFindingsRetired` above,
3761
- // since a widget retirement never touches `advisoryParts` or the
3762
- // `turn-end-findings-last` suppression cache the block below clears.
3763
- widgetDemotedFindingsRetired,
3764
- },
3765
- });
3766
- resetFormatService();
3767
- }