@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
@@ -10,11 +10,13 @@
10
10
  * Design: File-level targeted testing — only runs tests for the
11
11
  * specific file being edited, not the entire suite.
12
12
  */
13
+ import { createSubsystemLogger } from "./extension-log.js";
13
14
  import * as fs from "node:fs";
14
15
  import * as path from "node:path";
15
16
  import { minimatch } from "./deps/minimatch.js";
16
17
  import { detectFileRole } from "./file-role.js";
17
18
  import { findGlobalBinary } from "./package-manager.js";
19
+ import { isMeasuredDuration, toMeasuredDurationMs } from "./run-duration.js";
18
20
  import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
19
21
  // Source file → test file patterns (reverse lookup)
20
22
  const SOURCE_TO_TEST_PATTERNS = [
@@ -64,7 +66,7 @@ const MAX_NODE_MODULES_WALK_UP = 5;
64
66
  // unbounded walk of the whole tests tree.
65
67
  const MAX_PYTEST_RECURSE_DEPTH = 3;
66
68
  // --- Runner Detection ---
67
- const RUNNERS = {
69
+ export const RUNNERS = {
68
70
  vitest: {
69
71
  configFiles: ["vitest.config.ts", "vitest.config.js", "vitest.config.mjs"],
70
72
  command: "npx",
@@ -140,6 +142,11 @@ const RUNNERS = {
140
142
  rspec: {
141
143
  configFiles: [".rspec", "spec/spec_helper.rb"],
142
144
  command: "bundle",
145
+ // The real binary is "bundle" (the command runs `bundle exec rspec
146
+ // <file>`), NOT "rspec" — without this, binName defaulted to the
147
+ // runner key "rspec" and local/global resolution looked for the wrong
148
+ // binary name (#1098).
149
+ binName: "bundle",
143
150
  args: (testFile, _cwd) => ["exec", "rspec", testFile],
144
151
  parseJson: false,
145
152
  },
@@ -166,6 +173,26 @@ const RUNNERS = {
166
173
  parseJson: false, // mix test's default output is text-based
167
174
  },
168
175
  };
176
+ /**
177
+ * Drop the leading arg(s) of a runner's args() that merely NAME the binary
178
+ * being invoked (the npx-wrapper convention: `npx vitest run …` → once
179
+ * `vitest` becomes the resolved command itself, the leading "vitest" arg is
180
+ * redundant). This must NOT strip a real subcommand.
181
+ *
182
+ * Two wrapper shapes are recognized:
183
+ * - `[binName, ...rest]` (vitest/jest-style: `npx <bin> ...`) → drop 1.
184
+ * - `["-m", binName, ...rest]` (pytest-style: `python -m <bin> ...`) → drop 2.
185
+ * Anything else (cargo's `["test", "--no-fail-fast"]`, go's
186
+ * `["test", "-run", …]`, rspec's `["exec", "rspec", file]` once binName is
187
+ * "bundle", etc.) is a real subcommand/argv and is returned unchanged (#1098).
188
+ */
189
+ export function stripWrapperArgs(binName, args) {
190
+ if (args[0] === binName)
191
+ return args.slice(1);
192
+ if (args[0] === "-m" && args[1] === binName)
193
+ return args.slice(2);
194
+ return args;
195
+ }
169
196
  // --- Client ---
170
197
  export class TestRunnerClient {
171
198
  log;
@@ -180,7 +207,7 @@ export class TestRunnerClient {
180
207
  vitestTestGlobsCache = new Map();
181
208
  constructor(verbose = false) {
182
209
  this.log = verbose
183
- ? (msg) => console.error(`[test-runner] ${msg}`)
210
+ ? createSubsystemLogger("test-runner")
184
211
  : () => { };
185
212
  }
186
213
  /**
@@ -844,9 +871,17 @@ export class TestRunnerClient {
844
871
  runner,
845
872
  passed: json.numPassedTests || 0,
846
873
  failed: json.numFailedTests || 0,
847
- skipped: json.numSkippedTests || 0,
874
+ // #1452: `numSkippedTests` is absent from both reporters' JSON, so
875
+ // this read was always 0. `numPendingTests` is where a `test.skip`
876
+ // actually lands; `numTodoTests` is counted with it because the
877
+ // text parsers (pytest `N skipped`, mix `N excluded` + `N skipped`)
878
+ // also fold every not-run test into one `skipped` figure.
879
+ // `??` would accept a present 0, so a reporter that emits
880
+ // `numSkippedTests: 0` beside a real `numPendingTests` would
881
+ // reproduce the very defect this removes. Take the larger reading.
882
+ skipped: Math.max(json.numSkippedTests ?? 0, (json.numPendingTests || 0) + (json.numTodoTests || 0)),
848
883
  failures,
849
- duration: 0,
884
+ duration: this.jsonRunDurationMs(json.testResults),
850
885
  };
851
886
  }
852
887
  catch (err) {
@@ -855,6 +890,74 @@ export class TestRunnerClient {
855
890
  return this.emptyResult(testFile, "", runner, failed ? "Tests failed (could not parse output)" : undefined);
856
891
  }
857
892
  }
893
+ /**
894
+ * #1452: real run duration in ms from a vitest/jest `--json` payload.
895
+ *
896
+ * NOT `testResults[].perfStats`. That field exists on jest's INTERNAL
897
+ * `TestResult`, but the JSON reporter's `formatTestResults` projects it to
898
+ * per-suite `startTime`/`endTime` and drops it — measured absent from both
899
+ * vitest 4.1.10 and jest 30.4.2 output, so reading it would have left this
900
+ * at 0. The per-suite epoch pair is what both reporters actually emit.
901
+ *
902
+ * Wall-clock SPAN across suites (max end - min start), not a sum: suites in
903
+ * one payload may have run in parallel workers, and summing would report
904
+ * more elapsed time than the run took. With the single suite pi-lens
905
+ * actually produces (one test file per invocation) the two agree.
906
+ *
907
+ * The span excludes the runner's own startup: the top-level `startTime` is
908
+ * ~330ms earlier than the first suite's on this repo. What the per-suite
909
+ * pair then measures is NOT the same quantity across runners. On vitest it
910
+ * tracks test time closely (135ms span against 134ms of summed assertions),
911
+ * but jest stamps a suite's `startTime` before transform and module load,
912
+ * so the same fields give 5595ms against 128ms of assertions. Both are
913
+ * honest suite wall clock; neither is comparable to the other, and only the
914
+ * vitest figure is close to what pytest's `in 0.05s` or ExUnit's
915
+ * `Finished in 0.05 seconds` report.
916
+ *
917
+ * Falls back to the summed per-assertion `duration` when a reporter omits
918
+ * the suite pair. Never returns a negative or non-finite value — a garbled
919
+ * payload must degrade to "unmeasured", not to a wrong number.
920
+ *
921
+ * #1479: that degradation is now literal. This used to return 0 for a
922
+ * payload it could not read, which is the figure a sub-millisecond suite
923
+ * also produces, so the caller could not tell them apart. It returns
924
+ * `undefined` instead. A readable pair whose span is 0 still returns 0,
925
+ * because that is a measurement.
926
+ */
927
+ jsonRunDurationMs(suites) {
928
+ let minStart = Number.POSITIVE_INFINITY;
929
+ let maxEnd = Number.NEGATIVE_INFINITY;
930
+ let assertionTotal = 0;
931
+ for (const suite of suites || []) {
932
+ if (typeof suite.startTime === "number" &&
933
+ Number.isFinite(suite.startTime) &&
934
+ typeof suite.endTime === "number" &&
935
+ Number.isFinite(suite.endTime)) {
936
+ minStart = Math.min(minStart, suite.startTime);
937
+ maxEnd = Math.max(maxEnd, suite.endTime);
938
+ }
939
+ for (const assertion of suite.assertionResults || []) {
940
+ if (typeof assertion.duration === "number" &&
941
+ Number.isFinite(assertion.duration) &&
942
+ assertion.duration > 0) {
943
+ assertionTotal += assertion.duration;
944
+ }
945
+ }
946
+ }
947
+ const span = maxEnd - minStart;
948
+ if (Number.isFinite(span) && span > 0)
949
+ return Math.round(span);
950
+ if (assertionTotal > 0)
951
+ return Math.round(assertionTotal);
952
+ // Ordering above is unchanged from #1452 on purpose: a positive span
953
+ // still beats the assertion sum, and the sum still beats a suite pair
954
+ // that read as zero. Only the terminal case moved. A pair we could
955
+ // read whose span is 0 is a run that took under a millisecond — report
956
+ // it. Everything else was never measured.
957
+ if (Number.isFinite(span) && span === 0)
958
+ return 0;
959
+ return undefined;
960
+ }
858
961
  // --- Vitest Parser ---
859
962
  parseVitestOutput(stdout, stderr, testFile, cwd, runner) {
860
963
  return this.parseJsonTestOutput(stdout, stderr, testFile, cwd, runner);
@@ -873,7 +976,9 @@ export class TestRunnerClient {
873
976
  let passed = 0;
874
977
  let failed = 0;
875
978
  let skipped = 0;
876
- let duration = 0;
979
+ // #1479: undefined until pytest's own `in N.NNs` is read. `in 0.00s` is
980
+ // a real pytest summary, so 0 has to stay available as a measurement.
981
+ let duration;
877
982
  if (summaryMatch) {
878
983
  // Extract numbers from various patterns
879
984
  const passedMatch = output.match(/(\d+)\s+passed/);
@@ -883,7 +988,12 @@ export class TestRunnerClient {
883
988
  passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
884
989
  failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
885
990
  skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
886
- duration = durationMatch ? parseFloat(durationMatch[1]) * 1000 : 0;
991
+ // Rounded, like `jsonRunDurationMs` and PHPUnit's legacy path:
992
+ // `in 2.01s` is 2009.9999999999998 in binary floating point, and
993
+ // the turn-end log prints the number as it stands.
994
+ // (Routing this through `toMeasuredDurationMs` is #1484, not this.)
995
+ if (durationMatch)
996
+ duration = Math.round(parseFloat(durationMatch[1]) * 1000);
887
997
  }
888
998
  // Parse individual failures: "FAILED tests/test_foo.py::test_something - AssertionError: ..."
889
999
  const failureRegex = /FAILED\s+(\S+::\S+)\s*-\s*(.+?)(?:\n|$)/g;
@@ -946,6 +1056,46 @@ export class TestRunnerClient {
946
1056
  while ((match = failureRegex.exec(output)) !== null) {
947
1057
  failures.push({ name: match[1], message: match[1] });
948
1058
  }
1059
+ // #1452: PHPUnit prints its own elapsed time and this parser dropped it,
1060
+ // so every PHPUnit run reported 0ms. Two shapes are accepted because the
1061
+ // summary changed across supported majors:
1062
+ // PHPUnit >= 9.3 "Time: 00:00.123, Memory: 8.00 MB" (HH:)MM:SS.mmm
1063
+ // PHPUnit <= 9.2 "Time: 1.23 seconds, Memory: 10.00MB" | "Time: 123 ms"
1064
+ // NOT VERIFIED AGAINST A LIVE PHPUnit — there is no PHP toolchain on the
1065
+ // box this was written on. Both shapes are covered by unit tests against
1066
+ // literal summary lines taken from the PHPUnit printers, and the parser
1067
+ // leaves duration UNMEASURED when neither matches (#1479 — it used to
1068
+ // leave 0, which the turn-end log printed as a measurement), so an
1069
+ // unrecognised summary degrades to "we do not know" rather than to a
1070
+ // wrong figure.
1071
+ let duration;
1072
+ const clockMatch = output.match(/^Time:\s*(?:(\d+):)?(\d{1,2}):(\d{2})(?:\.(\d{1,3}))?/im);
1073
+ if (clockMatch) {
1074
+ const hours = clockMatch[1] ? Number.parseInt(clockMatch[1], 10) : 0;
1075
+ const minutes = Number.parseInt(clockMatch[2], 10);
1076
+ const seconds = Number.parseInt(clockMatch[3], 10);
1077
+ // ".1" is a tenth, ".12" hundredths — pad rather than parseInt, or
1078
+ // "Time: 00:00.1" would read as 1ms instead of 100ms.
1079
+ const millis = clockMatch[4]
1080
+ ? Number.parseInt(clockMatch[4].padEnd(3, "0"), 10)
1081
+ : 0;
1082
+ duration = ((hours * 60 + minutes) * 60 + seconds) * 1000 + millis;
1083
+ }
1084
+ else {
1085
+ const legacyMatch = output.match(/^Time:\s*([\d.]+)\s*(seconds?|s|ms|milliseconds?|minutes?)\b/im);
1086
+ if (legacyMatch) {
1087
+ const value = Number.parseFloat(legacyMatch[1]);
1088
+ const unit = legacyMatch[2].toLowerCase();
1089
+ const scale = unit.startsWith("ms") || unit.startsWith("milli")
1090
+ ? 1
1091
+ : unit.startsWith("min")
1092
+ ? 60_000
1093
+ : 1000;
1094
+ if (Number.isFinite(value) && value > 0) {
1095
+ duration = Math.round(value * scale);
1096
+ }
1097
+ }
1098
+ }
949
1099
  return {
950
1100
  file: testFile,
951
1101
  sourceFile: "",
@@ -954,7 +1104,7 @@ export class TestRunnerClient {
954
1104
  failed,
955
1105
  skipped,
956
1106
  failures,
957
- duration: 0,
1107
+ duration,
958
1108
  error: exitCode !== 0 && passed === 0 && failed === 0
959
1109
  ? "PHPUnit runner error"
960
1110
  : undefined,
@@ -966,7 +1116,8 @@ export class TestRunnerClient {
966
1116
  let passed = 0;
967
1117
  let failed = 0;
968
1118
  let skipped = 0;
969
- let duration = 0;
1119
+ // #1479: undefined until ExUnit's own `Finished in N seconds` is read.
1120
+ let duration;
970
1121
  // Summary: "3 tests, 1 failure" (optionally ", N excluded" / ", N skipped")
971
1122
  const summaryMatch = output.match(/(\d+)\s+tests?,\s*(\d+)\s+failures?(?:,\s*(\d+)\s+excluded)?(?:,\s*(\d+)\s+skipped)?/i);
972
1123
  if (summaryMatch) {
@@ -981,7 +1132,10 @@ export class TestRunnerClient {
981
1132
  }
982
1133
  const durationMatch = output.match(/Finished in\s+([\d.]+)\s+seconds?/i);
983
1134
  if (durationMatch) {
984
- duration = Number.parseFloat(durationMatch[1]) * 1000;
1135
+ // Rounded for the same reason pytest's is: `2.01` seconds is
1136
+ // 2009.9999999999998 ms unrounded, and that reaches the log.
1137
+ // (Routing this through `toMeasuredDurationMs` is #1484.)
1138
+ duration = Math.round(Number.parseFloat(durationMatch[1]) * 1000);
985
1139
  }
986
1140
  // Individual failures: " 1) test some behavior (MyModuleTest)"
987
1141
  const failures = [];
@@ -1009,17 +1163,239 @@ export class TestRunnerClient {
1009
1163
  };
1010
1164
  }
1011
1165
  // --- Generic text parser for non-JSON runners ---
1166
+ /**
1167
+ * #1480: elapsed time for the runners `parseGenericRunnerOutput` handles.
1168
+ *
1169
+ * Before this, only go's `ok pkg 0.25s` was read and every other runner
1170
+ * reported a hardcoded 0. #1479 made the log tell "measured" from
1171
+ * "unmeasured", but this parser is the `default:` arm behind cargo, dotnet,
1172
+ * maven, gradle, rspec, minitest and every unrecognised runner, so all of
1173
+ * them still reported a number nobody measured. Each runner below prints
1174
+ * its elapsed time in the same summary block this parser already regexes
1175
+ * for pass/fail counts.
1176
+ *
1177
+ * Absent, not 0, is the answer when nothing is found — see
1178
+ * `TestResult.duration` and `run-duration.ts`. A probe that returned 0 here
1179
+ * would be claiming a measurement.
1180
+ *
1181
+ * One parser serves all runners, so the probe is selected BY RUNNER NAME.
1182
+ * Running every probe over every runner's output was the original shape of
1183
+ * this code, and it let gradle borrow a number: `BUILD SUCCESSFUL in 3s`
1184
+ * plus a preceding `... ok` line satisfied go's `ok <pkg> <n>s` probe, so
1185
+ * the whole-build wall clock got reported as test time — the exact wrong
1186
+ * number this function refuses to print. Gating on the runner makes that
1187
+ * structurally impossible rather than merely unlikely, and it matters most
1188
+ * for the `default:` arm of the switch, which is where an unrecognised or
1189
+ * custom runner's arbitrary output lands.
1190
+ *
1191
+ * Within a runner the patterns are still anchored where an anchor helps,
1192
+ * for the same reason #1452's PHPUnit `Time:` pattern is anchored: an
1193
+ * unanchored /m match takes the FIRST hit over stdout+stderr, and a failure
1194
+ * diff quoting "Finished in ..." would beat the real summary. Note what the
1195
+ * `^` in `^Finished in` does and does not buy. It rejects a decoy that is
1196
+ * INDENTED, which is what a quoted expectation or an assertion diff is; it
1197
+ * does NOT rank two column-0 matches, so an unindented decoy printed by the
1198
+ * suite itself would still win. It is a cheap filter for the common shape,
1199
+ * not a proof of uniqueness. And it is not an anchor to the counts line for
1200
+ * rspec or minitest: both print their elapsed time on a `Finished in ...`
1201
+ * line and their counts (`3 examples, 0 failures`, `1 runs, 1 assertions,
1202
+ * ...`) on a different line.
1203
+ *
1204
+ * KNOWN LIMIT — first summary only. cargo across multiple crates, `dotnet
1205
+ * test` across multiple assemblies, and `go test ./...` across multiple
1206
+ * packages each print one summary per unit, and these probes take the
1207
+ * first. A multi-unit run therefore UNDER-REPORTS its duration. That is
1208
+ * left as-is deliberately: the count parsers below have the same first-match
1209
+ * shape for those runners, so duration and counts describe the same scope.
1210
+ * Fixing one without the other would trade an under-report for an
1211
+ * inconsistency. Pinned by test so it stays a known limit, not an accident.
1212
+ *
1213
+ * Formats and how each was verified:
1214
+ *
1215
+ * - go — `ok example.com/pkg 0.253s`. Pre-existing pattern, unchanged
1216
+ * apart from the shared finite/non-negative guard.
1217
+ *
1218
+ * - cargo — `test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured;
1219
+ * 0 filtered out; finished in 0.253s`. NOT VERIFIED AGAINST A LIVE CARGO
1220
+ * RUN — this box has no MSVC linker, so `cargo test` cannot link. Format
1221
+ * read out of the libtest printer shipped with the local rustc 1.94.1:
1222
+ * `library/test/src/formatters/pretty.rs` builds `"; finished in
1223
+ * {exec_time}"` and `library/test/src/time.rs` renders `TestSuiteExecTime`
1224
+ * as `{:.2}s`. Older rustc omits the suffix entirely; that degrades to
1225
+ * unmeasured.
1226
+ *
1227
+ * - dotnet/vstest — `Failed: 1, Passed: 2, Skipped: 0, Total: 3, Duration:
1228
+ * 1 m 30 s - t.dll (net8.0)`. NOT VERIFIED AGAINST A LIVE `dotnet test` —
1229
+ * NuGet restore has no network here. Format read out of the
1230
+ * vstest.console.dll shipped with the local .NET SDK 8.0.423, which holds
1231
+ * the literal `{0} - Failed: {1}, Passed: {2}, Skipped: {3}, Total: {4},
1232
+ * Duration: {5}` next to the unit literals `" h"`, `" m"`, `" s"`,
1233
+ * `" ms"`, `"< 1 ms"`. The duration is a space-joined token list, so it
1234
+ * is summed rather than read as one number.
1235
+ *
1236
+ * - maven/surefire — `Tests run: 4, Failures: 0, Errors: 0, Skipped: 0,
1237
+ * Time elapsed: 0.05 s -- in com.example.AppTest`. NOT VERIFIED AGAINST A
1238
+ * LIVE MAVEN — no mvn on this box. Summed across the per-class lines,
1239
+ * because surefire prints `Time elapsed` per test class and its final
1240
+ * `Results:` total carries no time. `[INFO] Total time: 3.4 s` is
1241
+ * deliberately NOT used: that is whole-build wall clock including compile,
1242
+ * which would report a wrong number rather than none. Surefire 2.x wrote
1243
+ * `sec` where 3.x writes `s`; both are accepted.
1244
+ *
1245
+ * EXPECT THIS TO BE ABSENT IN PRACTICE. pi-lens invokes `mvn test -q`
1246
+ * (see RUNNERS.maven above), and surefire logs its per-class `Tests run:
1247
+ * ..., Time elapsed: ...` lines at INFO, which `-q` suppresses. Only the
1248
+ * ERROR-level lines of a FAILING class survive, so a green maven run
1249
+ * typically reports unmeasured and a red one reports the failing classes'
1250
+ * time alone. REASONED, NOT RUN — there is no mvn on this box to confirm
1251
+ * it. Left in rather than dropped: it costs nothing, it is correct when
1252
+ * the output does carry the lines (a repo that sets `-Dsurefire.useFile`
1253
+ * or drops `-q` via `.mvn/maven.config`), and `unmeasured` is an honest
1254
+ * report of the quiet case.
1255
+ *
1256
+ * - rspec — `Finished in 0.32394 seconds (files took 0.49427 seconds to
1257
+ * load)`. VERIFIED against a live rspec-core 3.13.6 run on ruby 3.4.10.
1258
+ * The minutes form (`Finished in 2 minutes 15.14 seconds`) comes from
1259
+ * `RSpec::Core::Formatters::Helpers.format_duration` in the same
1260
+ * installed gem; rspec never prints hours. Load time trails the run time
1261
+ * on the same line and must not be read instead of it.
1262
+ *
1263
+ * - minitest — `Finished in 0.254594s, 7.8557 runs/s, 7.8557 assertions/s.`
1264
+ * VERIFIED against a live minitest 5.25.4 run on ruby 3.4.10. The format
1265
+ * string is `"Finished in %.6fs, ..."` in minitest.rb, always seconds.
1266
+ *
1267
+ * - gradle — deliberately left unmeasured, and now UNREACHABLE by any other
1268
+ * runner's probe rather than merely unmatched by it. Gradle's console
1269
+ * summary (`4 tests completed, 1 failed`) carries no elapsed time, and
1270
+ * `BUILD SUCCESSFUL in 3s` is whole-build wall clock including compile
1271
+ * and dependency resolution. Reporting that as test time would be a wrong
1272
+ * number; #1479 makes the absence legible in the log instead.
1273
+ */
1274
+ parseGenericRunnerDuration(output, runner) {
1275
+ switch (runner) {
1276
+ case "go":
1277
+ return this.parseGoDuration(output);
1278
+ case "cargo":
1279
+ return this.parseCargoDuration(output);
1280
+ case "dotnet":
1281
+ return this.parseDotnetDuration(output);
1282
+ case "maven":
1283
+ return this.parseMavenDuration(output);
1284
+ case "rspec":
1285
+ return this.parseRspecDuration(output);
1286
+ case "minitest":
1287
+ return this.parseMinitestDuration(output);
1288
+ default:
1289
+ // gradle and anything unrecognised: unmeasured, never
1290
+ // zero-as-measurement and never another runner's number.
1291
+ return undefined;
1292
+ }
1293
+ }
1294
+ /** go: `ok example.com/pkg 0.253s`. First package summary only. */
1295
+ parseGoDuration(output) {
1296
+ const goSummary = output.match(/ok\s+\S+\s+([\d.]+)s/m);
1297
+ if (!goSummary)
1298
+ return undefined;
1299
+ return toMeasuredDurationMs(Number.parseFloat(goSummary[1]) * 1000);
1300
+ }
1301
+ /** cargo: `...; 0 filtered out; finished in 0.25s`. First crate only. */
1302
+ parseCargoDuration(output) {
1303
+ const cargoTime = output.match(/^test result:.*?;\s*finished in\s+([\d.]+)\s*s\b/im);
1304
+ if (!cargoTime)
1305
+ return undefined;
1306
+ return toMeasuredDurationMs(Number.parseFloat(cargoTime[1]) * 1000);
1307
+ }
1308
+ /**
1309
+ * dotnet/vstest: `..., Total: 3, Duration: 1 m 30 s - t.dll (net8.0)`.
1310
+ *
1311
+ * Anchored to the counts line, and the tail stops at the ` - <dll>`
1312
+ * separator: without that stop an assembly name is scanned for unit tokens,
1313
+ * and a name like `Timeouts.30s.Tests.dll` adds 30 seconds of nothing.
1314
+ * First assembly only.
1315
+ */
1316
+ parseDotnetDuration(output) {
1317
+ const dotnetTime = output.match(/Failed:\s*\d+,\s*Passed:\s*\d+,\s*Skipped:\s*\d+,\s*Total:\s*\d+,\s*Duration:\s*([^\r\n-]+)/i);
1318
+ if (!dotnetTime)
1319
+ return undefined;
1320
+ // `< 1 ms` is vstest's "too fast to name a number", and under the
1321
+ // optional-duration contract 0 is exactly the right thing to say: the
1322
+ // run WAS measured and it rounds to 0 ms. The token scan below would
1323
+ // reach the same 0 by finding no tokens, but only by accident, and the
1324
+ // accident is indistinguishable from an unparseable tail — so the case
1325
+ // is spelled out.
1326
+ if (/^\s*</.test(dotnetTime[1]))
1327
+ return 0;
1328
+ let total = 0;
1329
+ let tokens = 0;
1330
+ // "ms" before "m", or "250 ms" scores as 250 minutes.
1331
+ const units = {
1332
+ ms: 1,
1333
+ s: 1000,
1334
+ m: 60_000,
1335
+ h: 3_600_000,
1336
+ };
1337
+ for (const token of dotnetTime[1].matchAll(/([\d.]+)\s*(ms|h|m|s)\b/gi)) {
1338
+ total += Number.parseFloat(token[1]) * units[token[2].toLowerCase()];
1339
+ tokens++;
1340
+ }
1341
+ // A tail we matched but could not read a single token out of is not a
1342
+ // zero-length run, it is an unrecognised format.
1343
+ if (tokens === 0)
1344
+ return undefined;
1345
+ return toMeasuredDurationMs(total);
1346
+ }
1347
+ /**
1348
+ * maven/surefire: summed across per-class `Time elapsed` lines.
1349
+ *
1350
+ * The guard is "did any line match", NOT "is the sum positive". Surefire
1351
+ * prints `Time elapsed: 0.00 s` for a trivial test class, and that is a
1352
+ * measurement of zero, not a failure to measure.
1353
+ */
1354
+ parseMavenDuration(output) {
1355
+ let surefireTotal = 0;
1356
+ let matched = false;
1357
+ for (const line of output.matchAll(/^.*Tests run:\s*\d+,.*?Time elapsed:\s*([\d.]+)\s*(?:s|sec|secs|seconds)\b.*$/gim)) {
1358
+ const seconds = Number.parseFloat(line[1]);
1359
+ if (!Number.isFinite(seconds) || seconds < 0)
1360
+ continue;
1361
+ surefireTotal += seconds;
1362
+ matched = true;
1363
+ }
1364
+ if (!matched)
1365
+ return undefined;
1366
+ return toMeasuredDurationMs(surefireTotal * 1000);
1367
+ }
1368
+ /** rspec: `Finished in 2 minutes 15.14 seconds (files took 0.5 ...)`. */
1369
+ parseRspecDuration(output) {
1370
+ const rspecTime = output.match(/^Finished in\s+(?:([\d.]+)\s+minutes?\s+)?([\d.]+)\s+seconds?/im);
1371
+ if (!rspecTime)
1372
+ return undefined;
1373
+ const minutes = rspecTime[1] ? Number.parseFloat(rspecTime[1]) : 0;
1374
+ return toMeasuredDurationMs(minutes * 60_000 + Number.parseFloat(rspecTime[2]) * 1000);
1375
+ }
1376
+ /**
1377
+ * minitest: `Finished in 0.254594s, 7.8557 runs/s, ...`.
1378
+ *
1379
+ * The trailing `,` is load-bearing, not decoration: it is what separates
1380
+ * minitest's own line from a bare `Finished in 99s` the suite under test
1381
+ * printed at column 0, which the `^` alone does not rank.
1382
+ */
1383
+ parseMinitestDuration(output) {
1384
+ const minitestTime = output.match(/^Finished in\s+([\d.]+)s\s*,/im);
1385
+ if (!minitestTime)
1386
+ return undefined;
1387
+ return toMeasuredDurationMs(Number.parseFloat(minitestTime[1]) * 1000);
1388
+ }
1012
1389
  parseGenericRunnerOutput(stdout, stderr, exitCode, testFile, runner) {
1013
1390
  const output = `${stdout}\n${stderr}`;
1014
1391
  const lower = output.toLowerCase();
1015
1392
  let passed = 0;
1016
1393
  let failed = exitCode === 0 ? 0 : 1;
1017
1394
  let skipped = 0;
1018
- let duration = 0;
1019
- const goSummary = output.match(/ok\s+\S+\s+([\d.]+)s/m);
1020
- if (goSummary) {
1021
- duration = Number.parseFloat(goSummary[1]) * 1000;
1022
- }
1395
+ // #1480: `number | undefined`, and sourced per runner. This used to be
1396
+ // `let duration = 0` with only go's probe able to move it, so every
1397
+ // other runner reported a zero it never measured.
1398
+ const duration = this.parseGenericRunnerDuration(output, runner);
1023
1399
  const cargoSummary = output.match(/test result:\s+\w+\.\s+(\d+)\s+passed;\s+(\d+)\s+failed;\s+(\d+)\s+ignored;/i);
1024
1400
  if (cargoSummary) {
1025
1401
  passed = Number.parseInt(cargoSummary[1], 10);
@@ -1032,13 +1408,41 @@ export class TestRunnerClient {
1032
1408
  passed = Number.parseInt(dotnetSummary[2], 10);
1033
1409
  skipped = Number.parseInt(dotnetSummary[3], 10);
1034
1410
  }
1035
- const mavenSummary = output.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/i);
1036
- if (mavenSummary) {
1037
- const total = Number.parseInt(mavenSummary[1], 10);
1038
- const failures = Number.parseInt(mavenSummary[2], 10);
1039
- const errors = Number.parseInt(mavenSummary[3], 10);
1040
- skipped = Number.parseInt(mavenSummary[4], 10);
1041
- failed = failures + errors;
1411
+ // #1480 (adjacent, duration-independent): surefire prints one
1412
+ // `Tests run:` line PER TEST CLASS (those carry `Time elapsed:`) and
1413
+ // then a per-MODULE aggregate under `Results:` (which does not). Taking
1414
+ // the FIRST match scored a run by its first class alone — a two-class
1415
+ // run with a failure in the second class reported 0 failures.
1416
+ //
1417
+ // Taking the LAST match is just as wrong, in a worse direction. A
1418
+ // multi-module reactor run prints one `Results:` aggregate per module,
1419
+ // and the last is the last module: a `--fail-at-end` build whose first
1420
+ // module had 3 failures and whose second module was green would report
1421
+ // 0 failures, turning a red build into `PASS` in the turn-end log. That
1422
+ // is reachable without pi-lens passing the flag, because maven also
1423
+ // reads `.mvn/maven.config` and `MAVEN_ARGS`.
1424
+ //
1425
+ // So: SUM the aggregates. That makes counts reactor-wide, the same
1426
+ // scope `parseMavenDuration` sums its per-class times over. When no
1427
+ // aggregate is present (output truncated, or `Results:` suppressed) the
1428
+ // per-class lines sum to the same totals, so they are the fallback.
1429
+ const mavenLines = [
1430
+ ...output.matchAll(/^.*?Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+).*$/gim),
1431
+ ];
1432
+ const mavenAggregates = mavenLines.filter((line) => !/Time elapsed:/i.test(line[0]));
1433
+ const mavenScored = mavenAggregates.length > 0 ? mavenAggregates : mavenLines;
1434
+ if (mavenScored.length > 0) {
1435
+ let total = 0;
1436
+ let mavenFailed = 0;
1437
+ let mavenSkipped = 0;
1438
+ for (const line of mavenScored) {
1439
+ total += Number.parseInt(line[1], 10);
1440
+ mavenFailed +=
1441
+ Number.parseInt(line[2], 10) + Number.parseInt(line[3], 10);
1442
+ mavenSkipped += Number.parseInt(line[4], 10);
1443
+ }
1444
+ failed = mavenFailed;
1445
+ skipped = mavenSkipped;
1042
1446
  passed = Math.max(0, total - failed - skipped);
1043
1447
  }
1044
1448
  const rspecSummary = output.match(/(\d+)\s+examples?,\s+(\d+)\s+failures?/i);
@@ -1061,6 +1465,22 @@ export class TestRunnerClient {
1061
1465
  failed = Number.parseInt(gradleSummary[2], 10);
1062
1466
  passed = Math.max(0, total - failed);
1063
1467
  }
1468
+ // Captured BEFORE the guard below, which rewrites `failed` out of the
1469
+ // state this condition reads. Without this the runner-error string
1470
+ // silently became unreachable.
1471
+ const runnerError = exitCode !== 0 && failed === 0 && lower.includes("error")
1472
+ ? `Runner ${runner} exited with ${exitCode}`
1473
+ : undefined;
1474
+ // #1480 (adjacent): a non-zero exit is the runner saying the run
1475
+ // failed. Every count parser above can legitimately arrive at
1476
+ // `failed === 0` — a summary that only covers part of the run, a green
1477
+ // module of a red reactor build, a failure outside any test — and the
1478
+ // turn-end log would then print PASS over a build the runner rejected.
1479
+ // Trust the exit code: no parse of the text may talk it out of at least
1480
+ // one failure.
1481
+ if (exitCode !== 0 && failed === 0) {
1482
+ failed = 1;
1483
+ }
1064
1484
  if (passed === 0 && failed === 0 && skipped === 0 && exitCode === 0) {
1065
1485
  passed = 1;
1066
1486
  }
@@ -1090,9 +1510,7 @@ export class TestRunnerClient {
1090
1510
  skipped,
1091
1511
  failures,
1092
1512
  duration,
1093
- error: exitCode !== 0 && failed === 0 && lower.includes("error")
1094
- ? `Runner ${runner} exited with ${exitCode}`
1095
- : undefined,
1513
+ error: runnerError,
1096
1514
  };
1097
1515
  }
1098
1516
  // --- Formatting ---
@@ -1108,7 +1526,21 @@ export class TestRunnerClient {
1108
1526
  if (total === 0) {
1109
1527
  return ""; // No tests to report
1110
1528
  }
1111
- const durationStr = result.duration > 0 ? ` (${(result.duration / 1000).toFixed(2)}s)` : "";
1529
+ // #1479 deliberately does NOT change this surface. The agent-facing
1530
+ // string already suppressed the suffix for a 0, so an unmeasured run
1531
+ // and a zero-length one look the same here and always did. The issue
1532
+ // scopes the unmeasured/zero distinction to the turn-end log line;
1533
+ // widening it to the LLM prompt is a separate call about prompt noise.
1534
+ // #1480: the "is this a measurement at all" half of the test comes from
1535
+ // `run-duration.ts` so this surface cannot drift from the log's answer.
1536
+ // The `> 0` half is the scope decision above and stays local to it — it
1537
+ // is what suppresses the suffix for a measured zero, which is a choice
1538
+ // about prompt noise rather than about the duration contract. Routing
1539
+ // the first half through the shared predicate also stops a non-finite
1540
+ // duration rendering as ` (Infinitys)`.
1541
+ const durationStr = isMeasuredDuration(result.duration) && result.duration > 0
1542
+ ? ` (${(result.duration / 1000).toFixed(2)}s)`
1543
+ : "";
1112
1544
  if (result.failed === 0) {
1113
1545
  return `[Tests] ✓ ${result.passed}/${total} passed${durationStr} — ${result.runner}`;
1114
1546
  }
@@ -1213,8 +1645,12 @@ export class TestRunnerClient {
1213
1645
  * Resolve the executable and args for a runner, preferring a local
1214
1646
  * node_modules/.bin binary over npx to avoid the ~150ms npx startup cost.
1215
1647
  *
1216
- * When a local binary is used, args()[0] (the runner name that npx needs)
1217
- * is dropped since it becomes the command itself.
1648
+ * When a resolved binary becomes the command itself, `stripWrapperArgs`
1649
+ * drops ONLY the leading arg(s) that named the wrapped binary — never a
1650
+ * real subcommand (#1098: `cargo test --no-fail-fast` unconditionally lost
1651
+ * `test` here because the old code assumed every runner's args() started
1652
+ * with an npx-style runner-name arg, which only holds for wrapper-style
1653
+ * runners like vitest/jest/pytest).
1218
1654
  */
1219
1655
  async resolveExec(runner, config, testFile, cwd) {
1220
1656
  // PHPUnit has no npx-style automatic local-binary resolution — Composer's
@@ -1232,15 +1668,15 @@ export class TestRunnerClient {
1232
1668
  const suffix = process.platform === "win32" ? ".cmd" : "";
1233
1669
  const localBin = path.join(cwd, "node_modules", ".bin", binName + suffix);
1234
1670
  // A resolved binary (local, or any manager's global bin) becomes the command
1235
- // itself, so the leading runner-name arg (e.g. "vitest") that npx needs is
1236
- // dropped from args().
1671
+ // itself, so the leading wrapper-name arg(s) that named it (e.g. "vitest",
1672
+ // or "-m pytest") are stripped from args() — see stripWrapperArgs.
1237
1673
  if (fs.existsSync(localBin)) {
1238
- return { command: localBin, args: config.args(testFile, cwd).slice(1) };
1674
+ return { command: localBin, args: stripWrapperArgs(binName, config.args(testFile, cwd)) };
1239
1675
  }
1240
1676
  // Any package manager's global bin dir (npm/pnpm/yarn/bun) before npx (#375).
1241
1677
  const globalBin = await findGlobalBinary(binName);
1242
1678
  if (globalBin) {
1243
- return { command: globalBin, args: config.args(testFile, cwd).slice(1) };
1679
+ return { command: globalBin, args: stripWrapperArgs(binName, config.args(testFile, cwd)) };
1244
1680
  }
1245
1681
  return { command: config.command, args: config.args(testFile, cwd) };
1246
1682
  }
@@ -1253,7 +1689,8 @@ export class TestRunnerClient {
1253
1689
  failed: 0,
1254
1690
  skipped: 0,
1255
1691
  failures: [],
1256
- duration: 0,
1692
+ // #1479: no duration key at all. Nothing ran, so there is nothing
1693
+ // to report — this used to say 0, which reads as "ran, instantly".
1257
1694
  error,
1258
1695
  };
1259
1696
  }