@herbertgao/pi-extensions 2026.9.0 → 2026.9.1

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 (293) hide show
  1. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
  2. package/node_modules/@czottmann/pi-automode/README.md +20 -0
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
  11. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  12. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  13. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  14. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  15. package/node_modules/pi-lens/CHANGELOG.md +988 -0
  16. package/node_modules/pi-lens/README.md +3 -0
  17. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  18. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  19. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  22. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  23. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  24. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
  25. package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
  26. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  27. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
  28. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  29. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  30. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
  31. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  32. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
  33. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  34. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  35. package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
  36. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  37. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
  38. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  39. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  40. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  41. package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
  42. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  43. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  44. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  45. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
  46. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  47. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  48. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  49. package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
  50. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  51. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  52. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  53. package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
  54. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  55. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  56. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
  57. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  58. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  59. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  60. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  63. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  64. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
  66. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  67. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  121. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
  122. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  123. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  124. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  125. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  126. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
  128. package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
  129. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  130. package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
  131. package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
  132. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  133. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
  134. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  135. package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
  136. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  137. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  138. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  139. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  140. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  141. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  142. package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
  143. package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
  144. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  145. package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
  146. package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
  147. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
  148. package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
  149. package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
  150. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
  151. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  153. package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
  154. package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
  155. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
  157. package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
  158. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
  159. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  160. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
  161. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  162. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  163. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  164. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
  165. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  167. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  168. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  171. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
  172. package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
  173. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  174. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  175. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
  177. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  178. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  179. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
  180. package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
  181. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  182. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  183. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  184. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  185. package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
  186. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  187. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  188. package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
  189. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  190. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  191. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  192. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  193. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  194. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  195. package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
  196. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  197. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  198. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  199. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  200. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  201. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  202. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
  203. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  204. package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
  205. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  206. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
  207. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  208. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  209. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  210. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  211. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
  212. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  213. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  214. package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
  215. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
  216. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  217. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  218. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  219. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  220. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
  221. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  222. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  223. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  224. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  225. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  226. package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
  227. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  228. package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
  229. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  230. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  231. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  232. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
  233. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  234. package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
  237. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  238. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  239. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  240. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  241. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  242. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
  243. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  244. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
  245. package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
  246. package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
  247. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  248. package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
  249. package/node_modules/pi-lens/dist/index.js +64335 -54300
  250. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  251. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  252. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  253. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  254. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  255. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
  256. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
  257. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  258. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  259. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  260. package/node_modules/pi-lens/docs/configuration.md +222 -0
  261. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  262. package/node_modules/pi-lens/docs/features.md +44 -5
  263. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  264. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  265. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  266. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
  267. package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
  268. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  269. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  270. package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
  271. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  272. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  273. package/node_modules/pi-lens/package.json +15 -23
  274. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  275. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  276. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  277. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  278. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  279. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  280. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  281. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  282. package/node_modules/pi-web-access/README.md +62 -15
  283. package/node_modules/pi-web-access/curator-page.ts +3 -1
  284. package/node_modules/pi-web-access/curator-server.ts +5 -1
  285. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  286. package/node_modules/pi-web-access/github-extract.ts +242 -1
  287. package/node_modules/pi-web-access/index.ts +118 -64
  288. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  289. package/node_modules/pi-web-access/package.json +2 -2
  290. package/node_modules/pi-web-access/perplexity.ts +14 -1
  291. package/node_modules/pi-web-access/utils.ts +23 -6
  292. package/node_modules/pi-web-access/xai-search.ts +96 -33
  293. package/package.json +5 -5
@@ -1,24 +1,34 @@
1
+ import { createHash } from "node:crypto";
1
2
  import * as nodeFs from "node:fs";
3
+ import { BoundedFifoMap } from "./bounded-cache.js";
2
4
  import { hostWouldApplyOldText, normalizeForGuardMatch, normalizeToLF, stripBom, } from "./host-edit-normalize.js";
3
- import { boundedIndexesForCount, createReadGuardEditBatchSummary, logReadGuardEvent, } from "./read-guard-logger.js";
4
- import { isToolCallEventType } from "./tool-event.js";
5
+ import { classifyMutatingTool } from "./mutating-tool.js";
6
+ import { isExactAppliedRetry, } from "./partial-edit-apply.js";
7
+ import { boundedEditIndexes, createReadGuardEditBatchSummary, logReadGuardEvent, } from "./read-guard-logger.js";
5
8
  // Track repeated oldtext_not_found failures per (filePath, preview) to escalate messages.
6
- const recentOldTextFailures = new Map();
9
+ export const MAX_FAILURE_TRACKER_SIZE = 200;
10
+ const recentOldTextFailures = new BoundedFifoMap(MAX_FAILURE_TRACKER_SIZE);
7
11
  const REPEAT_FAILURE_TTL_MS = 300_000;
8
- const MAX_FAILURE_TRACKER_SIZE = 200;
9
12
  function trackOldTextFailure(filePath, preview) {
10
13
  const key = `${filePath}::${preview}`;
11
14
  const now = Date.now();
12
15
  const prev = recentOldTextFailures.get(key);
13
16
  const count = prev && now - prev.lastTs < REPEAT_FAILURE_TTL_MS ? prev.count + 1 : 1;
14
- if (recentOldTextFailures.size >= MAX_FAILURE_TRACKER_SIZE) {
15
- const oldest = recentOldTextFailures.keys().next().value;
16
- if (oldest !== undefined)
17
- recentOldTextFailures.delete(oldest);
18
- }
19
17
  recentOldTextFailures.set(key, { count, lastTs: now });
20
18
  return count;
21
19
  }
20
+ /** #2442 test-only: exercise the bounded failure tracker's capacity
21
+ * eviction directly, without a full read-guard preflight call. */
22
+ export function _trackOldTextFailureForTests(filePath, preview) {
23
+ return trackOldTextFailure(filePath, preview);
24
+ }
25
+ /** #2442 test-only: was this (filePath, preview) pair's escalation count
26
+ * reset by capacity eviction? (A fresh `_trackOldTextFailureForTests` call
27
+ * answering 1 again, after previously answering >1, means yes — but this
28
+ * reads the tracker's residency directly instead.) */
29
+ export function _hasOldTextFailureForTests(filePath, preview) {
30
+ return recentOldTextFailures.has(`${filePath}::${preview}`);
31
+ }
22
32
  function findFirstLineOfOldText(content, oldText) {
23
33
  const firstLine = oldText.replace(/\r\n/g, "\n").split("\n")[0].trim();
24
34
  if (firstLine.length < 5)
@@ -130,138 +140,42 @@ function normalizeContent(text) {
130
140
  function lineNumberAt(content, index) {
131
141
  return content.substring(0, index).split("\n").length;
132
142
  }
133
- function parseHashlineAnchor(anchor) {
134
- if (typeof anchor !== "string")
135
- return undefined;
136
- const trimmed = anchor.trim();
137
- const separator = trimmed.indexOf(":");
138
- const lineText = separator === -1 ? trimmed : trimmed.slice(0, separator);
139
- if (!/^\d+$/.test(lineText))
140
- return undefined;
141
- const line = Number(lineText);
142
- return Number.isInteger(line) && line > 0 ? line : undefined;
143
- }
144
- function combineRanges(ranges) {
145
- const starts = ranges.map(([start]) => start);
146
- const ends = ranges.map(([, end]) => end);
147
- return {
148
- touchedLines: [Math.min(...starts), Math.max(...ends)],
149
- editRanges: ranges.length > 1 ? ranges : undefined,
150
- };
151
- }
152
- function getHashlineOperations(input) {
153
- if (Array.isArray(input.operations))
154
- return input.operations;
155
- if (Array.isArray(input.ops))
156
- return input.ops;
157
- if (input.set_line || input.replace_lines || input.replace_symbol)
158
- return [input];
159
- return [];
160
- }
161
- function resolveHashlineEditInput(input, filePath, sessionId, correlationId) {
162
- const operations = getHashlineOperations(input);
163
- if (operations.length === 0)
164
- return undefined;
165
- const ranges = [];
166
- const errors = [];
167
- for (let index = 0; index < operations.length; index += 1) {
168
- const op = operations[index];
169
- if (op.set_line) {
170
- const payload = op.set_line;
171
- const line = parseHashlineAnchor(payload.anchor);
172
- if (!line) {
173
- errors.push(`operation[${index}].set_line.anchor is malformed`);
174
- continue;
175
- }
176
- ranges.push([line, line]);
143
+ /**
144
+ * Deduplicated overlapping pairs, in candidate order, for the overlap error
145
+ * message. Each overlapping edit appears in exactly one pair where possible;
146
+ * a chain (A-B-C all overlapping) reports A-B and A-C.
147
+ */
148
+ function overlapPairs(candidates) {
149
+ const pairs = [];
150
+ const reported = new Set();
151
+ for (let i = 0; i < candidates.length; i += 1) {
152
+ const a = candidates[i];
153
+ if (reported.has(a))
177
154
  continue;
178
- }
179
- if (op.replace_lines) {
180
- const payload = op.replace_lines;
181
- const start = parseHashlineAnchor(payload.start_anchor);
182
- const end = parseHashlineAnchor(payload.end_anchor);
183
- if (!start || !end) {
184
- errors.push(`operation[${index}].replace_lines anchors are malformed`);
185
- continue;
186
- }
187
- if (start > end) {
188
- errors.push(`operation[${index}].replace_lines range is inverted`);
155
+ for (let j = i + 1; j < candidates.length; j += 1) {
156
+ const b = candidates[j];
157
+ if (reported.has(b))
189
158
  continue;
159
+ if (b.spanStart < a.spanEnd && a.spanStart < b.spanEnd) {
160
+ pairs.push([a, b]);
161
+ reported.add(a);
162
+ reported.add(b);
163
+ break;
190
164
  }
191
- ranges.push([start, end]);
192
- continue;
193
- }
194
- if (op.replace_symbol) {
195
- errors.push(`operation[${index}].replace_symbol cannot be resolved safely yet; use line anchors or a native ranged edit`);
196
- continue;
197
165
  }
198
- errors.push(`operation[${index}] is not a recognized hashline edit`);
199
166
  }
200
- if (errors.length > 0) {
201
- const editBatchSummary = createReadGuardEditBatchSummary({
202
- requestedIndexes: boundedIndexesForCount(operations.length),
203
- requestedTotal: operations.length,
204
- rejectedReasons: boundedIndexesForCount(operations.length).map((index) => ({
205
- index,
206
- code: "preflight_blocked",
207
- })),
208
- rejectedTotal: operations.length,
209
- durationMs: 0,
210
- terminalStatus: "blocked",
211
- });
212
- if (filePath) {
213
- logReadGuardEvent({
214
- event: "edit_preflight_blocked",
215
- correlationId,
216
- sessionId,
217
- filePath,
218
- metadata: {
219
- tool: "edit",
220
- source: "hashline_edit",
221
- reasonKind: "unsupported_hashline_edit_target",
222
- operationCount: operations.length,
223
- errorCount: errors.length,
224
- errors: errors.slice(0, 10),
225
- },
226
- });
227
- logReadGuardEvent({
228
- event: "edit_batch_summary",
229
- correlationId,
230
- filePath,
231
- metadata: { tool: "edit", editBatchSummary },
232
- });
233
- }
234
- // Every blocking verdict ends with a single concrete next-action line so
235
- // the agent recovers in one turn (#328 — uniform recovery-hint discipline).
236
- const target = filePath ? `\`${filePath}\`` : "the file";
237
- const retryHint = `Re-read ${target} to get current #line anchors, then retry using set_line / replace_lines with those anchors — or use a native ranged edit.`;
238
- return {
239
- touchedLines: undefined,
240
- preflightError: `🔴 BLOCKED — Unsupported hashline edit target\n\n${errors.join("\n")}\n\n${retryHint}`,
241
- editBatchSummary,
242
- };
243
- }
244
- if (ranges.length === 0)
245
- return undefined;
246
- const result = combineRanges(ranges);
247
- if (filePath) {
248
- logReadGuardEvent({
249
- event: "touched_lines_detected",
250
- correlationId,
251
- sessionId,
252
- filePath,
253
- metadata: {
254
- tool: "edit",
255
- source: ranges.length === 1 && ranges[0][0] === ranges[0][1]
256
- ? "hashline_set_line"
257
- : "hashline_replace_lines",
258
- touchedLines: result.touchedLines,
259
- editRanges: result.editRanges,
260
- operationCount: operations.length,
261
- },
262
- });
263
- }
264
- return result;
167
+ return pairs;
168
+ }
169
+ /** ✅ note appended to preflight/composed messages for recognized retries. */
170
+ export function formatAlreadyAppliedNotes(editIndexes) {
171
+ if (!editIndexes || editIndexes.length === 0)
172
+ return "";
173
+ const list = boundedEditIndexes(editIndexes)
174
+ .map((index) => `edits[${index}]`)
175
+ .join(", ");
176
+ const suffix = editIndexes.length === 1 ? "was" : "were";
177
+ const pronoun = editIndexes.length === 1 ? "it" : "them";
178
+ return `\n\n✅ ${list} ${suffix} already applied by an identical earlier edit — the file already contains the result. Do NOT resubmit ${pronoun}.`;
265
179
  }
266
180
  function findOccurrenceLines(content, needle) {
267
181
  const lines = [];
@@ -320,23 +234,75 @@ function countRawOccurrences(content, needle) {
320
234
  }
321
235
  return count;
322
236
  }
323
- function exactOldTextForApply(rawContentLf, oldText, candidate) {
237
+ function exactOldTextForApply(rawContentLf, normalizedContent, oldText, candidate) {
324
238
  const oldTextLf = oldText.replace(/\r\n/g, "\n");
325
- if (countRawOccurrences(rawContentLf, oldTextLf) === 1)
326
- return oldTextLf;
239
+ if (countRawOccurrences(rawContentLf, oldTextLf) === 1) {
240
+ const start = rawContentLf.indexOf(oldTextLf);
241
+ return { text: oldTextLf, start, end: start + oldTextLf.length };
242
+ }
327
243
  if (candidate !== oldTextLf &&
328
244
  countRawOccurrences(rawContentLf, candidate) === 1) {
329
- return candidate;
245
+ const start = rawContentLf.indexOf(candidate);
246
+ return { text: candidate, start, end: start + candidate.length };
330
247
  }
331
- return undefined;
248
+ if (candidate !== oldTextLf)
249
+ return undefined;
250
+ const matchStart = normalizedContent.indexOf(candidate);
251
+ if (matchStart < 0)
252
+ return undefined;
253
+ const rawLines = rawContentLf.split("\n");
254
+ const normalizedLines = normalizedContent.split("\n");
255
+ const rawLineStarts = [];
256
+ let rawOffset = 0;
257
+ for (const line of rawLines) {
258
+ rawLineStarts.push(rawOffset);
259
+ rawOffset += line.length + 1;
260
+ }
261
+ const normalizedLineStarts = [];
262
+ let normalizedOffset = 0;
263
+ for (const line of normalizedLines) {
264
+ normalizedLineStarts.push(normalizedOffset);
265
+ normalizedOffset += line.length + 1;
266
+ }
267
+ const mapOffset = (offset, preferEnd) => {
268
+ const line = Math.min(normalizedLines.length - 1, normalizedContent.slice(0, offset).split("\n").length - 1);
269
+ const rawLine = rawLines[line] ?? "";
270
+ const bomOffset = line === 0 && rawLine.startsWith("\uFEFF") ? 1 : 0;
271
+ const rawMatchLine = rawLine.slice(bomOffset);
272
+ const target = offset - normalizedLineStarts[line];
273
+ const normalizedLine = normalizedLines[line] ?? "";
274
+ if (target >= normalizedLine.length) {
275
+ if (preferEnd)
276
+ return rawLineStarts[line] + rawLine.length;
277
+ return rawLineStarts[line] + bomOffset + rawMatchLine.length;
278
+ }
279
+ let lower = 0;
280
+ let upper = rawMatchLine.length;
281
+ for (let boundary = 0; boundary <= rawMatchLine.length; boundary += 1) {
282
+ const length = normalizeContent(rawMatchLine.slice(0, boundary)).length;
283
+ if (length === target)
284
+ return rawLineStarts[line] + bomOffset + boundary;
285
+ if (length < target)
286
+ lower = boundary;
287
+ else if (upper === rawMatchLine.length)
288
+ upper = boundary;
289
+ }
290
+ return rawLineStarts[line] + bomOffset + (preferEnd ? upper : lower);
291
+ };
292
+ const start = mapOffset(matchStart, false);
293
+ const end = mapOffset(matchStart + candidate.length, true);
294
+ const text = rawContentLf.slice(start, end);
295
+ return normalizeContent(text) === candidate
296
+ ? { text, start, end }
297
+ : undefined;
332
298
  }
333
- function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
299
+ function resolveOldTextEdits(edits, filePath, sessionId, correlationId, partialApplyRecords) {
334
300
  const startedAt = Date.now();
335
301
  const requestedIndexes = [];
336
302
  const logBatchEvent = (entry) => logReadGuardEvent({ ...entry, correlationId });
337
- let rawContent;
303
+ let rawBytes;
338
304
  try {
339
- rawContent = nodeFs.readFileSync(filePath, "utf-8");
305
+ rawBytes = nodeFs.readFileSync(filePath);
340
306
  }
341
307
  catch {
342
308
  const editBatchSummary = createReadGuardEditBatchSummary({
@@ -363,8 +329,16 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
363
329
  });
364
330
  return { touchedLines: undefined, editBatchSummary };
365
331
  }
366
- const rawContentLf = rawContent.replace(/\r\n/g, "\n");
332
+ const rawContent = rawBytes.toString("utf8");
333
+ const rawContentLf = normalizeToLF(rawContent);
367
334
  const content = normalizeContent(rawContent);
335
+ // #1053: the identity the spans below are resolved against. The apply step
336
+ // re-reads the file and rejects the whole batch when this no longer holds,
337
+ // so spans are never re-interpreted against changed bytes. The hash covers
338
+ // byte identity; spans carry their own bounds checks.
339
+ const snapshot = {
340
+ hash: createHash("sha256").update(rawBytes).digest("hex"),
341
+ };
368
342
  const errors = [];
369
343
  const failureKinds = [];
370
344
  const failedEditIndexes = [];
@@ -372,7 +346,10 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
372
346
  const rejectedReasons = [];
373
347
  const resolvedIndexes = [];
374
348
  const resolvedRanges = [];
349
+ const candidateEdits = [];
375
350
  const passedEdits = [];
351
+ let overlapRejectedCount = 0;
352
+ const alreadyAppliedEdits = [];
376
353
  let maxFailCount = 0;
377
354
  for (let i = 0; i < edits.length; i++) {
378
355
  const oldText = edits[i].oldText;
@@ -381,6 +358,38 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
381
358
  requestedIndexes.push(editIndex);
382
359
  if (!oldText)
383
360
  continue;
361
+ // Exact-retry recognition (#2402): an identical (oldText, newText) pair
362
+ // this session already applied is recognized from the applied record and
363
+ // confirmed by content evidence — never resolved again, never re-applied,
364
+ // never counted as a failure. This is deliberately NOT a global
365
+ // newText-present heuristic: the record only exists for pairs this
366
+ // process actually committed.
367
+ if (partialApplyRecords) {
368
+ const record = partialApplyRecords.find(filePath, oldText, edits[i].newText);
369
+ if (record &&
370
+ isExactAppliedRetry({
371
+ contentLf: rawContentLf,
372
+ oldKey: normalizeContent(oldText),
373
+ newKey: normalizeContent(edits[i].newText ?? ""),
374
+ contentHash: snapshot.hash,
375
+ expectedContentHash: record.contentHash,
376
+ expectedAfterWriteHash: record.afterWriteContentHash,
377
+ })) {
378
+ if (alreadyAppliedEdits.length < 100)
379
+ alreadyAppliedEdits.push(editIndex);
380
+ logBatchEvent({
381
+ event: "edit_already_applied_retry",
382
+ sessionId,
383
+ filePath,
384
+ metadata: {
385
+ tool: "edit",
386
+ source: "edits_without_ranges",
387
+ editIndex,
388
+ },
389
+ });
390
+ continue;
391
+ }
392
+ }
384
393
  let needle = normalizeContent(oldText);
385
394
  let occurrenceLines = findOccurrenceLines(content, needle);
386
395
  if (occurrenceLines.length === 0) {
@@ -503,28 +512,60 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
503
512
  else if (occurrenceLines.length === 1) {
504
513
  const startLine = occurrenceLines[0];
505
514
  const endLine = startLine + needle.split("\n").length - 1;
506
- resolvedRanges.push([startLine, endLine]);
507
- if (resolvedIndexes.length < 100)
508
- resolvedIndexes.push(editIndex);
509
- const applyOldText = exactOldTextForApply(rawContentLf, oldText, needle);
510
- if (applyOldText !== undefined) {
511
- passedEdits.push({
512
- oldText: applyOldText,
515
+ const applySpan = exactOldTextForApply(rawContentLf, content, oldText, needle);
516
+ if (applySpan !== undefined) {
517
+ // #1053: carry the exact span the preflight approved, plus the
518
+ // snapshot identity it was resolved against. The apply step
519
+ // consumes these instead of re-searching oldText.
520
+ candidateEdits.push({
521
+ oldText,
522
+ appliedSpanText: applySpan.text,
513
523
  newText: edits[i].newText,
514
524
  originalIndex: editIndex,
525
+ snapshot,
526
+ spanStart: applySpan.start,
527
+ spanEnd: applySpan.end,
528
+ });
529
+ resolvedRanges.push([startLine, endLine]);
530
+ if (resolvedIndexes.length < 100)
531
+ resolvedIndexes.push(editIndex);
532
+ }
533
+ else {
534
+ failureKinds.push("oldtext_unrepresentable");
535
+ if (failedEditIndexes.length < 100)
536
+ failedEditIndexes.push(editIndex);
537
+ if (rejectedReasons.length < 100) {
538
+ rejectedReasons.push({
539
+ index: editIndex,
540
+ code: "oldtext_unrepresentable",
541
+ });
542
+ }
543
+ errors.push(`edits[${editIndex}].oldText matched normalized content but could not be mapped to one raw file span; the whole batch must be rebuilt from the current file content.`);
544
+ logBatchEvent({
545
+ event: "edit_preflight_blocked",
546
+ sessionId,
547
+ filePath,
548
+ metadata: {
549
+ tool: "edit",
550
+ source: "edits_without_ranges",
551
+ reasonKind: "oldtext_unrepresentable",
552
+ editIndex,
553
+ },
554
+ });
555
+ }
556
+ if (applySpan !== undefined) {
557
+ logBatchEvent({
558
+ event: "oldtext_resolved",
559
+ sessionId,
560
+ filePath,
561
+ metadata: {
562
+ tool: "edit",
563
+ source: "edits_without_ranges",
564
+ editIndex,
565
+ touchedLines: [startLine, endLine],
566
+ },
515
567
  });
516
568
  }
517
- logBatchEvent({
518
- event: "oldtext_resolved",
519
- sessionId,
520
- filePath,
521
- metadata: {
522
- tool: "edit",
523
- source: "edits_without_ranges",
524
- editIndex,
525
- touchedLines: [startLine, endLine],
526
- },
527
- });
528
569
  }
529
570
  else {
530
571
  const preview = oldText.trimStart().substring(0, 60).replace(/\n/g, "↵");
@@ -554,13 +595,76 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
554
595
  });
555
596
  }
556
597
  }
598
+ // Overlap guard (#1053): individually-unique matches can still overlap
599
+ // (e.g. "abc" and "bcd" both matching inside "xabcy"). Applying both would
600
+ // splice corrupt content, so overlapping candidates are rejected together
601
+ // with one explicit message instead of being silently skipped.
602
+ const sortedCandidates = [...candidateEdits].sort((a, b) => a.spanStart - b.spanStart);
603
+ const overlaps = overlapPairs(sortedCandidates);
604
+ for (let i = 0; i < sortedCandidates.length; i += 1) {
605
+ const candidate = sortedCandidates[i];
606
+ const overlapping = sortedCandidates.find((other) => other !== candidate &&
607
+ other.spanStart < candidate.spanEnd &&
608
+ candidate.spanStart < other.spanEnd);
609
+ if (overlapping) {
610
+ overlapRejectedCount += 1;
611
+ if (rejectedReasons.length < 100) {
612
+ rejectedReasons.push({
613
+ index: candidate.originalIndex,
614
+ code: "span_overlap",
615
+ });
616
+ }
617
+ continue;
618
+ }
619
+ passedEdits.push(candidate);
620
+ }
621
+ for (const [a, b] of overlaps) {
622
+ errors.push(`edits[${a.originalIndex}] and edits[${b.originalIndex}] resolved to overlapping spans of the file — their oldTexts match regions that share bytes. Extend one oldText with a unique neighboring line so each edit targets one unambiguous span, or apply them in separate calls.`);
623
+ }
624
+ // Overlap is a batch-level safety failure. Do not partially commit an
625
+ // unrelated candidate from the same request while another pair is
626
+ // ambiguous; the apply seam promises all-or-nothing preflight rejection.
627
+ if (overlaps.length > 0)
628
+ passedEdits.length = 0;
557
629
  const oldTextEditCount = edits.filter((edit) => !!edit.oldText).length;
558
- if (errors.length > 0 || resolvedRanges.length !== oldTextEditCount) {
630
+ // Pure already-applied batch (#2402): every oldText edit is a recognized
631
+ // exact retry, nothing failed, nothing remains to apply. The verdict is the
632
+ // idempotent "already applied" note, never an escalating error.
633
+ if (errors.length === 0 &&
634
+ passedEdits.length === 0 &&
635
+ alreadyAppliedEdits.length > 0 &&
636
+ alreadyAppliedEdits.length === oldTextEditCount) {
637
+ const header = `✅ ALREADY APPLIED — every edit in this call was already applied by an identical earlier submission. Do NOT retry this payload; re-read the file if you need to build a new edit.`;
638
+ const details = alreadyAppliedEdits.map((editIndex) => `edits[${editIndex}]: already applied — do not resubmit.`);
639
+ const editBatchSummary = createReadGuardEditBatchSummary({
640
+ requestedIndexes,
641
+ requestedTotal: edits.length,
642
+ alreadyAppliedIndexes: boundedEditIndexes(alreadyAppliedEdits),
643
+ alreadyAppliedTotal: alreadyAppliedEdits.length,
644
+ durationMs: Date.now() - startedAt,
645
+ terminalStatus: "skipped",
646
+ });
647
+ logBatchEvent({
648
+ event: "edit_batch_summary",
649
+ correlationId,
650
+ filePath,
651
+ metadata: { tool: "edit", editBatchSummary },
652
+ });
653
+ return {
654
+ touchedLines: undefined,
655
+ preflightError: `${header}\n\n${details.join("\n")}`,
656
+ preflightHeader: header,
657
+ preflightDetails: details,
658
+ alreadyAppliedEdits,
659
+ editBatchSummary,
660
+ };
661
+ }
662
+ if (errors.length > 0 ||
663
+ alreadyAppliedEdits.length > 0 ||
664
+ resolvedRanges.length !== oldTextEditCount) {
559
665
  const failureDetails = errors.length > 0
560
666
  ? errors
561
- : [
562
- "One or more edit targets could not be resolved to exact lines. Re-read the relevant section and retry with the exact content as it appears in the file.",
563
- ];
667
+ : alreadyAppliedEdits.map((editIndex) => `edits[${editIndex}]: already applied — do not resubmit.`);
564
668
  const uniqueFailureKinds = [...new Set(failureKinds)];
565
669
  const editBatchSummary = createReadGuardEditBatchSummary({
566
670
  requestedIndexes,
@@ -568,7 +672,12 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
568
672
  resolvedIndexes,
569
673
  resolvedTotal: resolvedRanges.length,
570
674
  rejectedReasons,
571
- rejectedTotal: Math.max(0, oldTextEditCount - resolvedRanges.length),
675
+ rejectedTotal: Math.max(0, oldTextEditCount -
676
+ resolvedRanges.length -
677
+ alreadyAppliedEdits.length +
678
+ overlapRejectedCount),
679
+ alreadyAppliedIndexes: boundedEditIndexes(alreadyAppliedEdits),
680
+ alreadyAppliedTotal: alreadyAppliedEdits.length,
572
681
  durationMs: Date.now() - startedAt,
573
682
  terminalStatus: "blocked",
574
683
  });
@@ -588,17 +697,21 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
588
697
  oldTextEditCount,
589
698
  resolvedOldTextEditCount: resolvedRanges.length,
590
699
  unresolvedOldTextEditCount: oldTextEditCount - resolvedRanges.length,
700
+ alreadyAppliedCount: alreadyAppliedEdits.length,
701
+ overlapRejectedCount,
591
702
  failedEditIndexes,
592
703
  oldTextPreviews: failedOldTextPreviews.slice(0, 5),
593
704
  errorCount: errors.length,
594
705
  },
595
706
  });
596
- const appliedNote = passedEdits.length > 0
597
- ? `\n\n${passedEdits.map((e) => `edits[${e.originalIndex}]`).join(", ")} ${passedEdits.length === 1 ? "was" : "were"} applied — do NOT re-submit ${passedEdits.length === 1 ? "it" : "them"}.`
598
- : "";
599
- const header = maxFailCount >= 2
600
- ? `🛑 RE-READ REQUIRED — You have submitted this oldText before and it still does not match.\n\nDo NOT retry from memory. Re-read \`${filePath}\` to get the current content, then rebuild your edit from the verbatim file text.`
601
- : `🔄 RETRYABLE — Edit target not found`;
707
+ const header = errors.length === 0
708
+ ? `✅ ALREADY APPLIED — some edits in this call were already applied by an identical earlier submission. Do NOT resubmit them.`
709
+ : maxFailCount >= 2
710
+ ? `🛑 RE-READ REQUIRED — You have submitted this oldText before and it still does not match.\n\nDo NOT retry from memory. Re-read \`${filePath}\` to get the current content, then rebuild your edit from the verbatim file text.`
711
+ : uniqueFailureKinds.length === 1 &&
712
+ uniqueFailureKinds[0] === "span_overlap"
713
+ ? `🔄 RETRYABLE — Overlapping edit spans`
714
+ : `🔄 RETRYABLE — Edit target not found`;
602
715
  logBatchEvent({
603
716
  event: "edit_batch_summary",
604
717
  correlationId,
@@ -607,8 +720,14 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
607
720
  });
608
721
  return {
609
722
  touchedLines: undefined,
610
- preflightError: `${header}\n\n${failureDetails.join("\n\n")}${appliedNote}`,
611
- partiallyApplicable: passedEdits.length > 0 ? passedEdits : undefined,
723
+ preflightError: `${header}\n\n${failureDetails.join("\n\n")}${formatAlreadyAppliedNotes(alreadyAppliedEdits)}`,
724
+ preflightHeader: header,
725
+ preflightDetails: failureDetails,
726
+ partiallyApplicable: passedEdits.length > 0 &&
727
+ !failureKinds.includes("oldtext_unrepresentable")
728
+ ? passedEdits
729
+ : undefined,
730
+ alreadyAppliedEdits: alreadyAppliedEdits.length > 0 ? alreadyAppliedEdits : undefined,
612
731
  editBatchSummary,
613
732
  };
614
733
  }
@@ -670,21 +789,15 @@ function resolveOldTextEdits(edits, filePath, sessionId, correlationId) {
670
789
  editBatchSummary,
671
790
  };
672
791
  }
792
+ // Canonical applied-edit keying lives beside the partial-apply seam that both
793
+ // writes and consumes it (partial-edit-apply.ts, #1053/#2402).
794
+ export { stripOldTextTrailingWhitespace } from "./partial-edit-apply.js";
673
795
  /**
674
796
  * Normalises an oldText string for whitespace-only differences that editors routinely
675
797
  * introduce: trailing spaces/tabs on each line are stripped, and any trailing blank
676
798
  * lines (lines that are empty after trimming) are removed from the end. CRLF is
677
799
  * normalised to LF. Returns the same string if no change was needed.
678
800
  */
679
- export function stripOldTextTrailingWhitespace(value) {
680
- const lines = value
681
- .replace(/\r\n/g, "\n")
682
- .split("\n")
683
- .map((l) => l.trimEnd());
684
- while (lines.length > 1 && lines[lines.length - 1] === "")
685
- lines.pop();
686
- return lines.join("\n");
687
- }
688
801
  /**
689
802
  * Tries to fix a tab/space indentation mismatch between the model's oldText and the
690
803
  * actual file. Returns the corrected oldText if a matching variant is found, or
@@ -1008,8 +1121,32 @@ export function relocateEditRange(input, from, to) {
1008
1121
  }
1009
1122
  return applied;
1010
1123
  }
1011
- export function getTouchedLinesForGuard(event, filePath, sessionId, correlationId) {
1012
- if (isToolCallEventType("edit", event)) {
1124
+ export function getTouchedLinesForGuard(event, filePath, sessionId, correlationId, partialApplyRecords) {
1125
+ // #2423: the seam decides whether this event mutates a file, and which shape
1126
+ // adapter (if any) already resolved its ranges. A tool named `replace` or
1127
+ // `insert` reaches the native-shape ladder below exactly like `edit` does.
1128
+ const mutation = classifyMutatingTool(event, {
1129
+ filePath,
1130
+ sessionId,
1131
+ correlationId,
1132
+ });
1133
+ if (mutation === undefined)
1134
+ return { touchedLines: undefined };
1135
+ if (mutation.kind === "edit") {
1136
+ // An adapter that recognized the input owns the answer, including a
1137
+ // blocking preflight error.
1138
+ if (mutation.touchedLines !== undefined || mutation.preflightError) {
1139
+ return {
1140
+ touchedLines: mutation.touchedLines,
1141
+ ...(mutation.editRanges ? { editRanges: mutation.editRanges } : {}),
1142
+ ...(mutation.preflightError
1143
+ ? { preflightError: mutation.preflightError }
1144
+ : {}),
1145
+ ...(mutation.editBatchSummary
1146
+ ? { editBatchSummary: mutation.editBatchSummary }
1147
+ : {}),
1148
+ };
1149
+ }
1013
1150
  // The host standard-edit fields (path, edits[].oldText/newText) are pinned
1014
1151
  // to the SDK's EditToolInput, so a host edit-schema change is a compile
1015
1152
  // error instead of silently falling through to `unknown_edit_schema`. The
@@ -1017,9 +1154,6 @@ export function getTouchedLinesForGuard(event, filePath, sessionId, correlationI
1017
1154
  // edit tools; oldText/newText are probed as optional because range-only
1018
1155
  // edits omit them (refs #3).
1019
1156
  const editInput = event.input;
1020
- const hashlineResult = resolveHashlineEditInput(editInput, filePath, sessionId, correlationId);
1021
- if (hashlineResult)
1022
- return hashlineResult;
1023
1157
  if (editInput.oldRange) {
1024
1158
  const touchedLines = [
1025
1159
  editInput.oldRange.start.line,
@@ -1056,16 +1190,24 @@ export function getTouchedLinesForGuard(event, filePath, sessionId, correlationI
1056
1190
  .filter((edit) => typeof edit.range?.start?.line !== "number" && !!edit.oldText);
1057
1191
  if (rangedEdits.length === 0) {
1058
1192
  if (filePath) {
1059
- return resolveOldTextEdits(editInput.edits, filePath, sessionId, correlationId);
1193
+ return resolveOldTextEdits(editInput.edits, filePath, sessionId, correlationId, partialApplyRecords);
1060
1194
  }
1061
1195
  return { touchedLines: undefined };
1062
1196
  }
1063
1197
  let oldTextTouchedLines;
1064
1198
  let oldTextEditRanges;
1065
1199
  if (unresolvedOldTextEdits.length > 0 && filePath) {
1066
- const resolved = resolveOldTextEdits(unresolvedOldTextEdits, filePath, sessionId, correlationId);
1200
+ const resolved = resolveOldTextEdits(unresolvedOldTextEdits, filePath, sessionId, correlationId, partialApplyRecords);
1067
1201
  if (resolved.preflightError) {
1068
- return resolved;
1202
+ // A mixed range/oldText request is one host operation. If any
1203
+ // oldText edit cannot be represented as a carried raw span,
1204
+ // close the whole batch instead of letting the host apply only
1205
+ // the native ranges.
1206
+ return {
1207
+ ...resolved,
1208
+ partiallyApplicable: undefined,
1209
+ touchedLines: undefined,
1210
+ };
1069
1211
  }
1070
1212
  oldTextTouchedLines = resolved.touchedLines;
1071
1213
  oldTextEditRanges = resolved.editRanges;
@@ -1118,6 +1260,18 @@ export function getTouchedLinesForGuard(event, filePath, sessionId, correlationI
1118
1260
  metadata: {
1119
1261
  tool: "edit",
1120
1262
  source: "unknown_edit_schema",
1263
+ // #2423: name the tool. An unknown schema under a third-party
1264
+ // tool name is the actionable case — it says which producer
1265
+ // needs a shape adapter.
1266
+ mutatingToolName: mutation.toolName,
1267
+ mutationProvenance: mutation.provenance,
1268
+ // #2423 review round 1 (F1): an adapter that RECOGNIZED the
1269
+ // shape but could not turn its anchors into lines says so here.
1270
+ // `adapterSource: "hashline-edit-pro"` with
1271
+ // `unresolvedReason: "remove_from:anchor_not_found"` is the
1272
+ // production record of a stale anchor — a report, not a block.
1273
+ adapterSource: mutation.source,
1274
+ unresolvedReason: mutation.unresolvedReason,
1121
1275
  topLevelKeys,
1122
1276
  hasNativeOldRange: !!editInput.oldRange,
1123
1277
  hasNativeEdits: Array.isArray(editInput.edits),
@@ -1131,24 +1285,22 @@ export function getTouchedLinesForGuard(event, filePath, sessionId, correlationI
1131
1285
  }
1132
1286
  return { touchedLines: undefined };
1133
1287
  }
1134
- if (isToolCallEventType("write", event)) {
1135
- const lineCount = filePath ? countFileLines(filePath) : 1;
1136
- const touchedLines = [1, lineCount];
1137
- if (filePath) {
1138
- logReadGuardEvent({
1139
- event: "touched_lines_detected",
1140
- correlationId,
1141
- sessionId,
1142
- filePath,
1143
- metadata: {
1144
- tool: "write",
1145
- source: "full_file_write",
1146
- touchedLines,
1147
- lineCount,
1148
- },
1149
- });
1150
- }
1151
- return { touchedLines };
1288
+ // kind === "write": the whole file is the touched range.
1289
+ const lineCount = filePath ? countFileLines(filePath) : 1;
1290
+ const writeTouchedLines = [1, lineCount];
1291
+ if (filePath) {
1292
+ logReadGuardEvent({
1293
+ event: "touched_lines_detected",
1294
+ correlationId,
1295
+ sessionId,
1296
+ filePath,
1297
+ metadata: {
1298
+ tool: "write",
1299
+ source: "full_file_write",
1300
+ touchedLines: writeTouchedLines,
1301
+ lineCount,
1302
+ },
1303
+ });
1152
1304
  }
1153
- return { touchedLines: undefined };
1305
+ return { touchedLines: writeTouchedLines };
1154
1306
  }