@herbertgao/pi-extensions 2026.8.4 → 2026.8.5

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 (288) hide show
  1. package/README.md +4 -4
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  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/index.ts +7 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  13. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  14. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  15. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  16. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  20. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  21. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  22. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  30. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  31. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  32. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  33. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  34. package/node_modules/pi-lens/README.md +13 -0
  35. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  36. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  37. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  39. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  40. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  41. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  42. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  43. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  44. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  45. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  46. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  47. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  51. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  52. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  53. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  54. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  56. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  57. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  58. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  59. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  60. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  63. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  64. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  66. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  67. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  68. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  69. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  70. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  71. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  72. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  73. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  75. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  99. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  100. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  101. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  102. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  103. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  104. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  105. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  106. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  107. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  108. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  109. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  110. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  111. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  112. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  113. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  114. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  116. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  117. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  118. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  120. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  121. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  122. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  123. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  124. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  125. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  126. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  127. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  128. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  129. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  130. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  131. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  132. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  134. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  135. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  136. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  137. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  138. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  139. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  140. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  141. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  142. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  143. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  144. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  145. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  146. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  147. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  148. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  149. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  150. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  151. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  152. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  153. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  154. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  155. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  157. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  158. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  159. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  160. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  161. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  162. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  163. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  164. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  165. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  166. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  167. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  168. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  169. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  170. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  171. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  172. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  173. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  174. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  175. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  177. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  178. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  179. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  180. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  181. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  182. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  183. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  184. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  185. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  186. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  187. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  188. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  189. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  190. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  191. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  192. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  193. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  194. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  196. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  197. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  198. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  199. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  200. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  201. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  202. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  203. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  204. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  205. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  210. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  211. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  212. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  213. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  214. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  215. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  216. package/node_modules/pi-lens/dist/index.js +49361 -36888
  217. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  218. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  219. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  220. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  221. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  222. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  223. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  224. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  225. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  226. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  227. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  228. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  229. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  230. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  231. package/node_modules/pi-lens/docs/fable.md +27 -0
  232. package/node_modules/pi-lens/docs/features.md +31 -2
  233. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  234. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  235. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  236. package/node_modules/pi-lens/docs/mcp.md +37 -4
  237. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  238. package/node_modules/pi-lens/docs/settings.md +12 -1
  239. package/node_modules/pi-lens/docs/tools.md +4 -1
  240. package/node_modules/pi-lens/docs/usage.md +18 -0
  241. package/node_modules/pi-lens/package.json +10 -10
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  252. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  253. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  254. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  255. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  256. package/node_modules/pi-mcp-adapter/README.md +34 -3
  257. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  258. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  259. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  260. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  261. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  262. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  263. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  264. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  265. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  266. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  267. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  268. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  269. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  270. package/node_modules/pi-mcp-adapter/package.json +3 -3
  271. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  272. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  273. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  274. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  275. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  276. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  277. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  278. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  279. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  280. package/package.json +11 -12
  281. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  282. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  283. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  284. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  285. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  286. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  287. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  288. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -9,10 +9,18 @@
9
9
  *
10
10
  * Errors are swallowed best-effort, matching every current logger. A
11
11
  * best-effort SYNC flush is registered on `process.on("exit")` (appendFileSync
12
- * is fine at exit — not the hot path; no child spawning, #234).
12
+ * is fine at exit — not the hot path; no child spawning, #234). Normal drains
13
+ * use promise-based mkdir/append/truncate, while rotation stays synchronous
14
+ * inside the already-deferred drain so it cannot race a flushSync rename. A
15
+ * The shared writer state uses the `NDJSON_GLOBAL_STATE_SCHEMA` protocol:
16
+ * version 1 (the 6a8a0994 shape) is upgraded in place to current version 2,
17
+ * retaining its queues while replacing stale exit flushers. A pre-7e4b9120
18
+ * private-queue shape is fenced rather than adopted, because its queues cannot
19
+ * be migrated safely.
13
20
  */
14
21
  import * as fs from "node:fs";
15
22
  import * as path from "node:path";
23
+ import { normalizeFilePath } from "./path-utils.js";
16
24
  import { redactSecrets } from "./redact/secrets.js";
17
25
  function resolve(v) {
18
26
  return typeof v === "function" ? v() : v;
@@ -25,12 +33,81 @@ function runBestEffort(operation) {
25
33
  return;
26
34
  }
27
35
  }
28
- // One shared exit handler flushes every logger — avoids an EventEmitter
29
- // MaxListeners warning once more than ~10 loggers exist (we ship eight, plus
30
- // diagnostic + test instances). No child spawning at teardown (#234).
31
- const exitFlushers = new Set();
32
- let exitHandlerRegistered = false;
33
- /** Test-only view of the registered exit flushers (see ndjson-logger.test.ts). */
36
+ const NDJSON_GLOBAL_STATE_SCHEMA = "pi-lens.ndjson-logger.state";
37
+ const NDJSON_LEGACY_GLOBAL_STATE_VERSION = 1;
38
+ const NDJSON_GLOBAL_STATE_VERSION = 2;
39
+ const NDJSON_GLOBAL_STATE_KEY = Symbol.for("pi-lens.ndjson-logger.state");
40
+ const globalStateHost = globalThis;
41
+ const existingGlobalState = globalStateHost[NDJSON_GLOBAL_STATE_KEY];
42
+ function isSharedWriterState(value) {
43
+ if (!value || typeof value !== "object")
44
+ return false;
45
+ const candidate = value;
46
+ const knownSchema = candidate.schema === undefined ||
47
+ candidate.schema === NDJSON_GLOBAL_STATE_SCHEMA;
48
+ const knownVersion = candidate.version === undefined ||
49
+ candidate.version === NDJSON_LEGACY_GLOBAL_STATE_VERSION ||
50
+ candidate.version === NDJSON_GLOBAL_STATE_VERSION;
51
+ return (knownSchema &&
52
+ knownVersion &&
53
+ candidate.writers instanceof Map &&
54
+ candidate.exitFlushers instanceof Set &&
55
+ candidate.registeredLogFiles instanceof Set &&
56
+ typeof candidate.exitHandlerRegistered === "boolean");
57
+ }
58
+ function isLegacyGlobalState(value) {
59
+ if (!value || typeof value !== "object")
60
+ return false;
61
+ const candidate = value;
62
+ return (candidate.exitFlushers instanceof Set &&
63
+ candidate.registeredLogFiles instanceof Set &&
64
+ typeof candidate.exitHandlerRegistered === "boolean");
65
+ }
66
+ let ndjsonGlobalState;
67
+ let legacyGlobalState;
68
+ if (isSharedWriterState(existingGlobalState)) {
69
+ // The 7e4b9120 graph had no version marker and 6a8a0994 used version 1.
70
+ // Both shapes are bridgeable: retain their queues, but replace every stale
71
+ // exit flusher with a closure from this module graph. A current state is
72
+ // left untouched so it keeps exactly one canonical flusher per writer.
73
+ const needsFlusherMigration = existingGlobalState.schema !== NDJSON_GLOBAL_STATE_SCHEMA ||
74
+ existingGlobalState.version !== NDJSON_GLOBAL_STATE_VERSION;
75
+ if (needsFlusherMigration) {
76
+ for (const state of existingGlobalState.writers.values()) {
77
+ const staleExitFlusher = state.exitFlusher;
78
+ const currentExitFlusher = () => flushStateSync(state);
79
+ state.exitFlusher = currentExitFlusher;
80
+ existingGlobalState.exitFlushers.delete(staleExitFlusher);
81
+ existingGlobalState.exitFlushers.add(currentExitFlusher);
82
+ }
83
+ }
84
+ existingGlobalState.schema = NDJSON_GLOBAL_STATE_SCHEMA;
85
+ existingGlobalState.version = NDJSON_GLOBAL_STATE_VERSION;
86
+ ndjsonGlobalState = existingGlobalState;
87
+ }
88
+ else if (existingGlobalState === undefined) {
89
+ ndjsonGlobalState = (globalStateHost[NDJSON_GLOBAL_STATE_KEY] = {
90
+ schema: NDJSON_GLOBAL_STATE_SCHEMA,
91
+ version: NDJSON_GLOBAL_STATE_VERSION,
92
+ writers: new Map(),
93
+ exitFlushers: new Set(),
94
+ exitHandlerRegistered: false,
95
+ registeredLogFiles: new Set(),
96
+ });
97
+ }
98
+ else if (isLegacyGlobalState(existingGlobalState)) {
99
+ // Do not mutate or replace this state: its private queues and exit flusher
100
+ // closures are not observable, so adopting it would falsely claim safety.
101
+ // New facades fail closed in requireCurrentGlobalState().
102
+ legacyGlobalState = existingGlobalState;
103
+ }
104
+ const exitFlushers = ndjsonGlobalState?.exitFlushers ??
105
+ legacyGlobalState?.exitFlushers ??
106
+ new Set();
107
+ const registeredLogFiles = ndjsonGlobalState?.registeredLogFiles ??
108
+ legacyGlobalState?.registeredLogFiles ??
109
+ new Set();
110
+ /** Test-only view of the canonical per-file exit flushers (see ndjson-logger.test.ts). */
34
111
  export function _exitFlushersForTest() {
35
112
  return exitFlushers;
36
113
  }
@@ -46,7 +123,6 @@ export function _exitFlushersForTest() {
46
123
  // `logs/{date}.jsonl`) is deliberately NOT registered: those already live
47
124
  // under the `logs/` subdirectory and are covered by log-cleanup's separate
48
125
  // `*.jsonl` daily-log sweep, not the single-file rotation list.
49
- const registeredLogFiles = new Set();
50
126
  /** Every absolute path registered by a static-filePath createNdjsonLogger instance. */
51
127
  export function getRegisteredLogFiles() {
52
128
  return registeredLogFiles;
@@ -55,133 +131,255 @@ export function getRegisteredLogFiles() {
55
131
  export function _resetRegisteredLogFilesForTest() {
56
132
  registeredLogFiles.clear();
57
133
  }
58
- function registerExitFlusher(flushSync) {
59
- exitFlushers.add(flushSync);
60
- if (!exitHandlerRegistered) {
61
- exitHandlerRegistered = true;
134
+ function requireCurrentGlobalState() {
135
+ if (ndjsonGlobalState)
136
+ return ndjsonGlobalState;
137
+ throw new Error("createNdjsonLogger: incompatible module graph state; a pre-7e4b9120 " +
138
+ "logger graph may still own private queues. Restart the process before " +
139
+ "loading the current logger graph");
140
+ }
141
+ function registerWriter(state) {
142
+ const globalState = requireCurrentGlobalState();
143
+ if (!exitFlushers.has(state.exitFlusher))
144
+ exitFlushers.add(state.exitFlusher);
145
+ if (!globalState.exitHandlerRegistered) {
146
+ globalState.exitHandlerRegistered = true;
62
147
  process.on("exit", () => {
63
148
  for (const flush of exitFlushers)
64
149
  runBestEffort(flush);
65
150
  });
66
151
  }
67
152
  }
68
- export function createNdjsonLogger(options) {
69
- if (typeof options.filePath === "string") {
70
- registeredLogFiles.add(options.filePath);
71
- }
72
- const queue = [];
73
- let drainPromise = null;
74
- let inFlightBatch = null;
75
- let ensuredDir = false;
76
- function ensureDir(file) {
77
- if (ensuredDir)
78
- return;
79
- runBestEffort(() => {
80
- fs.mkdirSync(path.dirname(file), { recursive: true });
81
- ensuredDir = true;
82
- });
153
+ function normalizeLogPath(file) {
154
+ return normalizeFilePath(path.resolve(file));
155
+ }
156
+ function assertCompatibleWriterOptions(existing, maxBytes, backupPath) {
157
+ if (existing.maxBytes === maxBytes && existing.backupPath === backupPath)
158
+ return;
159
+ throw new Error(`createNdjsonLogger: incompatible options for shared path ${existing.file}; ` +
160
+ `the first writer's maxBytes/backupPath must be reused`);
161
+ }
162
+ function applyQueueItemSync(state, item) {
163
+ ensureDirSync(state);
164
+ runBestEffort(() => {
165
+ if (item.kind === "truncate") {
166
+ fs.writeFileSync(state.file, "");
167
+ }
168
+ else {
169
+ rotateIfNeeded(state);
170
+ fs.appendFileSync(state.file, item.line);
171
+ }
172
+ });
173
+ }
174
+ function flushStateSync(state) {
175
+ // Drain the in-memory queue synchronously — safe at process exit.
176
+ // The in-flight async batch is INCLUDED even though its appendFile may
177
+ // also land: if the process dies before the threadpool issues that
178
+ // write, skipping the prefix would drop the whole batch. The per-line
179
+ // writer deliberately traded duplicate lines at exit for never-drops
180
+ // (#935 review) — keep that trade. If a late in-flight append crosses a
181
+ // truncate, the post-truncate operations are replayed after that append
182
+ // completes so pre-truncate data cannot be reintroduced.
183
+ const inFlight = state.inFlightBatch;
184
+ const inFlightAtHead = inFlight !== null &&
185
+ inFlight.every((item, index) => state.queue[index] === item);
186
+ const repairStart = inFlightAtHead
187
+ ? inFlight?.[0]?.kind === "truncate"
188
+ ? 0
189
+ : state.queue.findIndex((item) => item.kind === "truncate")
190
+ : -1;
191
+ if (state.syncRepairItems) {
192
+ state.syncRepairItems.push(...state.queue);
193
+ }
194
+ else if (repairStart >= 0) {
195
+ state.syncRepairItems = state.queue.slice(repairStart);
196
+ }
197
+ while (state.queue.length > 0) {
198
+ const item = state.queue.shift();
199
+ applyQueueItemSync(state, item);
200
+ }
201
+ }
202
+ function createWriterState(file, maxBytes, backupPath) {
203
+ const globalState = requireCurrentGlobalState();
204
+ const existing = globalState.writers.get(file);
205
+ if (existing) {
206
+ // A partially initialized global state from another graph still needs to
207
+ // be enrolled, but it must never get a second queue or exit flusher.
208
+ assertCompatibleWriterOptions(existing, maxBytes, backupPath);
209
+ if (!exitFlushers.has(existing.exitFlusher))
210
+ registerWriter(existing);
211
+ return existing;
212
+ }
213
+ const state = {};
214
+ state.file = file;
215
+ state.maxBytes = maxBytes;
216
+ state.backupPath = backupPath;
217
+ state.queue = [];
218
+ state.drainPromise = null;
219
+ state.inFlightBatch = null;
220
+ state.syncRepairItems = null;
221
+ state.ensuredDir = false;
222
+ state.exitFlusher = () => flushStateSync(state);
223
+ globalState.writers.set(file, state);
224
+ registerWriter(state);
225
+ return state;
226
+ }
227
+ function ensureDirSync(state) {
228
+ if (state.ensuredDir)
229
+ return;
230
+ runBestEffort(() => {
231
+ fs.mkdirSync(path.dirname(state.file), { recursive: true });
232
+ state.ensuredDir = true;
233
+ });
234
+ }
235
+ async function ensureDirAsync(state) {
236
+ if (state.ensuredDir)
237
+ return;
238
+ try {
239
+ await fs.promises.mkdir(path.dirname(state.file), { recursive: true });
240
+ state.ensuredDir = true;
241
+ }
242
+ catch {
243
+ // telemetry is best-effort
83
244
  }
84
- function rotateIfNeeded(file) {
85
- if (options.maxBytes === undefined)
245
+ }
246
+ function rotateIfNeeded(state) {
247
+ if (state.maxBytes === undefined)
248
+ return;
249
+ try {
250
+ const size = fs.statSync(state.file).size;
251
+ if (size < state.maxBytes)
86
252
  return;
87
- try {
88
- const size = fs.statSync(file).size;
89
- if (size < options.maxBytes)
90
- return;
91
- const backup = options.backupPath
92
- ? resolve(options.backupPath)
93
- : `${file}.1`;
94
- runBestEffort(() => fs.rmSync(backup, { force: true }));
95
- fs.renameSync(file, backup);
253
+ const backup = state.backupPath ?? `${state.file}.1`;
254
+ runBestEffort(() => fs.rmSync(backup, { force: true }));
255
+ fs.renameSync(state.file, backup);
256
+ }
257
+ catch {
258
+ // no file yet, or rename raced — nothing to rotate
259
+ }
260
+ }
261
+ async function applyQueueItemAsync(state, item) {
262
+ await ensureDirAsync(state);
263
+ try {
264
+ if (item.kind === "truncate") {
265
+ await fs.promises.writeFile(state.file, "");
96
266
  }
97
- catch {
98
- // no file yet, or rename raced — nothing to rotate
267
+ else {
268
+ // Rotation is deliberately synchronous here. This function is only
269
+ // reached from the already-deferred drain, and keeping stat/rm/rename
270
+ // in one synchronous section prevents flushSync from racing a late
271
+ // async rename after it has written new data.
272
+ rotateIfNeeded(state);
273
+ await fs.promises.appendFile(state.file, item.line);
99
274
  }
100
275
  }
101
- async function drainLoop() {
102
- // Peek, write, then remove — an item stays in the queue until it is on
103
- // disk, so a teardown flushSync (which abandons this async loop) never
104
- // drops an item this loop had already dequeued but not yet written.
105
- while (queue.length > 0) {
106
- const item = queue[0];
107
- const file = resolve(options.filePath);
108
- ensureDir(file);
109
- const truncateIndex = item.kind === "line"
110
- ? queue.findIndex((queued) => queued.kind === "truncate")
111
- : 0;
112
- const pendingEnd = truncateIndex === -1 ? queue.length : truncateIndex;
113
- const pending = item.kind === "truncate" ? [item] : queue.slice(0, pendingEnd);
114
- inFlightBatch = pending;
115
- try {
116
- if (item.kind === "truncate") {
117
- await fs.promises.writeFile(file, "");
118
- }
119
- else {
120
- rotateIfNeeded(file);
121
- await fs.promises.appendFile(file, pending
122
- .map((queued) => queued.line)
123
- .join(""));
124
- }
276
+ catch {
277
+ // telemetry is best-effort
278
+ }
279
+ }
280
+ async function drainLoop(state) {
281
+ // Peek, write, then remove — an item stays in the queue until it is on
282
+ // disk, so a teardown flushSync (which abandons this async loop) never
283
+ // drops an item this loop had already dequeued but not yet written.
284
+ while (state.queue.length > 0) {
285
+ const item = state.queue[0];
286
+ await ensureDirAsync(state);
287
+ const truncateIndex = item.kind === "line"
288
+ ? state.queue.findIndex((queued) => queued.kind === "truncate")
289
+ : 0;
290
+ const pendingEnd = truncateIndex === -1 ? state.queue.length : truncateIndex;
291
+ const pending = item.kind === "truncate"
292
+ ? [item]
293
+ : state.queue.slice(0, pendingEnd);
294
+ state.inFlightBatch = pending;
295
+ try {
296
+ if (item.kind === "truncate") {
297
+ await fs.promises.writeFile(state.file, "");
125
298
  }
126
- catch {
127
- // telemetry is best-effort
299
+ else {
300
+ // Rotation is kept synchronous inside the deferred drain. The
301
+ // append remains async, but no awaited rotation step can run after
302
+ // flushSync has written to the active file.
303
+ rotateIfNeeded(state);
304
+ await fs.promises.appendFile(state.file, pending
305
+ .map((queued) => queued.line)
306
+ .join(""));
128
307
  }
129
- for (const written of pending) {
130
- // flushSync may have drained this prefix while the append was in
131
- // flight. Never remove newer items from a later enqueue.
132
- if (queue[0] !== written)
133
- break;
134
- queue.shift();
308
+ }
309
+ catch {
310
+ // telemetry is best-effort
311
+ }
312
+ for (const written of pending) {
313
+ // flushSync may have drained this prefix while the append is in
314
+ // flight. Never remove newer items from a later enqueue.
315
+ if (state.queue[0] !== written)
316
+ break;
317
+ state.queue.shift();
318
+ }
319
+ if (state.inFlightBatch === pending) {
320
+ state.inFlightBatch = null;
321
+ const repairItems = state.syncRepairItems;
322
+ state.syncRepairItems = null;
323
+ if (repairItems) {
324
+ for (const repairItem of repairItems) {
325
+ await applyQueueItemAsync(state, repairItem);
326
+ }
135
327
  }
136
- if (inFlightBatch === pending)
137
- inFlightBatch = null;
138
328
  }
139
329
  }
140
- function drain() {
141
- // Serialize: a single in-flight drain owns the queue. flush() awaits this
142
- // same promise, so it never resolves before pending writes land. The loop
143
- // re-checks queue.length, so items enqueued mid-drain are picked up before
144
- // the promise settles — no stranded item, no second concurrent drainer.
145
- if (!drainPromise) {
146
- drainPromise = Promise.resolve()
147
- .then(drainLoop)
148
- .finally(() => {
149
- drainPromise = null;
150
- });
151
- }
152
- return drainPromise;
330
+ }
331
+ function drain(state) {
332
+ // Serialize: a single in-flight drain owns the canonical per-file queue.
333
+ // This guard lives in global state, so module re-evaluation cannot create a
334
+ // second drainer for the same path.
335
+ if (!state.drainPromise) {
336
+ state.drainPromise = Promise.resolve()
337
+ .then(() => drainLoop(state))
338
+ .finally(() => {
339
+ state.drainPromise = null;
340
+ });
341
+ }
342
+ return state.drainPromise;
343
+ }
344
+ export function createNdjsonLogger(options) {
345
+ const states = new Set();
346
+ // Static logger paths are canonicalized once. Lazy diagnostic paths are
347
+ // resolved at enqueue time, but each resolved raw path is canonicalized only
348
+ // once (normally once per date), keeping realpath work off the hot path.
349
+ const canonicalPaths = new Map();
350
+ const staticFile = typeof options.filePath === "string"
351
+ ? normalizeLogPath(options.filePath)
352
+ : undefined;
353
+ const staticBackupPath = typeof options.backupPath === "string" && options.maxBytes !== undefined
354
+ ? normalizeLogPath(options.backupPath)
355
+ : undefined;
356
+ function canonicalPath(file) {
357
+ const raw = path.resolve(file);
358
+ const cached = canonicalPaths.get(raw);
359
+ if (cached)
360
+ return cached;
361
+ const normalized = normalizeLogPath(raw);
362
+ canonicalPaths.set(raw, normalized);
363
+ return normalized;
364
+ }
365
+ function stateForCall() {
366
+ const file = staticFile ?? canonicalPath(resolve(options.filePath));
367
+ const backupPath = options.maxBytes !== undefined && options.backupPath
368
+ ? (staticBackupPath ?? canonicalPath(resolve(options.backupPath)))
369
+ : undefined;
370
+ const state = createWriterState(file, options.maxBytes, backupPath);
371
+ states.add(state);
372
+ return state;
373
+ }
374
+ if (staticFile !== undefined) {
375
+ const state = stateForCall();
376
+ registeredLogFiles.add(state.file);
153
377
  }
154
378
  function enqueue(item) {
155
- queue.push(item);
156
- void drain();
157
- }
158
- function flushSync() {
159
- // Drain the in-memory queue synchronously — safe at process exit.
160
- // The in-flight async batch is INCLUDED even though its appendFile may
161
- // also land: if the process dies before the threadpool issues that
162
- // write, skipping the prefix would drop the whole batch. The per-line
163
- // writer deliberately traded duplicate lines at exit for never-drops
164
- // (#935 review) — keep that trade. The drain-loop completion handler
165
- // only shifts items still at the queue head (identity-checked), so a
166
- // queue emptied here is simply left alone by the async loop.
167
- while (queue.length > 0) {
168
- const item = queue.shift();
169
- const file = resolve(options.filePath);
170
- ensureDir(file);
171
- runBestEffort(() => {
172
- if (item.kind === "truncate") {
173
- fs.writeFileSync(file, "");
174
- }
175
- else {
176
- rotateIfNeeded(file);
177
- fs.appendFileSync(file, item.line);
178
- }
179
- });
180
- }
379
+ const state = stateForCall();
380
+ state.queue.push(item);
381
+ void drain(state);
181
382
  }
182
- // Best-effort teardown flush of anything still buffered, via the single
183
- // shared exit handler. appendFileSync is fine here — not the hot path.
184
- registerExitFlusher(flushSync);
185
383
  return {
186
384
  log(obj) {
187
385
  const serialized = String(JSON.stringify(obj));
@@ -197,8 +395,11 @@ export function createNdjsonLogger(options) {
197
395
  enqueue({ kind: "truncate" });
198
396
  },
199
397
  async flush() {
200
- await drain();
398
+ await Promise.all([...states].map((state) => drain(state)));
399
+ },
400
+ flushSync() {
401
+ for (const state of states)
402
+ flushStateSync(state);
201
403
  },
202
- flushSync,
203
404
  };
204
405
  }
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Why: opengrep has no `workspace/diagnostic` pull support (push-only, per
12
12
  * `docs/servercapabilities.md`), and `reopenOnResync: true`
13
- * (`clients/lsp/server-strategies.ts`) means every LSP touch already forces a
13
+ * (`clients/lsp/wait-policy/strategies.ts`) means every LSP touch already forces a
14
14
  * full re-scan of that one file — there's no incremental efficiency lost by
15
15
  * moving bulk scans off the per-file touch loop. On a full sweep the old path
16
16
  * instead paid opengrep's full per-file wait-tier budget serially, one file at
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import { detectLineEnding, normalizeToLF, restoreLineEndings, } from "./host-edit-normalize.js";
3
+ import { boundedEditIndexes, createReadGuardEditBatchSummary, formatBoundedEditIndexes, logReadGuardEvent, } from "./read-guard-logger.js";
3
4
  function replaceOnce(content, oldText, newText) {
4
5
  const idx = content.indexOf(oldText);
5
6
  if (idx === -1)
@@ -16,6 +17,7 @@ function replaceOnce(content, oldText, newText) {
16
17
  * than logged as applied.
17
18
  */
18
19
  export async function applyPartiallyApplicableEdits(args) {
20
+ const startedAt = Date.now();
19
21
  const raw = fs.readFileSync(args.filePath, "utf-8");
20
22
  // Detect + restore line endings the way the host edit tool does:
21
23
  // first-occurrence-wins detection (not "any CRLF present") and lone-CR -> LF
@@ -24,22 +26,135 @@ export async function applyPartiallyApplicableEdits(args) {
24
26
  const ending = detectLineEnding(raw);
25
27
  let content = normalizeToLF(raw);
26
28
  const applied = [];
29
+ const skipped = [];
27
30
  for (const edit of args.edits) {
28
31
  const oldText = normalizeToLF(edit.oldText);
29
32
  const newText = normalizeToLF(edit.newText ?? "");
30
33
  const replaced = replaceOnce(content, oldText, newText);
31
- if (!replaced.changed)
34
+ if (!replaced.changed) {
35
+ skipped.push(edit.originalIndex);
32
36
  continue;
37
+ }
33
38
  content = replaced.content;
34
39
  applied.push(edit.originalIndex);
35
40
  }
36
- if (applied.length > 0) {
37
- fs.writeFileSync(args.filePath, restoreLineEndings(content, ending), "utf-8");
41
+ let commitStatus = applied.length > 0 ? "committed" : "no_changes";
42
+ try {
43
+ if (applied.length > 0) {
44
+ fs.writeFileSync(args.filePath, restoreLineEndings(content, ending), "utf-8");
45
+ }
38
46
  }
39
- const postEditOutput = applied.length > 0 ? await args.afterWrite?.() : undefined;
47
+ catch (error) {
48
+ commitStatus = "failed";
49
+ if (args.summary || args.correlationId) {
50
+ const summary = createReadGuardEditBatchSummary({
51
+ ...(args.summary ?? {
52
+ requestedIndexes: boundedEditIndexes(args.edits.slice(0, 100).map((edit) => edit.originalIndex)),
53
+ requestedTotal: args.edits.length,
54
+ }),
55
+ appliedIndexes: [],
56
+ appliedTotal: 0,
57
+ participantIds: args.correlationId ? [args.correlationId] : undefined,
58
+ participantTotal: args.correlationId ? 1 : undefined,
59
+ commitStatus,
60
+ terminalStatus: "failed",
61
+ durationMs: Date.now() - startedAt,
62
+ });
63
+ logReadGuardEvent({
64
+ event: "edit_batch_summary",
65
+ correlationId: args.correlationId,
66
+ filePath: args.filePath,
67
+ metadata: { tool: "edit", editBatchSummary: summary },
68
+ });
69
+ }
70
+ throw error;
71
+ }
72
+ let postEditOutput;
73
+ let postEditStatus = "not_run";
74
+ if (applied.length > 0 && args.afterWrite) {
75
+ try {
76
+ postEditOutput = await args.afterWrite();
77
+ postEditStatus = "succeeded";
78
+ }
79
+ catch (error) {
80
+ // The write is committed. Preserve the existing caller behavior for
81
+ // uninstrumented callers; the read-guard path records and handles it.
82
+ postEditStatus = "failed";
83
+ if (!args.summary && !args.correlationId)
84
+ throw error;
85
+ }
86
+ }
87
+ if (!args.summary && !args.correlationId) {
88
+ return {
89
+ appliedCount: applied.length,
90
+ appliedIndices: formatBoundedEditIndexes(applied),
91
+ postEditOutput,
92
+ };
93
+ }
94
+ const baseSummary = args.summary ??
95
+ createReadGuardEditBatchSummary({
96
+ requestedIndexes: boundedEditIndexes(args.edits.slice(0, 100).map((edit) => edit.originalIndex)),
97
+ requestedTotal: args.edits.length,
98
+ resolvedIndexes: boundedEditIndexes(args.edits.slice(0, 100).map((edit) => edit.originalIndex)),
99
+ resolvedTotal: args.edits.length,
100
+ });
101
+ const summary = createReadGuardEditBatchSummary({
102
+ ...baseSummary,
103
+ rejectedReasons: [
104
+ ...baseSummary.rejectedReasons,
105
+ ...skipped.slice(0, 100).map((index) => ({
106
+ index,
107
+ code: "replace_once_skipped",
108
+ })),
109
+ ],
110
+ rejectedTotal: baseSummary.rejectedTotal + skipped.length,
111
+ appliedIndexes: applied,
112
+ appliedTotal: applied.length,
113
+ participantIds: args.correlationId
114
+ ? [...baseSummary.participantIds, args.correlationId]
115
+ : baseSummary.participantIds,
116
+ participantTotal: args.correlationId
117
+ ? baseSummary.participantTotal + 1
118
+ : baseSummary.participantTotal,
119
+ commitStatus,
120
+ postEditStatus,
121
+ terminalStatus: postEditStatus === "failed"
122
+ ? "failed"
123
+ : applied.length === 0
124
+ ? "skipped"
125
+ : "success",
126
+ durationMs: Date.now() - startedAt,
127
+ });
128
+ if (postEditStatus === "failed") {
129
+ logReadGuardEvent({
130
+ event: "edit_post_edit_pipeline_failed",
131
+ correlationId: args.correlationId,
132
+ filePath: args.filePath,
133
+ metadata: {
134
+ tool: "edit",
135
+ commitStatus,
136
+ appliedCount: applied.length,
137
+ appliedIndexes: applied.slice(0, 100),
138
+ appliedTotal: applied.length,
139
+ },
140
+ });
141
+ }
142
+ logReadGuardEvent({
143
+ event: "edit_batch_summary",
144
+ correlationId: args.correlationId,
145
+ filePath: args.filePath,
146
+ metadata: { tool: "edit", editBatchSummary: summary },
147
+ });
40
148
  return {
41
149
  appliedCount: applied.length,
42
- appliedIndices: applied.map((index) => `edits[${index}]`).join(", "),
150
+ appliedTotal: applied.length,
151
+ appliedIndices: formatBoundedEditIndexes(applied),
43
152
  postEditOutput,
153
+ skippedCount: skipped.length,
154
+ skippedTotal: skipped.length,
155
+ skippedIndices: formatBoundedEditIndexes(skipped),
156
+ indexesTruncated: applied.length > 100 || skipped.length > 100 || summary.indexesTruncated,
157
+ postEditStatus,
158
+ summary,
44
159
  };
45
160
  }