@herbertgao/pi-extensions 2026.8.4 → 2026.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  26. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  36. package/node_modules/@narumitw/pi-btw/README.md +32 -24
  37. package/node_modules/@narumitw/pi-btw/package.json +6 -6
  38. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  39. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
  40. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  41. package/node_modules/pi-lens/CHANGELOG.md +2535 -954
  42. package/node_modules/pi-lens/README.md +13 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  44. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  45. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  46. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  47. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  48. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  50. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  51. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  52. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  53. package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
  54. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  55. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  56. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  57. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  58. package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
  59. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  60. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  61. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  62. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  63. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  64. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  65. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  66. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  67. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
  69. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  70. package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
  71. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  72. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  73. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  74. package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
  75. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
  76. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
  77. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
  78. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  79. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  80. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  81. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  83. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
  84. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  87. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  114. package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
  115. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  116. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  117. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  118. package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
  119. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  120. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  121. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  122. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  123. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
  125. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  126. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  127. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  128. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  129. package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
  130. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  131. package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  134. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  135. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  136. package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
  137. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  138. package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
  139. package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
  140. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  141. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  142. package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
  143. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  144. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  145. package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
  146. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  147. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  148. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
  149. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  150. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  151. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
  152. package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
  153. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  154. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
  155. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  156. package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
  157. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  158. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
  161. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  162. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  163. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  164. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  165. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  167. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
  168. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  171. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  172. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  173. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  174. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  175. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  177. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  179. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  180. package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
  181. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  182. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  183. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  184. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  185. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  186. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  187. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  188. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  189. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  190. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  191. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  192. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  193. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  194. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  195. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  196. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  197. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  198. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  199. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  201. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  202. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  203. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  204. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  205. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  206. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  207. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  208. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  217. package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
  218. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  219. package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
  220. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  221. package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
  222. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  223. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  224. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  225. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  226. package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
  227. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  228. package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
  229. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  230. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  233. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  234. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  237. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  238. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  239. package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
  240. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  241. package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
  242. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  243. package/node_modules/pi-lens/dist/index.js +51785 -36118
  244. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  245. package/node_modules/pi-lens/dist/mcp/server.js +219 -46
  246. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  247. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  248. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  249. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  250. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
  251. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
  252. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  253. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  254. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  255. package/node_modules/pi-lens/docs/agent-guide.md +40 -15
  256. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  257. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  258. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  259. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
  260. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  261. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  262. package/node_modules/pi-lens/docs/fable.md +27 -0
  263. package/node_modules/pi-lens/docs/features.md +45 -3
  264. package/node_modules/pi-lens/docs/globalconfig.md +56 -1
  265. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  266. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  267. package/node_modules/pi-lens/docs/mcp.md +37 -4
  268. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  269. package/node_modules/pi-lens/docs/settings.md +18 -1
  270. package/node_modules/pi-lens/docs/tools.md +4 -1
  271. package/node_modules/pi-lens/docs/usage.md +41 -5
  272. package/node_modules/pi-lens/docs/word-index.md +35 -0
  273. package/node_modules/pi-lens/package.json +10 -10
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  300. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  301. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  302. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
  303. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  304. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  305. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  306. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  307. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  308. package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
  309. package/node_modules/pi-mcp-adapter/README.md +38 -4
  310. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  311. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  312. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  313. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  314. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  315. package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
  316. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  317. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  318. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  319. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  320. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  321. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  322. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  323. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  324. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  325. package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
  326. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  327. package/node_modules/pi-mcp-adapter/package.json +4 -3
  328. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  329. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  330. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  331. package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
  332. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  333. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  334. package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
  335. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  336. package/node_modules/pi-mcp-adapter/types.ts +176 -27
  337. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  338. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  339. package/node_modules/pi-web-access/README.md +18 -12
  340. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  341. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  342. package/node_modules/pi-web-access/curator-page.ts +5 -3
  343. package/node_modules/pi-web-access/curator-server.ts +2 -1
  344. package/node_modules/pi-web-access/extract.ts +106 -34
  345. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  346. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  347. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  348. package/node_modules/pi-web-access/index.ts +120 -48
  349. package/node_modules/pi-web-access/package.json +2 -2
  350. package/node_modules/pi-web-access/summary-review.ts +11 -5
  351. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  352. package/package.json +12 -13
  353. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  354. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  355. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  356. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  357. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  358. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  359. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  360. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -2,13 +2,17 @@ import * as path from "node:path";
2
2
  import { appendActionableWarningsHistory, buildActionableWarningsReport, formatActionableWarningsAdvisory, writeActionableWarningsReport, } from "./actionable-warnings.js";
3
3
  import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
4
4
  import { appendCodeQualityWarningsHistory, buildCodeQualityWarningsReport, formatCodeQualityWarningsAdvisory, writeCodeQualityWarningsReport, } from "./code-quality-warnings.js";
5
+ import { clearGitGuardTestFailure, mergeGitGuardTestFailure, writeGitGuardRecord, } from "./git-guard.js";
5
6
  import { logCascade } from "./cascade-logger.js";
6
7
  import { normalizeMapKey } from "./path-utils.js";
7
8
  import { resolveRunnerPath, toRunnerDisplayPath, } from "./dispatch/runner-context.js";
8
9
  import { getKnipIgnorePatterns } from "./file-utils.js";
10
+ import { isTestRoleCollateral } from "./collateral-test-role.js";
9
11
  import { dedupeSecretFindings, fromAstGrepWarnings, fromGitleaks, fromTrivySecrets, isSecretWarning, secretLocationKey, } from "./secret-findings.js";
10
- import { formatDeadCodeAdvisory } from "./dead-code-client.js";
12
+ import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, } from "./dead-code-client.js";
13
+ import { logDeadCodeScan } from "./dead-code-logger.js";
11
14
  import { PROJECT_DIAGNOSTICS_CACHE_VERSION, writeProjectDiagnosticsDeltaReport, } from "./project-diagnostics/cache.js";
15
+ import { deadCodeIssueToProjectDiagnostic } from "./project-diagnostics/runner-adapters/dead-code.js";
12
16
  import { knipIssuesToProjectDiagnostics } from "./project-diagnostics/runner-adapters/knip.js";
13
17
  import { logLatency } from "./latency-logger.js";
14
18
  import { getLspBudgetIdleTimeoutMs, shouldShortenLspIdleTimeout, } from "./lsp-budget.js";
@@ -16,6 +20,19 @@ import { updateHeartbeat } from "./instance-registry.js";
16
20
  import { emitLensTurnFindings } from "./lens-events.js";
17
21
  import { RUNTIME_CONFIG } from "./runtime-config.js";
18
22
  import { isSubagentSession } from "./subagent-mode.js";
23
+ import { formatRunDurationMs } from "./run-duration.js";
24
+ import { MAX_ADVISORY_AFFECTED_FILES, dropFindingsForMissingPaths, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
25
+ /**
26
+ * Would writing `next` over `prev` throw away a good scan for a failed one?
27
+ *
28
+ * A failed run carries no findings. Writing it evicts the last good result, and
29
+ * every later reader then serves the failure as the answer — a 194-byte "not
30
+ * available" record replaced 149 KB of real findings in every dogfood project
31
+ * (#925, #1467). Callers keep the previous cache when this returns true.
32
+ */
33
+ function wouldPoisonCache(prev, next) {
34
+ return !next.success && prev?.data.success === true;
35
+ }
19
36
  // LSP idle reset scheduling — prevents thrashing by delaying shutdown
20
37
  let lspIdleResetTimeout = null;
21
38
  function emitIdleResetReporterWarning(reportErr) {
@@ -86,7 +103,7 @@ function capTurnEndMessage(content) {
86
103
  return out;
87
104
  }
88
105
  export async function handleTurnEnd(deps) {
89
- const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, resetLSPService, resetFormatService, } = deps;
106
+ const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, owner, resetLSPService, resetFormatService, } = deps;
90
107
  // #449 slice 1: piggyback the instance-registry heartbeat on this existing
91
108
  // per-turn touchpoint rather than adding a new timer/interval. Cheap (reads
92
109
  // process.memoryUsage().rss, one read-modify-write of instances.json) and
@@ -106,16 +123,48 @@ export async function handleTurnEnd(deps) {
106
123
  });
107
124
  const cwd = ctxCwd ?? process.cwd();
108
125
  let turnState = cacheManager.readTurnState(cwd);
109
- // Evict turn state written by a previous session it carries stale file
110
- // ranges that no longer reflect the current editing context.
111
- if (turnState.sessionId &&
112
- turnState.sessionId !== runtime.telemetrySessionId) {
113
- dbg(`turn_end: evicting stale turn state (session ${turnState.sessionId} ≠ current ${runtime.telemetrySessionId})`);
114
- cacheManager.clearTurnState(cwd);
126
+ // A live foreign writer owns this worklist. Do not clear or consume another
127
+ // pi/MCP session's files; a dead/aged owner is safely evicted instead.
128
+ const currentOwner = owner ?? {
129
+ kind: "pi",
130
+ id: runtime.telemetrySessionId,
131
+ pid: process.pid,
132
+ lastSeen: new Date().toISOString(),
133
+ };
134
+ const access = cacheManager.getTurnStateAccess(cwd, currentOwner);
135
+ const sameProcessPiSessionHandoff = access === "foreign-live" &&
136
+ currentOwner.kind === "pi" &&
137
+ turnState.owner?.kind === "pi" &&
138
+ turnState.owner.pid === process.pid &&
139
+ turnState.owner.id !== currentOwner.id;
140
+ if (access === "foreign-live" && !sameProcessPiSessionHandoff) {
141
+ dbg(`turn_end: foreign live owner retained (${turnState.owner?.kind ?? "legacy"}:${turnState.owner?.id ?? turnState.sessionId})`);
142
+ return;
143
+ }
144
+ if (access === "available" && (turnState.files || turnState.owner || turnState.sessionId)) {
145
+ dbg("turn_end: evicting stale turn-state owner");
146
+ cacheManager.clearTurnState(cwd, currentOwner);
115
147
  turnState = cacheManager.readTurnState(cwd);
116
148
  }
117
149
  const files = Object.keys(turnState.files);
118
- if (files.length === 0) {
150
+ // R1 (#1443 follow-up): a read-only turn (no files touched) must not take
151
+ // the fast idle-reset path while a carried cascade run — or one still
152
+ // settling — is waiting for its delivery opportunity. Falling through to
153
+ // the normal pipeline lets the settle/drain/merge logic below run exactly
154
+ // as it does for an edit turn, so a carried finding reaches the agent
155
+ // instead of dying unrendered. `hasCascadeRuns()` is a cheap peek (no
156
+ // pending work almost every turn), so the common read-only turn still
157
+ // takes the early return below.
158
+ if (files.length === 0 && !runtime.hasCascadeRuns()) {
159
+ // A genuinely clean session must invalidate the persisted guard record.
160
+ // Blocker records are retained only while the runtime still reports one.
161
+ if (getFlag("lens-guard") && !runtime.gitGuardHasBlockers) {
162
+ const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
163
+ if (guardRecord?.sessionId === runtime.telemetrySessionId &&
164
+ guardRecord.testFailures !== true) {
165
+ cacheManager.clearCache("turn-end-findings", cwd);
166
+ }
167
+ }
119
168
  // #713: subagent sessions use a shorter idle reset (60s) — a short-lived
120
169
  // task agent holding a warm fleet for 4 minutes after its last turn is
121
170
  // pure waste under fan-out. Classify ONCE here so every tick in this call
@@ -143,7 +192,7 @@ export async function handleTurnEnd(deps) {
143
192
  dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}`);
144
193
  if (cacheManager.isMaxCyclesExceeded(cwd)) {
145
194
  dbg("turn_end: max cycles exceeded, clearing state and forcing through");
146
- cacheManager.clearTurnState(cwd);
195
+ cacheManager.clearTurnState(cwd, currentOwner);
147
196
  runtime.fixedThisTurn.clear();
148
197
  resetFormatService();
149
198
  return;
@@ -155,7 +204,7 @@ export async function handleTurnEnd(deps) {
155
204
  const projectDiagnosticsSources = new Set();
156
205
  // Re-surface inline blockers from this turn that the agent didn't fix.
157
206
  // These were shown inline during write/edit but the agent moved on without resolving them.
158
- const unresolvedBlockers = runtime.consumeInlineBlockers();
207
+ const unresolvedBlockers = runtime.getInlineBlockersSnapshot();
159
208
  for (const { filePath: bPath, summary } of unresolvedBlockers) {
160
209
  const displayPath = toRunnerDisplayPath(cwd, bPath);
161
210
  blockerParts.push(`Unresolved from this turn — ${displayPath}:\n${summary}`);
@@ -180,7 +229,62 @@ export async function handleTurnEnd(deps) {
180
229
  // 2. Neighbor-level: each neighbor is claimed by the latest cascade result
181
230
  // that covers it — suppresses stale neighbor state from earlier writes.
182
231
  const t0 = Date.now();
183
- const cascadeRuns = runtime.consumeCascadeRuns();
232
+ const cascadeRuns = runtime.consumeCascadeRuns().filter((run) => {
233
+ const originSeq = run.origin?.projectSeq;
234
+ const originTurn = run.origin?.turnSeq;
235
+ // A deferred result from AFTER a later write is not current state. Old test
236
+ // fixtures without provenance remain accepted for compatibility.
237
+ //
238
+ // #1443: `turnSeq` alone is NOT a supersede signal, and it used to be an
239
+ // unconditional reject. Every LATE run — one whose compute missed the
240
+ // settle cap and was re-parked by `settleCascadeRuns`, and one the
241
+ // quiet-window reconcile appended after this turn's predecessor already
242
+ // consumed (carried across turn_start by `beginTurn`) — is BY DEFINITION
243
+ // from an earlier turn, so `originTurn === runtime.turnIndex` was always
244
+ // false for exactly the runs the carry-over was built to preserve. Both
245
+ // producers' contracts were dead code: the measured cases were the two
246
+ // highest-fan-out cascades of the day (38 and 40 neighbours).
247
+ //
248
+ // R2 (#1443 follow-up): `projectSeq` alone is NOT a per-file supersede
249
+ // signal — it is GLOBAL, advancing on every pi-observed write anywhere in
250
+ // the project. Rejecting on any mismatch meant an edit to an unrelated
251
+ // file superseded a run that had nothing to do with it, reintroducing the
252
+ // exact 38/40-neighbour loss #1443 was written to fix, one filter down.
253
+ // `getFilesChangedSince` (#451) is the honest per-file signal: a run is
254
+ // superseded only if its own primary file or one of its neighbours was
255
+ // actually rewritten since it launched. A late-but-not-superseded run is
256
+ // surfaced; a superseded one is dropped with a RECORD (never silently),
257
+ // so the loss stays countable.
258
+ if (originSeq !== undefined) {
259
+ const changedSince = runtime.getFilesChangedSince(originSeq);
260
+ if (changedSince.length > 0) {
261
+ const changedSet = new Set(changedSince);
262
+ const primaryKey = normalizeMapKey(path.resolve(run.filePath));
263
+ const neighborKeys = (run.result?.neighbors ?? []).map((n) => normalizeMapKey(path.resolve(n.filePath)));
264
+ const supersededByOwnFile = changedSet.has(primaryKey) ||
265
+ neighborKeys.some((k) => changedSet.has(k));
266
+ if (supersededByOwnFile) {
267
+ logCascade({
268
+ phase: "cascade_carry_over_drop",
269
+ filePath: run.filePath,
270
+ neighborCount: run.neighborCount,
271
+ diagnosticCount: run.diagnosticCount,
272
+ reason: "superseded_by_later_write",
273
+ metadata: {
274
+ originProjectSeq: originSeq,
275
+ projectSeq: runtime.projectSeq,
276
+ originTurnSeq: originTurn,
277
+ turnIndex: runtime.turnIndex,
278
+ carriedTurns: run.carriedTurns,
279
+ changedFiles: changedSince,
280
+ },
281
+ });
282
+ return false;
283
+ }
284
+ }
285
+ }
286
+ return true;
287
+ });
184
288
  const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
185
289
  if (cascadeResults.length > 0) {
186
290
  const seen = new Map();
@@ -198,11 +302,25 @@ export async function handleTurnEnd(deps) {
198
302
  }
199
303
  }
200
304
  const parts = [];
305
+ // #1446 item 1: track what actually gets injected — a suppressed result
306
+ // (real formatted cascade text, but every one of its neighbors was claimed
307
+ // by a LATER result — see the reverse-iteration ownership pass above) was
308
+ // previously indistinguishable from "no output"; this counts it explicitly
309
+ // instead of letting it vanish.
310
+ let injectedNeighborCount = 0;
311
+ let injectedDiagnosticCount = 0;
312
+ let suppressedByOwnership = 0;
201
313
  for (const result of seen.values()) {
202
314
  const pk = normalizeMapKey(result.filePath);
203
315
  const ownsAny = result.neighbors.some((n) => neighborOwner.get(normalizeMapKey(n.filePath)) === pk);
204
- if (ownsAny && result.formatted)
316
+ if (ownsAny && result.formatted) {
205
317
  parts.push(result.formatted);
318
+ injectedNeighborCount += result.neighbors.length;
319
+ injectedDiagnosticCount += result.neighbors.reduce((s, n) => s + n.diagnostics.length, 0);
320
+ }
321
+ else if (!ownsAny && result.formatted) {
322
+ suppressedByOwnership++;
323
+ }
206
324
  }
207
325
  // Suggest tests for cascade neighbors (files with diagnostics)
208
326
  const neighborFilesWithErrors = cascadeResults
@@ -210,9 +328,27 @@ export async function handleTurnEnd(deps) {
210
328
  .filter((n) => n.diagnostics.length > 0)
211
329
  .map((n) => n.filePath);
212
330
  const uniqueNeighborFiles = [...new Set(neighborFilesWithErrors)];
331
+ let testSuggestionCount = 0;
213
332
  if (uniqueNeighborFiles.length > 0 &&
214
333
  typeof testRunnerClient.suggestTestFiles === "function") {
215
334
  const testSuggestions = testRunnerClient.suggestTestFiles(uniqueNeighborFiles, cwd);
335
+ testSuggestionCount = testSuggestions.length;
336
+ // #1446 item 2: this path previously emitted nothing to any log — a
337
+ // zero-suggestion outcome (neighbors had errors but no test file
338
+ // resolved for any of them) is the more interesting case, so it is
339
+ // recorded on the same phase rather than only logging on a hit.
340
+ logCascade({
341
+ phase: "cascade_test_targets",
342
+ filePath: files[0] ?? cwd,
343
+ neighborCount: uniqueNeighborFiles.length,
344
+ metadata: {
345
+ neighborFiles: uniqueNeighborFiles.slice(0, 10),
346
+ suggestedTestFiles: testSuggestions.slice(0, 10).map((s) => s.testFile),
347
+ runner: testSuggestions[0]?.runner,
348
+ truncated: testSuggestions.length > 10,
349
+ zeroSuggestions: testSuggestions.length === 0,
350
+ },
351
+ });
216
352
  if (testSuggestions.length > 0) {
217
353
  const testLines = testSuggestions
218
354
  .slice(0, 5)
@@ -224,8 +360,30 @@ export async function handleTurnEnd(deps) {
224
360
  parts.push(testSection);
225
361
  }
226
362
  }
227
- if (parts.length > 0)
228
- blockerParts.push(parts.join("\n\n"));
363
+ if (parts.length > 0) {
364
+ const section = parts.join("\n\n");
365
+ blockerParts.push(section);
366
+ // #1446 item 1: proves the cascade section reached `blockerParts` —
367
+ // i.e. it was QUEUED for persistence into the turn-end advisory — not
368
+ // that it reached the agent. The counters alone (cascade_result,
369
+ // cascade_turn_end) never confirmed even that much, only computation.
370
+ // Actual delivery happens later, via consumeTurnEndFindings/
371
+ // peekTurnEndFindings, and can still be suppressed after this point
372
+ // (e.g. allFilesDeleted, cross-turn dedup, or the session ending
373
+ // before the next turn_end drains it) — this record does not prove
374
+ // the agent ever saw the text.
375
+ logCascade({
376
+ phase: "cascade_injected",
377
+ filePath: files[0] ?? cwd,
378
+ neighborCount: injectedNeighborCount,
379
+ diagnosticCount: injectedDiagnosticCount,
380
+ metadata: {
381
+ sectionChars: section.length,
382
+ testSuggestionCount,
383
+ suppressedByOwnership,
384
+ },
385
+ });
386
+ }
229
387
  logCascade({
230
388
  phase: "cascade_turn_end",
231
389
  filePath: files[0] ?? cwd,
@@ -249,32 +407,70 @@ export async function handleTurnEnd(deps) {
249
407
  // (over-correction guard).
250
408
  const indeterminateRuns = cascadeRuns.filter((r) => r.indeterminate);
251
409
  if (indeterminateRuns.length > 0) {
252
- const byDetail = new Map();
253
- for (const r of indeterminateRuns) {
254
- const detail = r.indeterminate?.detail ??
255
- (r.indeterminate?.reason === "missing_node"
256
- ? "changed file not in the review graph"
257
- : "review graph unavailable");
258
- const files = byDetail.get(detail) ?? [];
259
- files.push(toRunnerDisplayPath(cwd, r.filePath));
260
- byDetail.set(detail, files);
261
- }
262
- const lines = [];
263
- for (const [detail, filesRaw] of byDetail) {
264
- const files = [...new Set(filesRaw)];
265
- const shown = files.slice(0, 5).join(", ");
266
- const more = files.length > 5 ? ` (+${files.length - 5} more)` : "";
267
- lines.push(` • ${detail}: ${shown}${more}`);
268
- }
269
- const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
270
- const reasons = [...byDetail.keys()].join("; ");
410
+ // #1104 (review P3 on PR #1143, rides with the resultId main body): this
411
+ // preamble used to hardcode a graph-unavailability frame for EVERY
412
+ // indeterminate reason. That's accurate for `graph_degraded`/
413
+ // `missing_node`/`error` (the graph really couldn't produce a dependent
414
+ // set), but `lsp_binding_rejected` is a DIFFERENT failure shape — the
415
+ // graph WAS available and dependents WERE derived; only their LSP
416
+ // diagnostics display was withheld because a fallback snapshot's content
417
+ // binding didn't match current disk. Saying "the review graph was
418
+ // unavailable" for that case mis-attributes the cause. Bucket by reason
419
+ // family so each gets its own accurate frame.
420
+ const buildAdvisory = (runs, frame) => {
421
+ if (runs.length === 0)
422
+ return undefined;
423
+ const byDetail = new Map();
424
+ for (const r of runs) {
425
+ const detail = r.indeterminate?.detail ?? frame.fallbackDetail(r);
426
+ const files = byDetail.get(detail) ?? [];
427
+ files.push(toRunnerDisplayPath(cwd, r.filePath));
428
+ byDetail.set(detail, files);
429
+ }
430
+ const lines = [];
431
+ for (const [detail, filesRaw] of byDetail) {
432
+ const files = [...new Set(filesRaw)];
433
+ const shown = files.slice(0, 5).join(", ");
434
+ const more = files.length > 5 ? ` (+${files.length - 5} more)` : "";
435
+ lines.push(` • ${detail}: ${shown}${more}`);
436
+ }
437
+ const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath))).size;
438
+ const reasons = [...byDetail.keys()].join("; ");
439
+ return `${frame.lead(fileCount, reasons)}\n${lines.join("\n")}`;
440
+ };
441
+ // #1445: `excluded_by_role` (test files excluded from the graph BY DESIGN,
442
+ // #260) is never agent-facing — it is not a graph failure, and #1080
443
+ // already excludes test-role files from every neighbor surface, so "a
444
+ // clean result does not cover them" would itself be a false claim. It
445
+ // stays visible in the `cascade_indeterminate` log below (metadata-only,
446
+ // info-level) so the log can tell an intentional exclusion from a real
447
+ // graph gap, but it never reaches `buildAdvisory`/the agent.
448
+ const graphRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason !== "lsp_binding_rejected" &&
449
+ r.indeterminate?.reason !== "excluded_by_role");
450
+ const bindingRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "lsp_binding_rejected");
271
451
  // Factual/informational phrasing — the advisory tier wraps this with an
272
452
  // "ℹ️ Advisory — no action required this turn:" label, so an imperative
273
453
  // ("review dependents manually") would contradict it. The #533 substance
274
454
  // stays: a clean cascade result does NOT cover these files' dependents.
275
- advisoryParts.push(`Cascade could not compute downstream impact for ${fileCount} edited file(s) this turn — ` +
276
- `the review graph was unavailable (${reasons}), so their dependents were not ` +
277
- `cascade-checked and a clean cascade result does not cover them.\n${lines.join("\n")}`);
455
+ const graphAdvisory = buildAdvisory(graphRuns, {
456
+ lead: (fileCount, reasons) => `Cascade could not compute downstream impact for ${fileCount} edited file(s) this turn ` +
457
+ `the review graph was unavailable (${reasons}), so their dependents were not ` +
458
+ `cascade-checked and a clean cascade result does not cover them.`,
459
+ fallbackDetail: (r) => r.indeterminate?.reason === "missing_node"
460
+ ? "changed file not in the review graph"
461
+ : "review graph unavailable",
462
+ });
463
+ if (graphAdvisory)
464
+ advisoryParts.push(graphAdvisory);
465
+ const bindingAdvisory = buildAdvisory(bindingRuns, {
466
+ lead: (fileCount, reasons) => `Cascade identified dependents for ${fileCount} edited file(s) this turn, but their ` +
467
+ `diagnostics could not be freshly confirmed (${reasons}) and were withheld — a clean ` +
468
+ `cascade result does not cover them.`,
469
+ fallbackDetail: () => "cascade diagnostics withheld (binding rejected)",
470
+ });
471
+ if (bindingAdvisory)
472
+ advisoryParts.push(bindingAdvisory);
473
+ const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
278
474
  logCascade({
279
475
  phase: "cascade_indeterminate",
280
476
  filePath: files[0] ?? cwd,
@@ -322,8 +518,12 @@ export async function handleTurnEnd(deps) {
322
518
  // run tool checks every turn. Also back off after a timeout/kill so every
323
519
  // agent turn does not spend 30s launching another heavyweight knip process.
324
520
  const prevKnip = cacheManager.readCache("knip", cwd);
521
+ // An availability failure is NOT a hard knip failure: knip never ran, so
522
+ // there is nothing to back off from, and backing off would make an
523
+ // expiring probe verdict permanent again (#1467).
325
524
  const previousFailedHard = prevKnip &&
326
525
  !prevKnip.data.success &&
526
+ !prevKnip.data.failureKind &&
327
527
  /(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prevKnip.data.summary);
328
528
  if (previousFailedHard) {
329
529
  dbg(`turn_end: skipping knip after recent failure: ${prevKnip.data.summary}`);
@@ -331,13 +531,23 @@ export async function handleTurnEnd(deps) {
331
531
  }
332
532
  else {
333
533
  const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns());
334
- cacheManager.writeCache("knip", knipResult, cwd);
534
+ // Never overwrite a good scan with a failure (#925, #1467): the last
535
+ // good result stays until a new successful scan replaces it.
536
+ const knipWouldPoison = wouldPoisonCache(prevKnip, knipResult);
537
+ if (knipWouldPoison) {
538
+ dbg(`turn_end: keeping last good knip cache; this run failed: ${knipResult.summary}`);
539
+ }
540
+ else {
541
+ cacheManager.writeCache("knip", knipResult, cwd);
542
+ }
335
543
  knipMeta = {
336
544
  success: knipResult.success,
337
545
  totalIssues: knipResult.issues.length,
338
546
  newIssues: 0,
339
547
  blockerIssues: 0,
340
548
  ...(!knipResult.success && { reason: knipResult.summary }),
549
+ ...(knipResult.failureKind && { failureKind: knipResult.failureKind }),
550
+ ...(knipWouldPoison && { cacheKept: true }),
341
551
  };
342
552
  if (knipResult.success && knipResult.issues.length > 0) {
343
553
  const issueKey = (i) => `${i.type}:${i.file ?? ""}:${i.name}:${i.line ?? 0}:${i.package ?? ""}`;
@@ -404,6 +614,140 @@ export async function handleTurnEnd(deps) {
404
614
  durationMs: Date.now() - t2,
405
615
  metadata: knipMeta,
406
616
  });
617
+ // Cross-file dead-code (#127) for non-JS/TS languages, on knip's contract:
618
+ // re-scan only when this turn touched a file the client owns, then inject the
619
+ // ATTRIBUTABLE delta — symbols in those files that became unused because of
620
+ // the edit. The project-wide list is deliberately NOT injected per turn (the
621
+ // same reasoning as knip above) and stays available via lens_diagnostics.
622
+ // MUST run before the projectDiagnosticsDelta write below, or a dead-code-only
623
+ // turn would persist nothing.
624
+ const tDeadCode = Date.now();
625
+ const deadCodeMeta = {};
626
+ if (runtime.isStartupScanInFlight("dead-code")) {
627
+ dbg("turn_end: skipping dead-code (startup scan still in flight)");
628
+ deadCodeMeta.skipped = true;
629
+ deadCodeMeta.reason = "startup_scan_in_flight";
630
+ }
631
+ else if (deadCodeClients.length === 0) {
632
+ deadCodeMeta.reason = "no_clients";
633
+ }
634
+ else {
635
+ // The modified-file set costs a resolveRunnerPath per file, and that walks
636
+ // every ancestor to the filesystem root on a miss. Build it lazily, only
637
+ // once a client has actually claimed this project, so an all-JS repo with
638
+ // no dead-code client pays nothing per turn. Knip does the same.
639
+ let modifiedSet = null;
640
+ const modifiedFiles = () => (modifiedSet ??= new Set(files.map((f) => resolveRunnerPath(cwd, f))));
641
+ let newIssueTotal = 0;
642
+ const reasons = [];
643
+ // A malformed client or deps object must never abort turn_end. Before the
644
+ // per-turn delta this block only read a cache; now it iterates and awaits,
645
+ // so the whole thing needs the guard, not just `client.analyze`.
646
+ try {
647
+ for (const client of deadCodeClients) {
648
+ if (!client.detect(cwd)) {
649
+ reasons.push(`${client.id}:not_detected`);
650
+ continue;
651
+ }
652
+ if (![...modifiedFiles()].some((f) => client.owns(f))) {
653
+ reasons.push(`${client.id}:no_owned_files`);
654
+ continue;
655
+ }
656
+ const cacheKey = `dead-code-${client.id}`;
657
+ const prev = cacheManager.readCache(cacheKey, cwd);
658
+ // Back off after a timeout/kill so an unresponsive scanner cannot cost
659
+ // every later turn its full analysis budget (mirrors knip).
660
+ if (prev &&
661
+ !prev.data.success &&
662
+ /(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prev.data.summary)) {
663
+ dbg(`turn_end: skipping dead-code after failure: ${prev.data.summary}`);
664
+ deadCodeMeta.skipped = true;
665
+ reasons.push(`${client.id}:backoff:${prev.data.summary}`);
666
+ continue;
667
+ }
668
+ const startMs = Date.now();
669
+ try {
670
+ const result = await client.analyze(cwd);
671
+ const durationMs = Date.now() - startMs;
672
+ // Never overwrite a good scan with a failure (#925, #1467): a
673
+ // vulture timeout on one .py turn would otherwise evict the
674
+ // session_start scan, and the backoff above would then latch
675
+ // off the poisoned record.
676
+ if (wouldPoisonCache(prev, result)) {
677
+ dbg(`turn_end: keeping last good dead-code(${client.id}) cache; this run failed: ${result.summary}`);
678
+ deadCodeMeta.cacheKept = true;
679
+ }
680
+ else {
681
+ cacheManager.writeCache(cacheKey, result, cwd, {
682
+ scanDurationMs: durationMs,
683
+ });
684
+ }
685
+ // One event per cross-file scan (AGENTS.md) — the per-turn scan is
686
+ // now the primary path, so dead-code.log must see it too.
687
+ logDeadCodeScan({
688
+ language: client.language,
689
+ success: result.success,
690
+ cached: false,
691
+ unusedExports: result.unusedExports.length,
692
+ unusedFiles: result.unusedFiles.length,
693
+ unusedDeps: result.unusedDeps.length,
694
+ unlistedDeps: result.unlistedDeps.length,
695
+ durationMs: result.durationMs ?? durationMs,
696
+ ...(!result.success && { reason: result.summary }),
697
+ });
698
+ deadCodeMeta.success = result.success;
699
+ if (!result.success) {
700
+ reasons.push(`${client.id}:scan_failed:${result.summary}`);
701
+ continue;
702
+ }
703
+ deadCodeMeta.totalIssues =
704
+ (deadCodeMeta.totalIssues ?? 0) + deadCodeIssues(result).length;
705
+ // No baseline means every finding looks new. Report nothing rather
706
+ // than blame the edit for the whole project's pre-existing debt.
707
+ if (!prev?.data.success) {
708
+ reasons.push(`${client.id}:no_previous_scan`);
709
+ continue;
710
+ }
711
+ const prevKeys = new Set(deadCodeIssues(prev.data).map(deadCodeIssueKey));
712
+ const modified = modifiedFiles();
713
+ const newIssues = deadCodeIssues(result).filter((issue) => {
714
+ if (prevKeys.has(deadCodeIssueKey(issue)))
715
+ return false;
716
+ if (!issue.file)
717
+ return false;
718
+ return modified.has(resolveRunnerPath(cwd, issue.file));
719
+ });
720
+ if (newIssues.length === 0) {
721
+ reasons.push(`${client.id}:clean`);
722
+ continue;
723
+ }
724
+ newIssueTotal += newIssues.length;
725
+ projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
726
+ projectDiagnosticsSources.add("dead-code");
727
+ advisoryParts.push(formatDeadCodeDelta(newIssues, result.language));
728
+ }
729
+ catch (err) {
730
+ dbg(`turn_end: dead-code(${client.id}) failed: ${err}`);
731
+ reasons.push(`${client.id}:threw`);
732
+ }
733
+ }
734
+ }
735
+ catch (err) {
736
+ dbg(`turn_end: dead-code block failed: ${err}`);
737
+ reasons.push("block_threw");
738
+ }
739
+ deadCodeMeta.newIssues = newIssueTotal;
740
+ if (reasons.length > 0)
741
+ deadCodeMeta.reason = reasons.join(",");
742
+ }
743
+ logLatency({
744
+ type: "phase",
745
+ toolName: "turn_end",
746
+ filePath: cwd,
747
+ phase: "dead-code",
748
+ durationMs: Date.now() - tDeadCode,
749
+ metadata: deadCodeMeta,
750
+ });
407
751
  // govulncheck — surface session_start-cached Go CVE findings as advisory.
408
752
  // No per-turn re-run in this slice; the cache refreshes at next session_start.
409
753
  const govCacheEntry = cacheManager.readCache("govulncheck", cwd);
@@ -432,11 +776,25 @@ export async function handleTurnEnd(deps) {
432
776
  // location so a committed/hardcoded secret is reported ONCE (with combined
433
777
  // provenance) — a blocker, since credentials need rotation before merge.
434
778
  const gitleaksData = cacheManager.readCache("gitleaks", cwd)?.data;
779
+ // #1461 slice 1 (#1460): the gitleaks cache is TTL-only, so a finding for a
780
+ // file deleted after the scan is still served as a 🔴 blocker for the rest
781
+ // of the 30-minute window — the live case, and 119 of 126 findings in
782
+ // pi-lens's own cache. This read is the single agent-facing consumer of that
783
+ // store (session_start's read only decides whether to re-scan; the
784
+ // project-diagnostics path re-scans fresh and reconciles at load), so the
785
+ // drop belongs here, before the findings enter the shared secret pipeline.
786
+ // gitleaks only in this slice — trivy secrets are slice 1's sibling store.
787
+ const gitleaksFindings = dropFindingsForMissingPaths({
788
+ store: "gitleaks",
789
+ findings: gitleaksData?.findings ?? [],
790
+ cwd,
791
+ citedPath: (finding) => finding.file,
792
+ });
435
793
  const astSecretWarnings = runtime
436
794
  .peekActionableWarnings()
437
795
  .filter(isSecretWarning);
438
796
  const sessionSecrets = dedupeSecretFindings([
439
- ...fromGitleaks(gitleaksData?.findings ?? []),
797
+ ...fromGitleaks(gitleaksFindings),
440
798
  ...fromTrivySecrets(trivyCacheEntry?.data?.secrets ?? []),
441
799
  ]);
442
800
  // Locations already surfaced as session-scan secret blockers — used to enrich
@@ -516,6 +874,7 @@ export async function handleTurnEnd(deps) {
516
874
  advisoryParts.push(report);
517
875
  }
518
876
  const t3 = Date.now();
877
+ let madgeStats;
519
878
  if (await depChecker.ensureAvailable()) {
520
879
  const madgeFiles = cacheManager.getFilesForMadge(cwd);
521
880
  if (madgeFiles.length > 0) {
@@ -524,7 +883,9 @@ export async function handleTurnEnd(deps) {
524
883
  // the shared circular-dep state update is deferred/folded inside
525
884
  // checkFilesBatch so concurrent spawns can't clobber each other (#766).
526
885
  const absFiles = madgeFiles.map((file) => path.resolve(cwd, file));
527
- const depResults = await depChecker.checkFilesBatch(absFiles, cwd);
886
+ const batch = await depChecker.checkFilesBatch(absFiles, cwd);
887
+ const depResults = batch.results;
888
+ madgeStats = batch.stats;
528
889
  for (const file of madgeFiles) {
529
890
  const absPath = path.resolve(cwd, file);
530
891
  const depResult = depResults.get(absPath);
@@ -550,6 +911,7 @@ export async function handleTurnEnd(deps) {
550
911
  filePath: cwd,
551
912
  phase: "madge",
552
913
  durationMs: Date.now() - t3,
914
+ ...(madgeStats && { metadata: madgeStats }),
553
915
  });
554
916
  // --- Test runner: fire once per turn after all edits are done ---
555
917
  // Runs for each unique test target across modified files; results appear
@@ -600,8 +962,33 @@ export async function handleTurnEnd(deps) {
600
962
  if (targets.length > 0) {
601
963
  dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking)`);
602
964
  const firedAtTurn = runtime.turnIndex;
965
+ const firedSessionId = runtime.telemetrySessionId;
966
+ const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
967
+ const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
968
+ const provenanceFiles = [
969
+ ...candidates.map((candidate) => ({ path: candidate.abs, role: "source" })),
970
+ ...targets.map((target) => ({ path: target.testFile, role: "test" })),
971
+ ];
972
+ const launchedFrom = snapshotAdvisoryProvenance({
973
+ cwd,
974
+ runtime,
975
+ generation: testRunGeneration,
976
+ files: provenanceFiles,
977
+ });
978
+ cacheManager.writeCache("test-runner-findings", { ...(priorTestCache ?? { content: "" }), testRunGeneration }, cwd);
603
979
  Promise.allSettled(targets.map((t) => testRunnerClient.runTestFileAsync(t.testFile, cwd, t.runner, t.config)))
604
980
  .then((results) => {
981
+ const publishedAgainst = snapshotAdvisoryProvenance({
982
+ cwd,
983
+ runtime,
984
+ generation: testRunGeneration,
985
+ files: provenanceFiles,
986
+ });
987
+ const superseded = launchedFrom.revision.sessionId !== publishedAgainst.revision.sessionId ||
988
+ launchedFrom.revision.projectSeq !== publishedAgainst.revision.projectSeq ||
989
+ launchedFrom.revision.turnIndex !== publishedAgainst.revision.turnIndex ||
990
+ launchedFrom.files.some((file, index) => publishedAgainst.files[index]?.sha256 !== file.sha256 ||
991
+ publishedAgainst.files[index]?.path !== file.path);
605
992
  // #628: the turn advancing while tests ran no longer means the
606
993
  // results are thrown away — a late result is still real
607
994
  // information about what's currently broken. It's tagged `stale`
@@ -618,9 +1005,29 @@ export async function handleTurnEnd(deps) {
618
1005
  resultValues.push(r.value);
619
1006
  const { file, runner, passed, failed, duration, error } = r.value;
620
1007
  const shortFile = path.basename(file);
1008
+ // #1479: `(0ms)` used to be printed for a run nobody
1009
+ // timed — a payload with no suite timestamps, an
1010
+ // unrecognised summary line, or an empty result — and
1011
+ // that is the same string a genuinely sub-millisecond
1012
+ // run produces. A reader could not tell "measured 0"
1013
+ // from "not measured", which is the confusion #1452 was
1014
+ // reported for. `duration` is now absent when it was
1015
+ // never measured, and this line says which one it has.
1016
+ //
1017
+ // #1480: the test is `formatRunDurationMs`, not an
1018
+ // inline comparison. The "absent = unmeasured" contract
1019
+ // was being re-derived at every site that read a
1020
+ // duration, and a site that gets it slightly wrong —
1021
+ // treating a measured `0` as absent — puts the bug back
1022
+ // without touching this comment.
1023
+ const elapsed = formatRunDurationMs(duration);
1024
+ // Lifted out of the template below for the same reason
1025
+ // `elapsed` is: the pair read as a nested ternary, which
1026
+ // this line only got flagged for because #1479 touched it.
1027
+ const verdict = failed > 0 ? "FAIL" : "PASS";
621
1028
  const summary = error && passed === 0 && failed === 0
622
1029
  ? `error: ${error}`
623
- : `${failed > 0 ? "FAIL" : "PASS"} ${passed}p/${failed}f (${duration}ms)`;
1030
+ : `${verdict} ${passed}p/${failed}f (${elapsed})`;
624
1031
  dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} → ${summary}`);
625
1032
  if (failed > 0) {
626
1033
  const formatted = testRunnerClient.formatResult(r.value);
@@ -629,13 +1036,38 @@ export async function handleTurnEnd(deps) {
629
1036
  }
630
1037
  }
631
1038
  if (failures.length > 0) {
1039
+ const currentGeneration = cacheManager.readCache("test-runner-findings", cwd)?.data?.testRunGeneration;
1040
+ if (currentGeneration !== undefined && currentGeneration > testRunGeneration) {
1041
+ dbg(`turn_end: test generation ${testRunGeneration} superseded by ${currentGeneration}`);
1042
+ return;
1043
+ }
632
1044
  const content = stale
633
1045
  ? `[from a prior turn — the edit that triggered this run had already been superseded by the time results came back]\n\n${failures.join("\n\n")}`
634
1046
  : failures.join("\n\n");
635
- cacheManager.writeCache("test-runner-findings", { content, stale, results: resultValues }, cwd);
1047
+ cacheManager.writeCache("test-runner-findings", {
1048
+ content,
1049
+ stale,
1050
+ results: resultValues,
1051
+ testRunGeneration,
1052
+ launchedFrom,
1053
+ publishedAgainst,
1054
+ provenance: publishedAgainst,
1055
+ superseded,
1056
+ }, cwd);
1057
+ if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
1058
+ clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues
1059
+ .filter((value) => value.failed === 0)
1060
+ .map((value) => value.file));
1061
+ mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, resultValues
1062
+ .filter((value) => value.failed > 0)
1063
+ .map((value) => value.file));
1064
+ }
636
1065
  dbg(`turn_end: ${failures.length} test failure(s) cached for next context injection${stale ? " (stale — turn advanced while tests ran)" : ""}`);
637
1066
  }
638
1067
  else if (results.length > 0) {
1068
+ if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
1069
+ clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues.map((value) => value.file));
1070
+ }
639
1071
  dbg(`turn_end: all tests passed${stale ? " (stale — turn advanced while tests ran)" : ""}`);
640
1072
  }
641
1073
  })
@@ -659,37 +1091,68 @@ export async function handleTurnEnd(deps) {
659
1091
  // would drop the call-graph entries — so lens_diagnostics (which only reads the
660
1092
  // persisted report) would never surface the findings (#179/#533).
661
1093
  if (runtime.callGraph && files.length > 0) {
662
- try {
663
- const { impact, formatImpact } = await import("./call-graph.js");
664
- const { callGraphImpactToProjectDiagnostics } = await import("./project-diagnostics/runner-adapters/call-graph-impact.js");
665
- const impactLines = [];
666
- const impactFindings = [];
667
- for (const filePath of files.slice(0, 5)) {
668
- // Find callee keys for this file in the call graph
669
- const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => k.startsWith(`${filePath}:`));
670
- for (const calleeKey of fileCallerKeys.slice(0, 3)) {
671
- const results = impact(runtime.callGraph, calleeKey);
672
- if (results.length > 0) {
673
- impactFindings.push({ calleeKey, results });
674
- const summary = formatImpact(results, cwd);
675
- if (summary)
676
- impactLines.push(` ${calleeKey.split(":").pop()}: ${summary}`);
1094
+ const coverage = runtime.callGraph.coverage;
1095
+ if (!coverage || coverage.complete !== true) {
1096
+ // An incomplete graph can still contain useful edges, but emitting them
1097
+ // as ordinary impact findings would turn unsupported/partial extraction
1098
+ // into an authoritative-looking clean result for the rest of the file.
1099
+ // Keep the limitation visible and require a complete graph for this
1100
+ // user-facing impact surface (#1070).
1101
+ advisoryParts.push("Call-graph impact was not emitted because call-graph extraction coverage is incomplete; " +
1102
+ "the affected files may have unreported callers.");
1103
+ }
1104
+ else {
1105
+ try {
1106
+ const { impact, formatImpact, parseSymbolKey } = await import("./call-graph.js");
1107
+ const { callGraphImpactToProjectDiagnostics } = await import("./project-diagnostics/runner-adapters/call-graph-impact.js");
1108
+ const impactLines = [];
1109
+ const impactFindings = [];
1110
+ for (const filePath of files.slice(0, 5)) {
1111
+ // Turn-state files may be cwd-relative while graph keys are absolute,
1112
+ // and persisted graphs can contain either slash style/casing. Compare
1113
+ // through the shared normalized path seam; keep the original filePath
1114
+ // only for display and diagnostics.
1115
+ const changedFileKey = normalizeMapKey(resolveRunnerPath(cwd, filePath));
1116
+ const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => {
1117
+ const graphFilePath = parseSymbolKey(k).filePath;
1118
+ return normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) === changedFileKey;
1119
+ });
1120
+ for (const calleeKey of fileCallerKeys.slice(0, 3)) {
1121
+ // #1080: drop KNOWN test-role callers BEFORE both the human advisory
1122
+ // (formatImpact below) and the persisted delta (impactFindings →
1123
+ // callGraphImpactToProjectDiagnostics) — the advisory is rendered
1124
+ // first, so the filter must reach the shared `results` set that feeds
1125
+ // both. A test caller supplied by an old/fixture/expanded graph must
1126
+ // appear in neither surface. Fail-open: an unparseable/unclassifiable
1127
+ // key is retained (the adapter re-applies the same predicate).
1128
+ const results = impact(runtime.callGraph, calleeKey).filter((r) => {
1129
+ const callerFile = parseSymbolKey(r.symbolKey).filePath;
1130
+ return (!callerFile ||
1131
+ !isTestRoleCollateral(resolveRunnerPath(cwd, callerFile)));
1132
+ });
1133
+ if (results.length > 0) {
1134
+ impactFindings.push({ calleeKey, results });
1135
+ const summary = formatImpact(results, cwd);
1136
+ if (summary)
1137
+ impactLines.push(` ${parseSymbolKey(calleeKey).symbolName ?? calleeKey}: ${summary}`);
1138
+ }
677
1139
  }
678
1140
  }
679
- }
680
- if (impactLines.length > 0) {
681
- advisoryParts.push(`📊 Call-graph impact (changed symbols have callers):\n${impactLines.join("\n")}`);
682
- }
683
- if (impactFindings.length > 0) {
684
- const impactDiagnostics = callGraphImpactToProjectDiagnostics(cwd, impactFindings);
685
- if (impactDiagnostics.length > 0) {
686
- projectDiagnosticsDelta.push(...impactDiagnostics);
687
- projectDiagnosticsSources.add("call-graph");
1141
+ if (impactLines.length > 0) {
1142
+ advisoryParts.push(`📊 Call-graph impact (changed symbols have callers):\n${impactLines.join("\n")}`);
1143
+ }
1144
+ if (impactFindings.length > 0) {
1145
+ const impactDiagnostics = callGraphImpactToProjectDiagnostics(cwd, impactFindings);
1146
+ if (impactDiagnostics.length > 0) {
1147
+ projectDiagnosticsDelta.push(...impactDiagnostics);
1148
+ projectDiagnosticsSources.add("call-graph");
1149
+ }
688
1150
  }
1151
+ // Non-fatal — call graph is best-effort
1152
+ }
1153
+ catch {
1154
+ // Non-fatal — call graph is best-effort
689
1155
  }
690
- }
691
- catch {
692
- // Non-fatal — call graph is best-effort
693
1156
  }
694
1157
  }
695
1158
  if (projectDiagnosticsDelta.length > 0) {
@@ -818,27 +1281,7 @@ export async function handleTurnEnd(deps) {
818
1281
  },
819
1282
  });
820
1283
  }
821
- // Cross-file dead-code (#127): surface the cached session_start scan as an
822
- // advisory (project-wide unused symbols are slow to compute, so we read the
823
- // cache rather than re-scanning every turn — the analogue of knip's cache
824
- // for non-JS/TS languages). Merged across languages for polyglot repos.
825
- try {
826
- const deadCodeResults = [];
827
- for (const client of deadCodeClients) {
828
- if (!client.detect(cwd))
829
- continue;
830
- const cached = cacheManager.readCache(`dead-code-${client.id}`, cwd);
831
- if (cached?.data)
832
- deadCodeResults.push(cached.data);
833
- }
834
- const advisory = formatDeadCodeAdvisory(deadCodeResults);
835
- if (advisory)
836
- advisoryParts.push(advisory);
837
- }
838
- catch (err) {
839
- dbg(`turn_end: dead-code advisory failed: ${err}`);
840
- }
841
- cacheManager.incrementTurnCycle(cwd);
1284
+ cacheManager.incrementTurnCycle(cwd, currentOwner);
842
1285
  const labeledAdvisoryParts = advisoryParts.map((p) => `ℹ️ Advisory — no action required this turn:\n${p}`);
843
1286
  const findingParts = [...blockerParts, ...labeledAdvisoryParts];
844
1287
  if (findingParts.length > 0) {
@@ -852,12 +1295,88 @@ export async function handleTurnEnd(deps) {
852
1295
  if (last?.data?.signature === signature &&
853
1296
  last?.data?.sessionId === runtime.telemetrySessionId) {
854
1297
  dbg("turn_end: duplicate findings detected (same session), suppressing re-prompt");
855
- cacheManager.clearTurnState(cwd);
1298
+ if (getFlag("lens-guard")) {
1299
+ const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
1300
+ if (existingGuard) {
1301
+ writeGitGuardRecord(cacheManager, runtime, cwd, {
1302
+ ...existingGuard,
1303
+ content,
1304
+ blockerContent: blockerParts.length > 0
1305
+ ? capTurnEndMessage(blockerParts.join("\n\n"))
1306
+ : undefined,
1307
+ hasBlockers: blockerParts.length > 0 || existingGuard.testFailures === true,
1308
+ blockingFiles: blockerParts.length > 0 ? existingGuard.affectedFiles : undefined,
1309
+ projectSeqStart: runtime.turnStartProjectSeq,
1310
+ projectSeqEnd: runtime.projectSeq,
1311
+ fileSeqByPath: Object.fromEntries(runtime.getFileSeqEntries().map(([filePath, seq]) => [normalizeMapKey(path.resolve(filePath)), seq])),
1312
+ fileContentHashes: {},
1313
+ consumed: false,
1314
+ });
1315
+ }
1316
+ }
1317
+ cacheManager.clearTurnState(cwd, currentOwner);
856
1318
  runtime.fixedThisTurn.clear();
857
1319
  resetFormatService();
858
1320
  return;
859
1321
  }
860
- cacheManager.writeCache("turn-end-findings", { content }, cwd);
1322
+ const fileSeqByPath = {};
1323
+ for (const [filePath, seq] of runtime.getFileSeqEntries()) {
1324
+ fileSeqByPath[normalizeMapKey(path.resolve(filePath))] = seq;
1325
+ }
1326
+ if (getFlag("lens-guard")) {
1327
+ const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
1328
+ const blockingContent = blockerParts.length > 0
1329
+ ? capTurnEndMessage(blockerParts.join("\n\n"))
1330
+ : undefined;
1331
+ const affectedFiles = [
1332
+ ...(existingGuard?.affectedFiles ?? []),
1333
+ ...files.map((file) => resolveRunnerPath(cwd, file)),
1334
+ ...cascadeResults.flatMap((result) => result.neighbors
1335
+ .filter((neighbor) => neighbor.diagnostics.length > 0)
1336
+ .map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
1337
+ ];
1338
+ writeGitGuardRecord(cacheManager, runtime, cwd, {
1339
+ content: [content, existingGuard?.testFailureContent]
1340
+ .filter((value) => !!value)
1341
+ .join("\n\n"),
1342
+ blockerContent: blockingContent,
1343
+ blockingFiles: blockerParts.length > 0 ? affectedFiles : undefined,
1344
+ hasBlockers: !!blockingContent || existingGuard?.testFailures === true,
1345
+ affectedFiles,
1346
+ sessionId: runtime.telemetrySessionId,
1347
+ projectSeqStart: runtime.turnStartProjectSeq,
1348
+ projectSeqEnd: runtime.projectSeq,
1349
+ fileSeqByPath,
1350
+ fileContentHashes: {},
1351
+ consumed: false,
1352
+ testFailures: existingGuard?.testFailures,
1353
+ testFailureContent: existingGuard?.testFailureContent,
1354
+ testFailureFiles: existingGuard?.testFailureFiles,
1355
+ });
1356
+ }
1357
+ else {
1358
+ const allAffectedFiles = [
1359
+ ...files.map((file) => resolveRunnerPath(cwd, file)),
1360
+ ...cascadeResults.flatMap((result) => result.neighbors
1361
+ .filter((neighbor) => neighbor.diagnostics.length > 0)
1362
+ .map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
1363
+ ];
1364
+ const affectedFiles = [...new Set(allAffectedFiles)]
1365
+ .slice(0, MAX_ADVISORY_AFFECTED_FILES);
1366
+ const affectedFilesTruncated = new Set(allAffectedFiles).size > affectedFiles.length;
1367
+ cacheManager.writeCache("turn-end-findings", {
1368
+ content,
1369
+ affectedFiles,
1370
+ affectedFilesTruncated,
1371
+ provenance: snapshotAdvisoryProvenance({
1372
+ cwd,
1373
+ runtime,
1374
+ generation: 0,
1375
+ files: affectedFiles.map((file) => ({ path: file, role: "affected" })),
1376
+ truncated: affectedFilesTruncated,
1377
+ }),
1378
+ }, cwd);
1379
+ }
861
1380
  cacheManager.writeCache("turn-end-findings-last", {
862
1381
  signature,
863
1382
  sessionId: runtime.telemetrySessionId,
@@ -875,7 +1394,14 @@ export async function handleTurnEnd(deps) {
875
1394
  });
876
1395
  }
877
1396
  if (blockerParts.length === 0) {
878
- cacheManager.clearTurnState(cwd);
1397
+ cacheManager.clearTurnState(cwd, currentOwner);
1398
+ if (getFlag("lens-guard") && advisoryParts.length === 0 && !runtime.gitGuardHasBlockers) {
1399
+ const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
1400
+ if (guardRecord?.sessionId === runtime.telemetrySessionId &&
1401
+ guardRecord.testFailures !== true) {
1402
+ cacheManager.clearCache("turn-end-findings", cwd);
1403
+ }
1404
+ }
879
1405
  }
880
1406
  runtime.fixedThisTurn.clear();
881
1407
  runtime.clearActionableWarnings();