@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.
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
- package/node_modules/@czottmann/pi-automode/README.md +20 -0
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
- package/node_modules/pi-lens/CHANGELOG.md +988 -0
- package/node_modules/pi-lens/README.md +3 -0
- package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
- package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
- package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
- package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
- package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
- package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
- package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
- package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
- package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
- package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
- package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
- package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
- package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
- package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
- package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
- package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
- package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
- package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
- package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
- package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
- package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
- package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
- package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
- package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
- package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
- package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
- package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
- package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
- package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
- package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
- package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
- package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
- package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
- package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
- package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
- package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
- package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
- package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
- package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
- package/node_modules/pi-lens/dist/index.js +64335 -54300
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +127 -5
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
- package/node_modules/pi-lens/dist/tools/shared.js +0 -1
- package/node_modules/pi-lens/docs/agent-guide.md +64 -1
- package/node_modules/pi-lens/docs/configuration.md +222 -0
- package/node_modules/pi-lens/docs/dependencies.md +3 -3
- package/node_modules/pi-lens/docs/features.md +44 -5
- package/node_modules/pi-lens/docs/language-coverage.md +2 -2
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
- package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
- package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
- package/node_modules/pi-lens/package.json +15 -23
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +62 -15
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +5 -1
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +242 -1
- package/node_modules/pi-web-access/index.ts +118 -64
- package/node_modules/pi-web-access/mistral-search.ts +281 -0
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/perplexity.ts +14 -1
- package/node_modules/pi-web-access/utils.ts +23 -6
- package/node_modules/pi-web-access/xai-search.ts +96 -33
- 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 {
|
|
4
|
-
import {
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
+
const start = rawContentLf.indexOf(candidate);
|
|
246
|
+
return { text: candidate, start, end: start + candidate.length };
|
|
330
247
|
}
|
|
331
|
-
|
|
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
|
|
303
|
+
let rawBytes;
|
|
338
304
|
try {
|
|
339
|
-
|
|
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
|
|
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
|
-
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
oldText
|
|
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
|
-
|
|
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 -
|
|
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
|
|
597
|
-
?
|
|
598
|
-
:
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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")}${
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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:
|
|
1305
|
+
return { touchedLines: writeTouchedLines };
|
|
1154
1306
|
}
|