@herbertgao/pi-extensions 2026.8.6 → 2026.8.7

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 (343) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +272 -22
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
  15. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  16. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  17. package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
  18. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
  19. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
  22. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
  26. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
  27. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
  29. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
  30. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
  31. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
  32. package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
  33. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +1 -0
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +26 -10
  36. package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
  37. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
  38. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +2 -1
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
  49. package/node_modules/@narumitw/pi-btw/README.md +120 -111
  50. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  51. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  52. package/node_modules/@narumitw/pi-btw/package.json +10 -6
  53. package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
  54. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  55. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  56. package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
  57. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  58. package/node_modules/pi-lens/CHANGELOG.md +909 -0
  59. package/node_modules/pi-lens/README.md +11 -3
  60. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  61. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  62. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
  63. package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
  64. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  65. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  66. package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
  67. package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
  68. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  69. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
  70. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  71. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  72. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
  73. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  74. package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
  75. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  76. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  77. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  78. package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
  79. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  80. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  81. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
  82. package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
  83. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
  84. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
  86. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
  87. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  122. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  123. package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
  124. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  125. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
  126. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  127. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  128. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  129. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  130. package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
  131. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  134. package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
  135. package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
  136. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  137. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  138. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  139. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  140. package/node_modules/pi-lens/dist/clients/jscpd-client.js +82 -6
  141. package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
  142. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  143. package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
  144. package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
  145. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  146. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  147. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  148. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +102 -7
  149. package/node_modules/pi-lens/dist/clients/lsp/client.js +1516 -159
  150. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
  151. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  152. package/node_modules/pi-lens/dist/clients/lsp/index.js +2274 -147
  153. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  154. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  155. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  156. package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
  157. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  158. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  159. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
  160. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
  161. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  162. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  163. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  164. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  165. package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
  166. package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
  167. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  168. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  169. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  170. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  171. package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
  172. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  173. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  174. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  175. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
  176. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  177. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  178. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
  179. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  180. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  181. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  182. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  183. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
  185. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  186. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  187. package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
  188. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
  189. package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
  190. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +308 -45
  191. package/node_modules/pi-lens/dist/clients/runtime-session.js +332 -9
  192. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  193. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
  194. package/node_modules/pi-lens/dist/clients/runtime-turn.js +575 -67
  195. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  196. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  197. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  198. package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
  199. package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
  200. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  201. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  202. package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
  203. package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
  204. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  205. package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  210. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  211. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  212. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  213. package/node_modules/pi-lens/dist/index.js +67738 -55739
  214. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +466 -54
  215. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
  216. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  217. package/node_modules/pi-lens/docs/agent-guide.md +6 -1
  218. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
  219. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  220. package/node_modules/pi-lens/docs/features.md +19 -5
  221. package/node_modules/pi-lens/docs/globalconfig.md +3 -0
  222. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  223. package/node_modules/pi-lens/docs/mcp.md +2 -2
  224. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  225. package/node_modules/pi-lens/docs/settings.md +19 -1
  226. package/node_modules/pi-lens/docs/usage.md +1 -0
  227. package/node_modules/pi-lens/package.json +10 -6
  228. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  232. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
  233. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  234. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  235. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  236. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  237. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  238. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  239. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
  240. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
  241. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  252. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  253. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  254. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  255. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  256. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  257. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  258. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  288. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
  289. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  290. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  291. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
  292. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
  293. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
  294. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  295. package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
  296. package/node_modules/pi-mcp-adapter/README.md +47 -5
  297. package/node_modules/pi-mcp-adapter/cli.js +163 -1
  298. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  299. package/node_modules/pi-mcp-adapter/config.ts +25 -7
  300. package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
  301. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  302. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  303. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  304. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  305. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  306. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  307. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  308. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  309. package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
  310. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  311. package/node_modules/pi-mcp-adapter/package.json +3 -1
  312. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  313. package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
  314. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  315. package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
  316. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  317. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  318. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  319. package/node_modules/pi-mcp-adapter/types.ts +2 -0
  320. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  321. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  322. package/node_modules/pi-web-access/CHANGELOG.md +23 -0
  323. package/node_modules/pi-web-access/README.md +27 -9
  324. package/node_modules/pi-web-access/anysearch.ts +4 -2
  325. package/node_modules/pi-web-access/brave.ts +16 -4
  326. package/node_modules/pi-web-access/curator-page.ts +6 -3
  327. package/node_modules/pi-web-access/curator-server.ts +4 -1
  328. package/node_modules/pi-web-access/exa.ts +18 -7
  329. package/node_modules/pi-web-access/extract.ts +28 -3
  330. package/node_modules/pi-web-access/gemini-search.ts +16 -4
  331. package/node_modules/pi-web-access/index.ts +41 -57
  332. package/node_modules/pi-web-access/package.json +1 -1
  333. package/node_modules/pi-web-access/page-query.ts +4 -2
  334. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  335. package/node_modules/pi-web-access/parallel.ts +2 -2
  336. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  337. package/node_modules/pi-web-access/serper.ts +211 -0
  338. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  339. package/node_modules/pi-web-access/summary-review.ts +44 -22
  340. package/node_modules/pi-web-access/tavily.ts +16 -4
  341. package/node_modules/pi-web-access/utils.ts +87 -0
  342. package/node_modules/pi-web-access/valyu.ts +199 -0
  343. package/package.json +13 -13
@@ -9,14 +9,21 @@ const writer = createNdjsonLogger({
9
9
  filePath: LATENCY_LOG_FILE,
10
10
  maxBytes: getMaxLogSizeMB() * 1024 * 1024,
11
11
  });
12
+ /** Bound on the `recentPhases` ring below — keeps the attribution record small. */
13
+ export const RECENT_PHASE_CAP = 5;
12
14
  /**
13
- * Most recent non-`loop_block` phase seen by `logLatency`, for cheap block
14
- * attribution (#1122 / #1123 item 1): the event-loop-block probe fires at
15
- * turn_end and cannot see *what* stalled the loop, so it stamps the last phase
16
- * that ran as a starting point for root-causing a genuine block. Tracked before
17
- * the test-mode guard so it is deterministic and unit-testable.
15
+ * Most recent non-`loop_block` phases seen by `logLatency`, newest first, for
16
+ * cheap block attribution (#1122 / #1123 item 1, widened #1723): the
17
+ * event-loop-block probe fires at turn_end and cannot see *what* stalled the
18
+ * loop, so it stamps the phases that ran as a starting point for root-causing
19
+ * a genuine block. A single `lastPhase` pointer names only the phase that
20
+ * finished right before the block — useless when the CPU hog itself never got
21
+ * to log a completion (it was busy blocking the loop). Keeping a short,
22
+ * bounded ring instead gives the block's neighborhood, not just one frame of
23
+ * it, while staying O(1) per call and fixed-size in memory. Tracked before the
24
+ * test-mode guard so it is deterministic and unit-testable.
18
25
  */
19
- let lastPhase;
26
+ let recentPhases = [];
20
27
  /**
21
28
  * Phases excluded from `lastPhase` attribution alongside `loop_block`.
22
29
  * #1412 L3: `lsp_typescript_project_identity` is the classic-TS first-open
@@ -33,8 +40,14 @@ let lastPhase;
33
40
  * zero-duration siblings of the same shape, added alongside them.
34
41
  *
35
42
  * `lsp_aux_wait_outcome` (#1458) is DIFFERENT from every entry above: its
36
- * `durationMs` is a REAL bounded wait (the post-primary auxiliary grace, up
37
- * to a few seconds), not zero-duration decision telemetry. It is still
43
+ * `durationMs` is a REAL bounded wait, not zero-duration decision telemetry.
44
+ * WHICH wait depends on the row's `waitShape` (#1533), so read that field before
45
+ * comparing two rows: `"aux_grace"` measures only the post-primary auxiliary
46
+ * grace (up to a few seconds), while `"aggregate"` — the `clientScope: "all"`
47
+ * producer, which arms no grace of its own — measures the whole diagnostics wait
48
+ * from before the primaries settled. The `"aggregate"` number is therefore
49
+ * systematically larger for the same auxiliary; it is not a regression. It is
50
+ * still
38
51
  * excluded because it is a WAIT-OUTCOME RECORD written after the aux wait
39
52
  * already completed, not the stall itself — that wait ran inside the
40
53
  * `lsp_touch_file`/diagnostics phase surrounding it, so letting this summary
@@ -51,7 +64,15 @@ let lastPhase;
51
64
  *
52
65
  * #1461 slice 1: `finding_dead_path_drop` is the same shape — the record a
53
66
  * delivery seam writes when it drops findings whose cited path no longer
54
- * exists.
67
+ * exists. #1622 adds `finding_stale_line_demote`, its sibling for findings
68
+ * whose cited file was edited after the scan.
69
+ *
70
+ * #1459: the three scanner-coverage records are summary rows written from inside
71
+ * `lsp_touch_file`, and two of them carry ANOTHER write's age as `durationMs`
72
+ * (`lsp_notify_resync_deferred`, `lsp_notify_write_late_landed`) rather than
73
+ * their own work. `lsp_scanner_coverage_gap` reports the touch's elapsed time for
74
+ * joinability. Letting any of them win `lastPhase` would attribute a `loop_block`
75
+ * to the record instead of to the touch that is actually stalled.
55
76
  */
56
77
  const LAST_PHASE_EXCLUDED = new Set([
57
78
  "loop_block",
@@ -65,6 +86,12 @@ const LAST_PHASE_EXCLUDED = new Set([
65
86
  "tool_set_mutation",
66
87
  "availability_decision",
67
88
  "finding_dead_path_drop",
89
+ "finding_stale_line_demote",
90
+ "lsp_scanner_coverage_gap",
91
+ "lsp_notify_resync_deferred",
92
+ "lsp_notify_write_late_landed",
93
+ "degradation_ledger",
94
+ "path_attribution_verified_rollup",
68
95
  ]);
69
96
  /**
70
97
  * The last non-`loop_block` phase logged, or undefined if none yet. Carries its
@@ -74,12 +101,414 @@ const LAST_PHASE_EXCLUDED = new Set([
74
101
  * trusting it as the cause (it is a breadcrumb, not proof).
75
102
  */
76
103
  export function getLastLoggedPhase() {
77
- return lastPhase;
104
+ return recentPhases[0];
105
+ }
106
+ /**
107
+ * The last `limit` non-`loop_block` phases logged, newest first (#1723).
108
+ * Bounded to `RECENT_PHASE_CAP` regardless of `limit` — a caller cannot
109
+ * request an unbounded ring. Same staleness caveat as `getLastLoggedPhase`:
110
+ * these are breadcrumbs from whatever ran most recently, not proof of cause.
111
+ */
112
+ export function getRecentLoggedPhases(limit = RECENT_PHASE_CAP) {
113
+ return recentPhases.slice(0, Math.min(limit, RECENT_PHASE_CAP));
114
+ }
115
+ /**
116
+ * Test-only: the ring's actual storage length, bypassing the read-side
117
+ * `Math.min` clamp in `getRecentLoggedPhases` entirely. Needed because that
118
+ * clamp and the write-side `.slice(0, RECENT_PHASE_CAP)` in `logLatency` are
119
+ * a compensating pair — as long as ONE of them is intact, the other's
120
+ * deletion is invisible to any test that only observes
121
+ * `getRecentLoggedPhases()` output length. This pins the write-side guard
122
+ * specifically: if its `.slice` is deleted, storage grows past the cap and
123
+ * this returns the unclamped size regardless of what the read side does.
124
+ */
125
+ export function _recentPhasesStorageLengthForTest() {
126
+ return recentPhases.length;
127
+ }
128
+ /**
129
+ * Test-only: seed the ring directly, bypassing the write-side cap in
130
+ * `logLatency`. Lets a test put the ring in a state the normal write path can
131
+ * never produce (more than `RECENT_PHASE_CAP` entries), so the read-side
132
+ * clamp in `getRecentLoggedPhases` can be pinned independently of the
133
+ * write-side guard (see `_recentPhasesStorageLengthForTest` above for why
134
+ * that independence matters).
135
+ */
136
+ export function _setRecentPhasesForTest(entries) {
137
+ recentPhases = entries;
138
+ }
139
+ /** Bound on the closed-bracket ring below — same size discipline as `recentPhases`. */
140
+ export const CLOSED_BRACKET_CAP = RECENT_PHASE_CAP;
141
+ /**
142
+ * Phases currently executing, keyed by their own start token (#1723 review
143
+ * round: this replaces an earlier single-slot design that broke two ways.
144
+ * `dispatchForFile` runs its runner groups in PARALLEL (`Promise.all`,
145
+ * dispatcher.ts), so overlap is the NORMAL case, not an edge case:
146
+ * F1 — a single slot holds only the LAST starter. A cheap idle runner
147
+ * starting after a CPU hog wins the slot outright; a block gets
148
+ * attributed to the innocent idle runner while the hog stays
149
+ * anonymous.
150
+ * F2 — the inverse: a quick sibling that starts SECOND but FINISHES FIRST
151
+ * clears the slot the moment its own `finally` runs, wiping out the
152
+ * still-running long phase's attribution — the identity-token check
153
+ * only guarded the OTHER stale-clear direction (an EARLIER phase's
154
+ * late finish clobbering a LATER phase), not this one.
155
+ * A `Map` keyed by the token itself (not by phase name — two calls can share
156
+ * a name) fixes both: every bracket owns its own entry regardless of what
157
+ * else is live, and `phaseFinished` only ever removes ITS OWN entry. `Map`
158
+ * iteration order is insertion order, so the oldest surviving entry — read by
159
+ * `getCurrentPhase` — is the longest-running phase still open: the one most
160
+ * likely to own a long block.
161
+ */
162
+ const liveBrackets = new Map();
163
+ /**
164
+ * Recently CLOSED brackets, newest first, bounded to `CLOSED_BRACKET_CAP`
165
+ * (#1723 review round, F3 — the decisive finding). `phaseFinished` runs
166
+ * inside a `finally`, which resumes as a MICROTASK, while the host schedules
167
+ * `turn_end` as a MACROTASK — and microtasks always fully drain before the
168
+ * next macrotask runs. For the motivating SYNCHRONOUS block, this means the
169
+ * offending phase has ALREADY closed (its `finally` already ran) by the time
170
+ * `turn_end` gets to sample: `liveBrackets` alone reads EMPTY for exactly the
171
+ * case this feature exists to catch. Keeping a short history of recently
172
+ * closed brackets lets `getPhaseForWindow` below check whether a block's time
173
+ * window overlaps a bracket that already closed, not only ones still open.
174
+ */
175
+ let closedBrackets = [];
176
+ /**
177
+ * Mark a phase as started. Returns a token the caller must pass back to
178
+ * `phaseFinished` — the token itself is the map key, so it identifies this
179
+ * bracket uniquely even against a same-named sibling running concurrently.
180
+ * Cost per call: one object allocation, one `Date.now()`/`toISOString()`
181
+ * call, and one `Map.set` (O(1), no scan of any collection) — negligible
182
+ * next to the runner/phase work it brackets. Measured against this PR's own
183
+ * call site (dispatcher.ts's per-runner `runRunner`): a native ast-grep or
184
+ * tree-sitter scan, or a subprocess spawn, is orders of magnitude more
185
+ * expensive than one map insert.
186
+ */
187
+ export function phaseStarted(phase) {
188
+ const token = { phase, startedAt: new Date().toISOString() };
189
+ liveBrackets.set(token, true);
190
+ return token;
191
+ }
192
+ /**
193
+ * Close a bracket: removes it from `liveBrackets` (one `Map.delete`, O(1))
194
+ * and records it on the closed-bracket ring, bounded to `CLOSED_BRACKET_CAP`
195
+ * with the oldest entry dropped first — see the `closedBrackets` doc comment
196
+ * above for why a closed history is load-bearing, not just nice-to-have
197
+ * (#1723 review F3). `Map.delete` reports whether it actually removed
198
+ * anything, so a stale or duplicate `phaseFinished` call (the token isn't
199
+ * live — already closed, or never was) is a no-op and can't push a phantom
200
+ * entry onto the closed ring.
201
+ *
202
+ * Callers MUST pair this with `phaseStarted` via try/finally — an exception
203
+ * or an abandoned promise that skips this call leaves a phantom entry in
204
+ * `liveBrackets` forever, silently misattributing every LATER loop_block to
205
+ * stale work (catalog: a bracket that must clear on finish, never left to a
206
+ * process-lifetime latch). `resetCurrentPhaseForSession` is the session-
207
+ * boundary backstop for the case where even try/finally doesn't run (a torn-
208
+ * down activation whose in-flight phase never gets a chance to unwind).
209
+ *
210
+ * #1723 review round 3 (N3) — the honest residual: a bracket leaked this way
211
+ * (e.g. a torn-down concurrent secondary, see `resetCurrentPhaseForSession`)
212
+ * has no age or size cap of its own and sits in `liveBrackets` until the next
213
+ * FULL session start clears it. It does NOT go unnoticed in the meantime: a
214
+ * leaked bracket is old by construction, so its `elapsedMs` is large, and
215
+ * `getPhaseForWindow`'s tie-break below prefers the SMALLER-`elapsedMs`
216
+ * candidate on an overlap tie — a genuine culprit whose own lifetime roughly
217
+ * matches the block window beats a stale bracket that merely contains it.
218
+ * The leak is real; it is demoted, not silently trusted.
219
+ */
220
+ export function phaseFinished(token) {
221
+ if (!liveBrackets.delete(token))
222
+ return;
223
+ closedBrackets = [
224
+ { phase: token.phase, startedAt: token.startedAt, closedAt: new Date().toISOString() },
225
+ ...closedBrackets,
226
+ ].slice(0, CLOSED_BRACKET_CAP);
227
+ }
228
+ /**
229
+ * The OLDEST phase still executing, if any — the longest-running open
230
+ * bracket. #1723 review round 3: this has NO production consumer — `turn_end`
231
+ * (index.ts) reads `getPhaseForWindow` below instead, since this function
232
+ * alone cannot see a phase that already closed, which is exactly what
233
+ * happens for a synchronous block by the time anything samples it (F3).
234
+ * Kept deliberately as a TEST SEAM: several regression tests assert "which
235
+ * phase is currently open" directly, without constructing a window, which is
236
+ * simpler to read and still exercises the same `liveBrackets` map the
237
+ * production path shares. Remove if it stops earning its keep.
238
+ */
239
+ export function getCurrentPhase() {
240
+ const oldest = liveBrackets.keys().next();
241
+ return oldest.done ? undefined : oldest.value;
242
+ }
243
+ /**
244
+ * A candidate bracket is ignored outright if its OWN lifetime (`elapsedMs`)
245
+ * is under this fraction of the window's length (#1723 review round 3, N4).
246
+ * Overlap alone is not enough: the bounded closed-bracket ring can churn a
247
+ * real culprit out (busy siblings filling `CLOSED_BRACKET_CAP`), leaving only
248
+ * a 1ms bracket that happens to have SOME positive overlap with an 18-second
249
+ * window — reporting it would be a CONFIDENT WRONG ANSWER, worse than no
250
+ * answer. 5%: a genuine cause's own duration should be a meaningful fraction
251
+ * of the block it explains, not a sliver of it. Below that, absent-but-honest
252
+ * (return `undefined`) beats present-but-wrong.
253
+ */
254
+ const MIN_PLAUSIBLE_ELAPSED_FRACTION = 0.05;
255
+ /**
256
+ * Attribute a block's time window `[windowStartMs, windowEndMs]` to whichever
257
+ * bracket — live OR recently closed — best explains it (#1723 review, F3:
258
+ * the decisive finding). A live bracket's end, for overlap purposes, is
259
+ * "now" (it is still running); a closed bracket's end is its own `closedAt`.
260
+ *
261
+ * Ranked by CONTAINMENT FRACTION (`overlapMs / max(elapsedMs, windowLengthMs)`),
262
+ * not raw overlap (#1723 review round 4, N1 — after round 3's elapsedMs
263
+ * tie-break turned out not to engage in practice). Raw overlap is capped at
264
+ * the window's own length, and a LIVE bracket's end is always `nowMs` — the
265
+ * window's own end — so a long-lived, still-running INNOCENT bracket always
266
+ * scores the maximum possible raw overlap. A genuine CLOSED culprit can only
267
+ * match that by closing at the exact sample instant, which `turn_end` never
268
+ * does: it samples milliseconds AFTER the culprit's `finally` runs, so the
269
+ * culprit's raw overlap is always slightly short of the window's full length
270
+ * — round 3's "exact tie" tie-break never engaged, and the innocent bracket
271
+ * won every time on raw overlap alone. Fraction fixes this structurally: a
272
+ * culprit whose own lifetime roughly IS the window scores close to 1.0
273
+ * regardless of a few milliseconds of sampling lag, while a long-lived
274
+ * innocent bracket that merely CONTAINS the window scores meaningfully
275
+ * lower (its `elapsedMs` denominator is bigger).
276
+ *
277
+ * The denominator is `max(elapsedMs, windowLengthMs)`, not bare `elapsedMs`
278
+ * (#1723 review round 5, R1 — the fraction metric's own blind spot). A bare
279
+ * `elapsedMs` denominator meant a SHORT bracket wholly INSIDE the window
280
+ * scored a perfect, undeserved 1.0 (`overlapMs === elapsedMs` whenever a
281
+ * bracket never spills past either window edge) and beat a real culprit
282
+ * whose bracket spans slightly MORE than the window — e.g. a 1s innocent
283
+ * bracket fully inside an 18.27s window outscored a genuine 19.27s-lifetime
284
+ * culprit that explains the whole window and then some. Using the WINDOW's
285
+ * length as a floor on the denominator caps the achievable fraction at
286
+ * `overlapMs / windowLengthMs` for any bracket no bigger than the window
287
+ * itself, so a small bracket can no longer out-rank a big one purely by
288
+ * being small; a bracket LARGER than the window still divides by its own
289
+ * `elapsedMs`, so the round-4 sampling-lag fix (a big bracket's fraction
290
+ * tracks how much of ITS OWN lifetime overlaps) is unaffected. Free
291
+ * improvement noted while verifying this: it also further demotes an N3
292
+ * leaked bracket when something is competing against it (its `elapsedMs`
293
+ * denominator, already large, only grows).
294
+ *
295
+ * The round-3 elapsedMs comparison is kept as the tie-break for a genuine
296
+ * FRACTION tie (two candidates scoring identically) — still reachable under
297
+ * this metric: two brackets that both partially overlap the window by the
298
+ * SAME amount (`overlapMs` equal) but have different lifetimes both share the
299
+ * `windowLengthMs` denominator (since both are ≤ the window), so their
300
+ * fractions tie while their `elapsedMs` differs. See
301
+ * `tests/clients/latency-logger.test.ts` for a constructed example.
302
+ *
303
+ * The tie-break compares within an EPSILON band (`FRACTION_TIE_EPSILON` =
304
+ * `1e-4`), not exact `===` equality (#1723 review round 6, R3). Co-starting
305
+ * — two brackets opening with the identical `startedAt` millisecond string
306
+ * — is the NORMAL path, not an edge case: `dispatchForFile` launches its
307
+ * runner groups via `Promise.all`, and a runner's `when` precondition awaits
308
+ * only microtasks, so this happens routinely. Closed form for a culprit
309
+ * that closes `lag` ms before the sample versus a co-started bracket that
310
+ * stays live: the culprit scores `1 - lag/W`, the co-started bracket scores
311
+ * `1/(1 + lag/W)` — the co-started bracket wins by `(lag/W)^2`, which at a
312
+ * real `W` = 18 270ms and `lag` = 5ms is ~7e-8 — far below millisecond
313
+ * timestamp resolution, so an EXACT tie never actually engages and the
314
+ * "winner" becomes floating-point rounding noise instead. `1e-4` is well
315
+ * above any realistic `(lag/W)^2` term (a co-started pair collapses into it)
316
+ * and well below the gap between two genuinely different candidates in every
317
+ * test/probe this module has been checked against.
318
+ *
319
+ * The tie-break is a TWO-PASS selection over every candidate, not a
320
+ * single-pass streaming comparison (#1723 review round 7, S1 — a blocker).
321
+ * A single running `bestFraction`, reassigned on every accepted candidate
322
+ * INCLUDING a tie-win, chains: "near" is not transitive, so three candidates
323
+ * at fractions 0.600000 / 0.600080 / 0.600160 (adjacent gaps inside the
324
+ * epsilon band, but the OUTER gap between the first and third outside it)
325
+ * let the running best walk across the whole band one hop at a time — the
326
+ * final answer then depends on SCAN ORDER, reachable at roughly 1.5ms of
327
+ * overlap difference in an 18s window, which is routine under `Promise.all`
328
+ * co-start with three or more candidates. Reseeding `bestFraction` via
329
+ * `Math.max(...)` alone does not fix this — the defect is the chaining
330
+ * itself, not the seed value. The fix: pass 1 finds the maximum fraction
331
+ * across ALL candidates (a plain reduction, immune to order); pass 2 keeps
332
+ * only the candidates within the epsilon band OF THAT MAXIMUM — not of each
333
+ * other, and not of a running value that could itself have drifted — and
334
+ * picks the smallest `elapsedMs` among them. Order-independent by
335
+ * construction: the three-candidate example above always excludes the
336
+ * first candidate (its gap from the true maximum exceeds the epsilon, even
337
+ * though its gap from its immediate neighbor does not), regardless of scan
338
+ * order.
339
+ *
340
+ * Composes with N4's plausibility floor below in the natural order: the
341
+ * floor FILTERS candidates (an implausibly short bracket is never even a
342
+ * candidate for fraction ranking, and never enters the `candidates` array at
343
+ * all); fraction then RANKS the survivors via the two passes above. A
344
+ * bracket that clears the floor always has a well-defined fraction in (0, 1].
345
+ *
346
+ * A candidate needs a STRICTLY POSITIVE overlap: `if (fraction <= 0) return;`
347
+ * enforces that explicitly, before the candidate is even collected — pass 1's
348
+ * `maxFraction` is seeded at 0 and only ever updated by a STRICTLY GREATER
349
+ * value, so without this guard a lone zero-or-negative-fraction candidate
350
+ * would sit "within the epsilon band of the seeded 0" in pass 2 and be
351
+ * wrongly attributed. #1723 review N2 deleted an EARLIER version of this
352
+ * guard as dead code under the round-2 single-pass design, where a strict
353
+ * `>` comparison against a 0-seeded running best already excluded it with no
354
+ * help needed — that reasoning does not carry over to the two-pass shape,
355
+ * where pass 2's job is specifically to find candidates NEAR a seed value.
356
+ *
357
+ * This is the seam that makes the SYNCHRONOUS motivating case attributable at
358
+ * all: the offending phase's `finally` (a microtask) always runs before
359
+ * `turn_end` (a macrotask) samples, so by the time this is called the
360
+ * bracket has typically already moved from `liveBrackets` to
361
+ * `closedBrackets` — checking only the live set reads empty for exactly that
362
+ * case, which is why `getCurrentPhase` alone was not enough.
363
+ *
364
+ * KNOWN RESIDUAL (#1723 review round 4, stated not fixed): a leaked bracket
365
+ * (see `phaseFinished`'s doc comment on N3) with NO competing candidate still
366
+ * wins any window it fully contains, however many times over its own
367
+ * lifetime exceeds the window — fraction ranking only demotes a leak when
368
+ * something else is competing for the same window. A ceiling on elapsedMs
369
+ * relative to the window length (e.g. reject a candidate whose lifetime is
370
+ * some large multiple of the window) would close this, at the cost of a
371
+ * second magic number; left open pending a concrete false-positive to size
372
+ * it against.
373
+ *
374
+ * Caveat for an automated consumer (e.g. #1549's loop_block ↔
375
+ * lsp_diagnostics_timeout correlation): the window's own end is `now` at
376
+ * SAMPLE time, which assumes the block ended right when `turn_end` read
377
+ * `getEventLoopStats()` — a reasonable approximation, not a measured fact.
378
+ * `startedAt`/`elapsedMs`/`stillRunning` on the returned attribution let a
379
+ * human reader judge that assumption per-record; an automated correlation
380
+ * should account for the same slack rather than treating the window edges as
381
+ * exact.
382
+ */
383
+ /**
384
+ * #1723 review round 6, R3 / round 7, S1: how close two fractions must be to
385
+ * count as the SAME answer rather than a genuinely different one. See
386
+ * `getPhaseForWindow`'s doc comment for the closed-form derivation (a
387
+ * co-started bracket's fraction gap from the true culprit is order
388
+ * `(lag/W)^2`, ~7e-8 at production scale) and why a single-pass streaming
389
+ * comparison against this band is NOT enough on its own (S1, below).
390
+ */
391
+ const FRACTION_TIE_EPSILON = 1e-4;
392
+ export function getPhaseForWindow(windowStartMs, windowEndMs) {
393
+ const nowMs = Date.now();
394
+ const windowLengthMs = windowEndMs - windowStartMs;
395
+ const minPlausibleElapsedMs = windowLengthMs * MIN_PLAUSIBLE_ELAPSED_FRACTION;
396
+ const candidates = [];
397
+ const consider = (phase, startedAt, endMs, stillRunning) => {
398
+ const startMs = Date.parse(startedAt);
399
+ const elapsedMs = endMs - startMs;
400
+ if (elapsedMs < minPlausibleElapsedMs)
401
+ return; // N4: implausibly short to be the cause
402
+ const overlapMs = Math.min(endMs, windowEndMs) - Math.max(startMs, windowStartMs);
403
+ // #1723 review round 5, R1: divide by max(elapsedMs, windowLengthMs), not
404
+ // elapsedMs alone — see the function doc comment for why a bare
405
+ // elapsedMs denominator let a short bracket wholly INSIDE the window
406
+ // score a perfect, undeserved 1.0.
407
+ const fraction = overlapMs / Math.max(elapsedMs, windowLengthMs);
408
+ if (fraction <= 0)
409
+ return; // no meaningful overlap — never a candidate
410
+ candidates.push({ phase, startedAt, stillRunning, elapsedMs, fraction });
411
+ };
412
+ for (const token of liveBrackets.keys()) {
413
+ consider(token.phase, token.startedAt, nowMs, true);
414
+ }
415
+ for (const closed of closedBrackets) {
416
+ consider(closed.phase, closed.startedAt, Date.parse(closed.closedAt), false);
417
+ }
418
+ if (candidates.length === 0)
419
+ return undefined;
420
+ // #1723 review round 7, S1 (blocker): a single-pass streaming comparison
421
+ // against `FRACTION_TIE_EPSILON` (round 6's shape) CHAINS — each accepted
422
+ // candidate becomes the new `bestFraction`, including a tie-win, and
423
+ // "near" is not transitive. A chain of near-ties whose adjacent gaps sit
424
+ // inside the epsilon band but whose OUTER gap does not (e.g. three
425
+ // candidates at fractions 0.600000 / 0.600080 / 0.600160, an 18s window)
426
+ // lets the running "best" walk across the whole band one hop at a time,
427
+ // so the final answer depends on SCAN ORDER — reachable at roughly 1.5ms
428
+ // of overlap difference in an 18s window, routine under `Promise.all`
429
+ // co-start. `bestFraction = Math.max(...)` alone does NOT fix this: the
430
+ // bug is the CHAINING, not the seed value.
431
+ //
432
+ // Fixed with two independent passes instead, order-independent by
433
+ // construction:
434
+ // Pass 1 finds the MAXIMUM fraction across every candidate — a plain
435
+ // reduction, immune to scan order.
436
+ // Pass 2 keeps only the candidates within `FRACTION_TIE_EPSILON` OF
437
+ // THAT MAXIMUM (not of each other, and not of a running "best" that
438
+ // could itself have already drifted) and picks the smallest
439
+ // `elapsedMs` among THEM. In the three-candidate example above, only
440
+ // the two candidates within the band of the true maximum (0.600160)
441
+ // compete; the third, whose gap from the maximum exceeds the epsilon
442
+ // even though its gap from its IMMEDIATE neighbor does not, is
443
+ // correctly excluded regardless of which order the three were
444
+ // scanned in.
445
+ let maxFraction = 0;
446
+ for (const candidate of candidates) {
447
+ if (candidate.fraction > maxFraction)
448
+ maxFraction = candidate.fraction;
449
+ }
450
+ let best;
451
+ for (const candidate of candidates) {
452
+ if (Math.abs(candidate.fraction - maxFraction) > FRACTION_TIE_EPSILON)
453
+ continue;
454
+ if (best === undefined || candidate.elapsedMs < best.elapsedMs)
455
+ best = candidate;
456
+ }
457
+ if (best === undefined)
458
+ return undefined;
459
+ return {
460
+ phase: best.phase,
461
+ startedAt: best.startedAt,
462
+ stillRunning: best.stillRunning,
463
+ elapsedMs: best.elapsedMs,
464
+ };
465
+ }
466
+ /**
467
+ * Test-only: the closed-bracket ring's actual storage length, mirroring
468
+ * `_recentPhasesStorageLengthForTest` above — pins that `phaseFinished`'s
469
+ * `.slice(0, CLOSED_BRACKET_CAP)` guard is intact independent of any
470
+ * read-side behavior (#1723 review: "ring unbounded" mutation).
471
+ */
472
+ export function _closedBracketsStorageLengthForTest() {
473
+ return closedBrackets.length;
474
+ }
475
+ /**
476
+ * Session-boundary backstop: unconditionally clears BOTH the live-bracket map
477
+ * and the closed-bracket ring. `phaseFinished` is the normal clear path
478
+ * (paired via try/finally at each call site), but this module's state is
479
+ * process-lifetime, not session-scoped, so a phase abandoned mid-flight by a
480
+ * torn-down activation (an abort that skips straight past the `finally`, a
481
+ * session replaced mid-scan) would otherwise leave a phantom bracket live
482
+ * forever — misattributing every loop_block in every LATER session to that
483
+ * one stale phase.
484
+ *
485
+ * #1723 review F4 — placement is deliberate, and DIFFERENT from
486
+ * `warmDispatchAtSessionStart()`: call this only once `session_start` has
487
+ * confirmed it is running a FULL session start (`decideSessionStart(...)
488
+ * .runFullSessionStart`), i.e. from BEHIND the #473 concurrent-secondary
489
+ * gate, not before it. The reason is the opposite of what an unconditional,
490
+ * "re-arm on every session_start" placement would give: this module's state
491
+ * is process-shared across every concurrently-live activation in the same
492
+ * process, exactly like the LSP fleet / runtime generation #473 already
493
+ * protects — a concurrent secondary's `session_start` firing while the
494
+ * PARENT activation has genuinely in-flight brackets must not wipe the
495
+ * parent's live attribution out from under it. Only a real sequential
496
+ * session replacement (new/resume/fork/reload confirmed by
497
+ * `decideSessionStart`) may safely assume no other activation still owns
498
+ * live brackets in this process. A concurrent secondary's OWN torn-down
499
+ * brackets are still covered: each caller's `phaseFinished` runs in its own
500
+ * try/finally, and if that activation is torn down hard enough to skip even
501
+ * that, the accepted cost is a stale bracket that the NEXT full session start
502
+ * clears — not a wrong clear of a sibling that is still working.
503
+ */
504
+ export function resetCurrentPhaseForSession() {
505
+ liveBrackets.clear();
506
+ closedBrackets = [];
78
507
  }
79
508
  export function logLatency(entry) {
80
509
  const ts = new Date().toISOString();
81
510
  if (entry.type === "phase" && entry.phase && !LAST_PHASE_EXCLUDED.has(entry.phase)) {
82
- lastPhase = { phase: entry.phase, ts };
511
+ recentPhases = [{ phase: entry.phase, ts }, ...recentPhases].slice(0, RECENT_PHASE_CAP);
83
512
  }
84
513
  if (isTestMode()) {
85
514
  return;
@@ -0,0 +1,21 @@
1
+ export function createLazyImport(load) {
2
+ let cached;
3
+ return {
4
+ get() {
5
+ // `Promise.resolve().then(load)` (not a bare `load()`) so a loader
6
+ // that throws SYNCHRONOUSLY — rather than returning a rejected
7
+ // promise — still yields a rejected promise instead of throwing out
8
+ // of `get()` itself. `import(...)` never throws synchronously, but a
9
+ // caller-supplied `load` is not guaranteed to keep that contract.
10
+ return (cached ??= Promise.resolve()
11
+ .then(() => load())
12
+ .catch((err) => {
13
+ cached = undefined;
14
+ throw err;
15
+ }));
16
+ },
17
+ resetForTests() {
18
+ cached = undefined;
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The degradation ledger's field-truncation policy, as a leaf module (#1816).
3
+ *
4
+ * It lives here rather than in `degradation-ledger.ts` because reason BUILDERS
5
+ * (`formatToolFailure`, `firstOutputLine`) need the bound without importing the
6
+ * ledger itself. Test files routinely `vi.mock` the ledger to observe recorded
7
+ * degradations; a builder that reached through that mock for a constant would
8
+ * break every one of them.
9
+ */
10
+ /** The single truncation bound for every ledger field, and for the reasons callers build. */
11
+ export const LEDGER_FIELD_MAX = 200;
12
+ export function normalizeForLedger(value) {
13
+ return String(value ?? "unknown");
14
+ }
15
+ /** Bound a value to `LEDGER_FIELD_MAX`, marking the elision. */
16
+ export function truncateForLedger(value) {
17
+ const text = normalizeForLedger(value);
18
+ return text.length > LEDGER_FIELD_MAX
19
+ ? `${text.slice(0, LEDGER_FIELD_MAX)}…`
20
+ : text;
21
+ }
@@ -172,6 +172,14 @@ export const LENS_FLAGS = [
172
172
  default: false,
173
173
  scope: "global",
174
174
  },
175
+ {
176
+ name: "lens-turn-end-madge",
177
+ description: "Run the per-turn-end madge circular-dependency check on import-changed files. Off by default: the pass only writes debug output, and user-facing madge diagnostics come from the session-start scan cache + lens_diagnostics. Also via turnEnd.madge.enabled=true in ~/.pi-lens/config.json.",
178
+ configKey: "turnEnd.madge.enabled",
179
+ negated: false,
180
+ default: false,
181
+ scope: "global",
182
+ },
175
183
  ];
176
184
  const byName = new Map(LENS_FLAGS.map((spec) => [spec.name, spec]));
177
185
  export function getLensFlagSpec(name) {