@herbertgao/pi-extensions 2026.9.0 → 2026.9.2
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/@narumitw/pi-btw/README.md +57 -141
- package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
- package/node_modules/@narumitw/pi-btw/package.json +2 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
- package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
- package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
- 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 +1072 -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 -9
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
- 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 -9
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -3
- package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -6
- 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-pid-file-lock.js +1 -1
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -9
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +3 -11
- 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 +40 -19
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
- 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 +50 -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 +164 -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 +165 -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 +234 -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 -6
- 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-dispositions.js +1 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +15 -5
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -44
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +27 -8
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
- 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 +383 -27
- 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 +4 -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 +1 -2
- 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 +14 -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 +6 -3
- 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 +1 -2
- 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 +1 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -3
- 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 -67
- 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/utils.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -10
- 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/event-loop-monitor.js +2 -2
- 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-kinds.js +1 -38
- package/node_modules/pi-lens/dist/clients/file-time.js +7 -2
- package/node_modules/pi-lens/dist/clients/file-utils.js +117 -10
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
- package/node_modules/pi-lens/dist/clients/format-service.js +6 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +385 -104
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +116 -22
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
- package/node_modules/pi-lens/dist/clients/git-guard.js +6 -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/grammar-source.js +3 -3
- 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 +289 -59
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +110 -236
- package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
- package/node_modules/pi-lens/dist/clients/language-registry.js +599 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +165 -9
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +34 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +160 -30
- package/node_modules/pi-lens/dist/clients/lens-engine.js +33 -41
- package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +77 -6
- package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -38
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +172 -33
- package/node_modules/pi-lens/dist/clients/lsp/config.js +484 -158
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +18 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +892 -96
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -177
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -18
- 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/sync-kind.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +14 -7
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.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/metrics-history.js +28 -107
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +3 -5
- package/node_modules/pi-lens/dist/clients/module-report.js +21 -46
- 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 -24
- 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 +70 -28
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +5 -1
- 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 +451 -15
- package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
- 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 +79 -18
- 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-changes.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
- 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/jscpd.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +28 -4
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +438 -122
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
- 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 +16 -5
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
- package/node_modules/pi-lens/dist/clients/read-guard.js +26 -2
- package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +155 -114
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
- 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 +19 -3
- 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 +184 -32
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +179 -36
- 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 -12
- package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
- 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/session-summary.js +0 -24
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
- package/node_modules/pi-lens/dist/clients/sgconfig.js +7 -6
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +4 -4
- package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
- 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 +244 -0
- package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
- package/node_modules/pi-lens/dist/clients/tool-policy.js +92 -22
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +87 -100
- 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/tui-fit.js +0 -4
- package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +6 -6
- package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -9
- package/node_modules/pi-lens/dist/clients/word-index-store.js +23 -12
- package/node_modules/pi-lens/dist/clients/word-index.js +252 -47
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +4 -1
- package/node_modules/pi-lens/dist/index.js +65899 -55340
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -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 +96 -25
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +104 -86
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
- package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
- package/node_modules/pi-lens/dist/tools/shared.js +0 -1
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
- 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/globalconfig.md +20 -0
- package/node_modules/pi-lens/docs/language-coverage.md +2 -2
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
- 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 +33 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +49 -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 +190 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
- 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/docs/word-index.md +1 -1
- package/node_modules/pi-lens/package.json +18 -35
- 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/rules/typos/_typos.toml +4 -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/lib/warm-loader-cache.mjs +1 -1
- package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
- 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 +6 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
3
|
+
import { buildActionableWarningsReport, formatActionableWarningsAdvisory, publishActionableWarningsReport, writeDeferredActionableWarningsReport, } from "./actionable-warnings.js";
|
|
3
4
|
import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
|
|
4
5
|
import { appendCodeQualityWarningsHistory, buildCodeQualityWarningsReport, formatCodeQualityWarningsAdvisory, writeCodeQualityWarningsReport, } from "./code-quality-warnings.js";
|
|
5
6
|
import { clearGitGuardTestFailure, mergeGitGuardTestFailure, writeGitGuardRecord, } from "./git-guard.js";
|
|
@@ -29,6 +30,7 @@ import { emitLensTurnFindings } from "./lens-events.js";
|
|
|
29
30
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
30
31
|
import { isSubagentSession } from "./subagent-mode.js";
|
|
31
32
|
import { formatRunDurationMs } from "./run-duration.js";
|
|
33
|
+
import { isExcludedTestTarget, isRunnerErrorResult, RUNNERS, } from "./test-runner-client.js";
|
|
32
34
|
import { MAX_ADVISORY_AFFECTED_FILES, gateFindingsByPathFreshness, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
|
|
33
35
|
import { DEPENDENCY_DRIFT_MAX_DELIVERIES, sweepInlineBlockerFreshness, } from "./blocker-freshness.js";
|
|
34
36
|
import { sweepInlineBlockerPastEof } from "./blocker-past-eof.js";
|
|
@@ -40,11 +42,239 @@ import { drainPendingRunnerFindings, dropStaleRunnerFindings, pendingRunnerFindi
|
|
|
40
42
|
// #1631 review V2: moved to its own leaf module so a low-level store
|
|
41
43
|
// (widget-state.ts) can use the marker without importing this orchestrator —
|
|
42
44
|
// see clients/stale-marker.ts's doc comment.
|
|
43
|
-
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
45
|
+
import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
46
|
+
import { mapWithConcurrency } from "./map-with-concurrency.js";
|
|
47
|
+
import { getAmbientAbortSignal } from "./safe-spawn.js";
|
|
48
|
+
import { emitBounded } from "./bounded-telemetry.js";
|
|
44
49
|
import { degradeDemotedFindingBody, formatDeliveryCapNote, formatRetirementNote, } from "./demoted-finding-render.js";
|
|
45
50
|
import { STALE_LINE_MARKER } from "./stale-marker.js";
|
|
46
51
|
import { getActiveSessionId } from "./session-lifecycle.js";
|
|
47
|
-
import { getWidgetBlockingFilesForSweep, markWidgetFileBlockersStale, recordRunner, } from "./widget-state.js";
|
|
52
|
+
import { drainRenderedDependencyDriftFilePaths, getWidgetBlockingFilesForSweep, incrementWidgetDependencyDriftDelivery, markWidgetFileBlockersStale, recordRunner, retireWidgetDependencyDriftBlockers, } from "./widget-state.js";
|
|
53
|
+
/** Maximum detailed notify-stall coverage-gap rows emitted in one turn. */
|
|
54
|
+
const LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN = 20;
|
|
55
|
+
/**
|
|
56
|
+
* #2504 — bounds on the per-turn test-runner fan-out.
|
|
57
|
+
*
|
|
58
|
+
* The reported turn fired 59 `vitest.cmd` spawns at once from a bare
|
|
59
|
+
* `Promise.allSettled(targets.map(...))`. Each spawn carries its OWN 60 s
|
|
60
|
+
* timeout (`test-runner-client.ts`), which bounds one target and says nothing
|
|
61
|
+
* about the batch: 59 of them starved the event loop for the whole turn
|
|
62
|
+
* (`cpuCoverageRatio 0.56`, 20 orphan-backstop escalations inside the storm).
|
|
63
|
+
*
|
|
64
|
+
* Three separate bounds, because they fail differently:
|
|
65
|
+
* - CONCURRENCY caps how much CPU the batch can hold at one instant;
|
|
66
|
+
* - TARGET COUNT caps how much work one turn may enqueue at all;
|
|
67
|
+
* - the WALL BUDGET caps how long the batch may keep spawning, and is raced
|
|
68
|
+
* against the ambient abort signal so a cancelled turn stops dispatching
|
|
69
|
+
* immediately rather than at the next natural boundary.
|
|
70
|
+
*
|
|
71
|
+
* #2522: the batch budget shipped at 90 s in #2509 — well over the turn
|
|
72
|
+
* itself, so a slow/hung batch could still hold up the agent for most of a
|
|
73
|
+
* turn before the bound even engaged. Reconciled down to 20 s (one constant,
|
|
74
|
+
* not a second budget alongside it): well under the turn, and still an order
|
|
75
|
+
* of magnitude below the PER-TARGET 60 s cap in `test-runner-client.ts`, so
|
|
76
|
+
* the batch bound is the one that actually fires first for a stuck target.
|
|
77
|
+
*
|
|
78
|
+
* #2522 review round 2: lowering the budget on its own made things WORSE,
|
|
79
|
+
* because the "deferred to the next turn" this comment used to claim did not
|
|
80
|
+
* exist. `runTestTargetsBounded` returned a skipped COUNT and threw the
|
|
81
|
+
* identity of the unrun targets away, so a target slower than the budget
|
|
82
|
+
* produced no result, no cache record and no output — every turn, forever —
|
|
83
|
+
* while a PARTIAL batch fell through to the clean branch and was reported as
|
|
84
|
+
* "all tests passed". The deferral is now a real mechanism:
|
|
85
|
+
*
|
|
86
|
+
* - the batch owns an `AbortController` that is handed to each target's spawn
|
|
87
|
+
* (`TestRunRequest.signal` → `safeSpawnAsync`), so reaching the bound KILLS
|
|
88
|
+
* what is in flight instead of leaving it to burn its own 60 s timeout and
|
|
89
|
+
* then mutate a results array the caller has already consumed;
|
|
90
|
+
* - the killed set plus the never-dispatched set is returned BY IDENTITY as
|
|
91
|
+
* `deferred`, persisted on the `test-runner-findings` record as
|
|
92
|
+
* `deferredTargets`, and dispatched FIRST on the next turn — ahead of
|
|
93
|
+
* failed-first/related/self — under the same `TEST_RUNNER_MAX_TARGETS` cap;
|
|
94
|
+
* - a batch whose `deferred` set is non-empty can never take the clean
|
|
95
|
+
* branch, so an unfinished run is never reported as a green one and never
|
|
96
|
+
* relaxes the `--lens-guard` commit gate.
|
|
97
|
+
*/
|
|
98
|
+
export const TEST_RUNNER_BATCH_CONCURRENCY = 4;
|
|
99
|
+
export const TEST_RUNNER_MAX_TARGETS = 12;
|
|
100
|
+
export const TEST_RUNNER_BATCH_BUDGET_MS = 20_000;
|
|
101
|
+
/**
|
|
102
|
+
* How many consecutive turn-end batches a target may be cut out of before it
|
|
103
|
+
* is retired from turn-end selection altogether.
|
|
104
|
+
*
|
|
105
|
+
* Without this the deferral is a LIVELOCK, and not hypothetically: this
|
|
106
|
+
* repo's own `tests/index-integration.test.ts` — which turn-end resolves
|
|
107
|
+
* through the `related` strategy whenever `clients/runtime-turn.ts` is
|
|
108
|
+
* edited — takes 26.4 s, MORE than the entire 20 s batch budget on its own.
|
|
109
|
+
* Deferring it puts it FIRST in the next batch, where it is cut again at
|
|
110
|
+
* 20 s, and again, every turn, forever. A target that cannot fit the budget
|
|
111
|
+
* is not a scheduling problem retrying can solve; it is reported once per
|
|
112
|
+
* occurrence and dropped, so the turn stops paying 20 s for it.
|
|
113
|
+
*/
|
|
114
|
+
export const TEST_RUNNER_MAX_DEFERRALS = 2;
|
|
115
|
+
/**
|
|
116
|
+
* Ceiling on how many entries either persisted target list may carry.
|
|
117
|
+
*
|
|
118
|
+
* #2522 review round 4, I1: a write may no longer destroy another session's
|
|
119
|
+
* entries, so nothing in the write path prunes them any more — each session
|
|
120
|
+
* that ever cut or retired a target in this project leaves its rows behind, on
|
|
121
|
+
* a record read at every single turn_end. The bound is applied at the one
|
|
122
|
+
* writer, on the whole list, and sheds FOREIGN rows first (the writer orders
|
|
123
|
+
* this session's entries last), so it can never evict the entries this turn
|
|
124
|
+
* depends on. Generous on purpose: it is a backstop against unbounded growth,
|
|
125
|
+
* not a scheduling policy.
|
|
126
|
+
*/
|
|
127
|
+
export const TEST_RUNNER_MAX_PERSISTED_TARGETS = 64;
|
|
128
|
+
/**
|
|
129
|
+
* Union two deferral sets by target identity, keeping the HIGHER attempt count
|
|
130
|
+
* (#2522 review round 3, F3). Two overlapping batches can both be cut on the
|
|
131
|
+
* same target; taking the lower count would let a target trade an attempt for
|
|
132
|
+
* every overlap and never converge on `TEST_RUNNER_MAX_DEFERRALS`.
|
|
133
|
+
*
|
|
134
|
+
* Identity is (session, path), not path alone (#2522 review round 4, I1): two
|
|
135
|
+
* sessions can each owe a run of the same file, and collapsing those into one
|
|
136
|
+
* row makes the surviving row's `sessionId` decide whose deferral is honoured
|
|
137
|
+
* and whose is silently dropped. The path half is keyed through
|
|
138
|
+
* `normalizeMapKey` so `/`- and `\`-separated spellings are one entry
|
|
139
|
+
* (AGENTS.md cross-form-path screen).
|
|
140
|
+
*/
|
|
141
|
+
function deferralEntryKey(entry) {
|
|
142
|
+
return `${entry.sessionId ?? ""}\u0000${normalizeMapKey(path.resolve(entry.testFile))}`;
|
|
143
|
+
}
|
|
144
|
+
function mergeDeferredTargets(existing, incoming) {
|
|
145
|
+
const byKey = new Map();
|
|
146
|
+
for (const entry of [...existing, ...incoming]) {
|
|
147
|
+
const key = deferralEntryKey(entry);
|
|
148
|
+
const prior = byKey.get(key);
|
|
149
|
+
if (prior && (prior.attempts ?? 0) >= (entry.attempts ?? 0))
|
|
150
|
+
continue;
|
|
151
|
+
byKey.set(key, entry);
|
|
152
|
+
}
|
|
153
|
+
return [...byKey.values()];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Run `run` over `targets` with a concurrency cap, a batch-wide wall budget
|
|
157
|
+
* and an abort-signal race. Reuses `mapWithConcurrency` (the repo's existing
|
|
158
|
+
* worker-pool shape) rather than hand-rolling a second pool.
|
|
159
|
+
*
|
|
160
|
+
* Both bounds are enforced twice on purpose: cooperatively, before each
|
|
161
|
+
* dispatch, so no NEW work starts past the bound; and as a real race against
|
|
162
|
+
* the pool, so an in-flight target cannot hold the batch past its budget.
|
|
163
|
+
* Whatever has settled by then is returned — a partial batch of real results
|
|
164
|
+
* is strictly better than none, and the caller reports the shortfall.
|
|
165
|
+
*/
|
|
166
|
+
export async function runTestTargetsBounded(args) {
|
|
167
|
+
const results = [];
|
|
168
|
+
if (args.targets.length === 0)
|
|
169
|
+
return { results, deferred: [] };
|
|
170
|
+
let stopReason;
|
|
171
|
+
// One controller for the whole batch. Aborting it tree-kills every child the
|
|
172
|
+
// runner has in flight (`safeSpawnAsync` honours the signal it is handed),
|
|
173
|
+
// which is what turns "the bound fired" into "the work actually stopped".
|
|
174
|
+
const batchAbort = new AbortController();
|
|
175
|
+
// Latched at the moment this call returns. Nothing may push into `results`
|
|
176
|
+
// after that: the caller has already consumed the array, and a late push
|
|
177
|
+
// would silently change a batch it has finished reasoning about.
|
|
178
|
+
let closed = false;
|
|
179
|
+
/**
|
|
180
|
+
* #2522 review round 4, I4: "was this target CUT, or did it FINISH?" is a
|
|
181
|
+
* semantic question, and this set is the answer — stamped by the `.then`
|
|
182
|
+
* attached to `run`'s promise AT DISPATCH, which is that promise's FIRST
|
|
183
|
+
* continuation. Nothing but a microtask can run between the promise settling
|
|
184
|
+
* and this stamp, and every path that closes the batch (`setTimeout`, the
|
|
185
|
+
* ambient signal's `abort` event, the pool settling after every mapper has
|
|
186
|
+
* returned) reaches `finish()` from a macrotask or later. So `closed` read
|
|
187
|
+
* here is exactly "the batch was already cut when this value existed".
|
|
188
|
+
*
|
|
189
|
+
* Round 3 read the same flag from the mapper's own `await` continuation,
|
|
190
|
+
* which is several continuations downstream of the settle — a macrotask CAN
|
|
191
|
+
* interleave there — and papered over the resulting misclassification with a
|
|
192
|
+
* `setImmediate` hop plus a `latchedOut` parking array. Both are gone: the
|
|
193
|
+
* hop made the answer depend on queue ordering, which is the thing that kept
|
|
194
|
+
* producing findings (a spawn aborted BEFORE it started resolves
|
|
195
|
+
* synchronously, so the hop folded it back in as a completed run — round 4
|
|
196
|
+
* P3c).
|
|
197
|
+
*/
|
|
198
|
+
const settledBeforeClose = new Set();
|
|
199
|
+
const budgetMs = Math.max(0, args.budgetMs);
|
|
200
|
+
const deadline = Date.now() + budgetMs;
|
|
201
|
+
const shouldStop = () => {
|
|
202
|
+
if (stopReason !== undefined)
|
|
203
|
+
return true;
|
|
204
|
+
if (args.signal?.aborted) {
|
|
205
|
+
stopReason = "abort";
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (Date.now() >= deadline) {
|
|
209
|
+
stopReason = "budget";
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
const pool = mapWithConcurrency(args.targets.map((target, index) => ({ target, index })), Math.max(1, args.concurrency), async ({ target, index }) => {
|
|
215
|
+
if (shouldStop())
|
|
216
|
+
return;
|
|
217
|
+
const running = args.run(target, batchAbort.signal);
|
|
218
|
+
// Attached HERE, at dispatch, so it is the promise's first
|
|
219
|
+
// continuation — see `settledBeforeClose`. A value that only exists
|
|
220
|
+
// because the batch was cut (a killed spawn's runner error, or
|
|
221
|
+
// `safeSpawnAsync`'s synchronous "Spawn aborted before start") settles
|
|
222
|
+
// strictly after the close and is therefore never recorded, so the
|
|
223
|
+
// caller defers that target instead of reading it as a completed run.
|
|
224
|
+
const stamp = running.then((value) => {
|
|
225
|
+
if (closed)
|
|
226
|
+
return;
|
|
227
|
+
settledBeforeClose.add(index);
|
|
228
|
+
results.push({ status: "fulfilled", value });
|
|
229
|
+
}, (reason) => {
|
|
230
|
+
if (closed)
|
|
231
|
+
return;
|
|
232
|
+
settledBeforeClose.add(index);
|
|
233
|
+
results.push({ status: "rejected", reason });
|
|
234
|
+
});
|
|
235
|
+
// The stamp swallows both settlements, so this only keeps the pool's
|
|
236
|
+
// concurrency slot held for as long as the work actually runs.
|
|
237
|
+
await stamp;
|
|
238
|
+
});
|
|
239
|
+
// The pool's mapper swallows every throw from an async `run`; a `run` that throws
|
|
240
|
+
// SYNCHRONOUSLY still rejects the pool and closes the whole batch (everything defers,
|
|
241
|
+
// nothing publishes clean). Production runners are async, so this is fail-safe, not reachable.
|
|
242
|
+
void pool.catch(() => { });
|
|
243
|
+
await new Promise((resolve) => {
|
|
244
|
+
let settled = false;
|
|
245
|
+
const finish = (reason) => {
|
|
246
|
+
if (settled)
|
|
247
|
+
return;
|
|
248
|
+
settled = true;
|
|
249
|
+
if (reason !== undefined)
|
|
250
|
+
stopReason ??= reason;
|
|
251
|
+
// Latch BEFORE resolving, so no continuation scheduled by the kill
|
|
252
|
+
// below can slip a result in behind the caller's back.
|
|
253
|
+
closed = true;
|
|
254
|
+
// Kill whatever is still in flight. Without this the pool ran on past
|
|
255
|
+
// the bound: a stuck vitest spawn kept a core busy to its own 60s
|
|
256
|
+
// timeout, long after the turn had moved on.
|
|
257
|
+
batchAbort.abort();
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
260
|
+
resolve();
|
|
261
|
+
};
|
|
262
|
+
const onAbort = () => finish("abort");
|
|
263
|
+
const timer = setTimeout(() => finish("budget"), budgetMs);
|
|
264
|
+
// A pending batch timer must never hold the process open.
|
|
265
|
+
timer.unref?.();
|
|
266
|
+
if (args.signal?.aborted)
|
|
267
|
+
finish("abort");
|
|
268
|
+
else
|
|
269
|
+
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
270
|
+
void pool.then(() => finish(), () => finish());
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
results,
|
|
274
|
+
deferred: args.targets.filter((_, index) => !settledBeforeClose.has(index)),
|
|
275
|
+
stopReason,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
48
278
|
/**
|
|
49
279
|
* #1617: turn_end reads gitleaks/govulncheck/trivy straight from their
|
|
50
280
|
* session-scan caches and formats them into advisory/blocker text — a
|
|
@@ -239,7 +469,7 @@ function capTurnEndMessage(content) {
|
|
|
239
469
|
return out;
|
|
240
470
|
}
|
|
241
471
|
export async function handleTurnEnd(deps) {
|
|
242
|
-
const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, owner, resetLSPService, resetFormatService, } = deps;
|
|
472
|
+
const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, owner, resetLSPService, resetFormatService, } = deps;
|
|
243
473
|
// #449 slice 1: piggyback the instance-registry heartbeat on this existing
|
|
244
474
|
// per-turn touchpoint rather than adding a new timer/interval. Cheap (reads
|
|
245
475
|
// process.memoryUsage().rss, one read-modify-write of instances.json) and
|
|
@@ -261,13 +491,27 @@ export async function handleTurnEnd(deps) {
|
|
|
261
491
|
let turnState = cacheManager.readTurnState(cwd);
|
|
262
492
|
// A live foreign writer owns this worklist. Do not clear or consume another
|
|
263
493
|
// pi/MCP session's files; a dead/aged owner is safely evicted instead.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
494
|
+
// #2504: `sessionStartedAt` dates the persisted worklist against THIS
|
|
495
|
+
// session. Without it an ownerless turn-state.json — the resting shape
|
|
496
|
+
// before #2504 — read back as "owned" no matter how old it was.
|
|
497
|
+
const currentOwner = {
|
|
498
|
+
...(owner ?? {
|
|
499
|
+
kind: "pi",
|
|
500
|
+
id: runtime.telemetrySessionId,
|
|
501
|
+
pid: process.pid,
|
|
502
|
+
lastSeen: new Date().toISOString(),
|
|
503
|
+
}),
|
|
504
|
+
sessionStartedAt: owner?.sessionStartedAt ?? runtime.sessionStartedAt,
|
|
269
505
|
};
|
|
270
506
|
const access = cacheManager.getTurnStateAccess(cwd, currentOwner);
|
|
507
|
+
// Captured BEFORE the eviction below rewrites the file: the owner the gate
|
|
508
|
+
// actually judged. This pair is what would have settled #2504 from the
|
|
509
|
+
// debug log alone.
|
|
510
|
+
const gateOwnerLabel = turnState.owner
|
|
511
|
+
? `${turnState.owner.kind}:${turnState.owner.id}`
|
|
512
|
+
: turnState.sessionId
|
|
513
|
+
? `legacy:${turnState.sessionId}`
|
|
514
|
+
: "none";
|
|
271
515
|
const sameProcessPiSessionHandoff = access === "foreign-live" &&
|
|
272
516
|
currentOwner.kind === "pi" &&
|
|
273
517
|
turnState.owner?.kind === "pi" &&
|
|
@@ -284,6 +528,49 @@ export async function handleTurnEnd(deps) {
|
|
|
284
528
|
turnState = cacheManager.readTurnState(cwd);
|
|
285
529
|
}
|
|
286
530
|
const files = Object.keys(turnState.files);
|
|
531
|
+
/**
|
|
532
|
+
* #2275: widget-footer sibling of #1950's inline-blocker cap, for the
|
|
533
|
+
* widget store's OWN dependency-drift demotion
|
|
534
|
+
* (`markWidgetFileBlockersStale`, driven by the freshness sweep further
|
|
535
|
+
* down this function) — a completely separate store from
|
|
536
|
+
* `RuntimeCoordinator`'s inline-blocker map, so it needed its own
|
|
537
|
+
* delivery count (`WidgetDiagnostic.staleDeliveryCount`) rather than
|
|
538
|
+
* inheriting one.
|
|
539
|
+
*
|
|
540
|
+
* Review F1: the population is what the footer RENDERED since the last
|
|
541
|
+
* turn end, drained here — not every file that merely holds a demoted
|
|
542
|
+
* row. The footer draws one record per pass (`withBlocking[0]`, its top
|
|
543
|
+
* five entries) and may not be drawn at all, so a per-turn walk of the
|
|
544
|
+
* whole store charged deliveries the agent never received and retired a
|
|
545
|
+
* delivery early. This is the widget-surface analogue of the inline
|
|
546
|
+
* loop's own `pendingDependencyDriftDeliveries` deferral below: both
|
|
547
|
+
* commit a delivery only once the surface has actually served it. Every
|
|
548
|
+
* `deliveryCount` reported to the ledger is therefore a count of RENDERS.
|
|
549
|
+
*
|
|
550
|
+
* Fix-round 3 (#2275 review F1): this drain/charge MUST run before the
|
|
551
|
+
* `files.length === 0` early return below — a read-only turn (no
|
|
552
|
+
* modified files) still repaints the footer and can draw a demoted row,
|
|
553
|
+
* so a cap that only charged deliveries below the early return silently
|
|
554
|
+
* starved on quiet turns: the footer re-rendered the same demoted row
|
|
555
|
+
* every turn while the delivery count never advanced. The drain is a
|
|
556
|
+
* Set.take() plus one map lookup per drained file — cheap enough to run
|
|
557
|
+
* unconditionally on every turn end.
|
|
558
|
+
*/
|
|
559
|
+
let widgetDemotedFindingsRetired = 0;
|
|
560
|
+
for (const wPath of drainRenderedDependencyDriftFilePaths()) {
|
|
561
|
+
const deliveryCount = incrementWidgetDependencyDriftDelivery(wPath);
|
|
562
|
+
if (deliveryCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
|
|
563
|
+
const capRetired = retireWidgetDependencyDriftBlockers(wPath);
|
|
564
|
+
if (capRetired) {
|
|
565
|
+
widgetDemotedFindingsRetired += 1;
|
|
566
|
+
incrementDegradationCount({
|
|
567
|
+
kind: "demoted-finding-retired",
|
|
568
|
+
subject: `widget-blocker:${toRunnerDisplayPath(cwd, wPath)}`,
|
|
569
|
+
reason: `capped after ${deliveryCount} deliveries with no re-run; hidden from the pi-lens footer, still listed by lens_diagnostics mode=all — re-run can still confirm`,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
287
574
|
// R1 (#1443 follow-up): a read-only turn (no files touched) must not take
|
|
288
575
|
// the fast idle-reset path while a carried cascade run — or one still
|
|
289
576
|
// settling — is waiting for its delivery opportunity. Falling through to
|
|
@@ -347,7 +634,7 @@ export async function handleTurnEnd(deps) {
|
|
|
347
634
|
cancelLSPIdleReset();
|
|
348
635
|
dbg("turn_end: cancelled pending LSP idle reset (active editing)");
|
|
349
636
|
}
|
|
350
|
-
dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}`);
|
|
637
|
+
dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}, access: ${access}, owner: ${gateOwnerLabel}`);
|
|
351
638
|
if (cacheManager.isMaxCyclesExceeded(cwd)) {
|
|
352
639
|
dbg("turn_end: max cycles exceeded, clearing state and forcing through");
|
|
353
640
|
cacheManager.clearTurnState(cwd, currentOwner);
|
|
@@ -1457,10 +1744,12 @@ export async function handleTurnEnd(deps) {
|
|
|
1457
1744
|
metadata: madgeStats ?? { skipped: true },
|
|
1458
1745
|
});
|
|
1459
1746
|
// --- Test runner: fire once per turn after all edits are done ---
|
|
1460
|
-
// Runs for each unique test target across modified files; results
|
|
1461
|
-
//
|
|
1747
|
+
// Runs for each unique test target across modified files; results remain in
|
|
1748
|
+
// the pull-diagnostics cache and are delivered after the agent settles.
|
|
1462
1749
|
if (!getFlag("no-tests") && files.length > 0) {
|
|
1463
1750
|
const seen = new Set();
|
|
1751
|
+
// `strategy` is widened by one value the resolver itself cannot produce:
|
|
1752
|
+
// a target carried over from the previous turn's cut batch (#2522 R2 F1).
|
|
1464
1753
|
const targets = [];
|
|
1465
1754
|
// #628: also target the test companions of this turn's cascade neighbors
|
|
1466
1755
|
// (files that import an edited file) — a neighbor's own tests can break
|
|
@@ -1491,10 +1780,266 @@ export async function handleTurnEnd(deps) {
|
|
|
1491
1780
|
candidates.push({ display: neighbor.filePath, abs, isNeighbor: true });
|
|
1492
1781
|
}
|
|
1493
1782
|
}
|
|
1783
|
+
// #2522 review round 2, F1: the previous turn's unfinished targets go
|
|
1784
|
+
// FIRST — ahead of failed-first/related/self — under the same
|
|
1785
|
+
// TEST_RUNNER_MAX_TARGETS cap, so a batch that keeps getting cut makes
|
|
1786
|
+
// progress instead of re-running whatever this turn happened to touch
|
|
1787
|
+
// and dropping the remainder again. The list is consumed here (written
|
|
1788
|
+
// back empty below) so a target can never be carried forever.
|
|
1789
|
+
const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
1790
|
+
const carriedDeferred = priorTestCache?.deferredTargets ?? [];
|
|
1791
|
+
// #2522 review round 3, F5: a deferral list belongs to the session that
|
|
1792
|
+
// cut it. `runtime.telemetrySessionId` is the same identity the batch
|
|
1793
|
+
// stamps its own entries with below (`firedSessionId`), so this is a
|
|
1794
|
+
// single comparison against one source of truth.
|
|
1795
|
+
const turnSessionId = sessionId ?? runtime.telemetrySessionId;
|
|
1796
|
+
const isThisSession = (entry) => entry.sessionId === turnSessionId;
|
|
1797
|
+
const entryPathKey = (entry) => normalizeMapKey(path.resolve(cwd, entry.testFile));
|
|
1798
|
+
/**
|
|
1799
|
+
* #2522 review round 3, F1: the retirement, carried forward.
|
|
1800
|
+
*
|
|
1801
|
+
* Round 2 announced the retirement and dropped it on the floor, so the
|
|
1802
|
+
* candidate loop below re-resolved the very same file through
|
|
1803
|
+
* `related`/`self` in the SAME turn and ran it anyway with a fresh
|
|
1804
|
+
* (undefined) attempt count. Steady state was a 3-turn cycle that spawned
|
|
1805
|
+
* and cut the slow suite on every turn. The retired set is persisted on
|
|
1806
|
+
* the record instead.
|
|
1807
|
+
*
|
|
1808
|
+
* Round 4, I1: this is a SELECTION filter and nothing more. Round 3 also
|
|
1809
|
+
* wrote the filtered list straight back, so a turn taken by the other
|
|
1810
|
+
* entry point into the same project — `clients/mcp/session.ts` passes no
|
|
1811
|
+
* `sessionId` and falls back to `telemetrySessionId`, while `index.ts`
|
|
1812
|
+
* passes pi's stable id — erased the entries belonging to the other
|
|
1813
|
+
* route. Both lists survive every write now; see `writeTestFindings`.
|
|
1814
|
+
*/
|
|
1815
|
+
const retiredCarry = (priorTestCache?.retiredTargets ?? []).filter(isThisSession);
|
|
1816
|
+
const retiredKeys = new Set(retiredCarry.map(entryPathKey));
|
|
1817
|
+
/**
|
|
1818
|
+
* Deferral entries of THIS session that this turn has settled: dispatched
|
|
1819
|
+
* into `targets`, retired at the cap, or dropped as unrunnable. Only
|
|
1820
|
+
* these may leave the persisted list, and only when this turn's own write
|
|
1821
|
+
* is not re-asserting them (a cut target comes straight back with
|
|
1822
|
+
* `attempts + 1`).
|
|
1823
|
+
*/
|
|
1824
|
+
const resolvedDeferralKeys = new Set();
|
|
1825
|
+
/**
|
|
1826
|
+
* #2522 review round 4, I5: entries this turn could not fit under
|
|
1827
|
+
* `TEST_RUNNER_MAX_TARGETS`. They were not run, so they keep their attempt
|
|
1828
|
+
* count and stay on the list — round 3 folded the over-cap case in with
|
|
1829
|
+
* "no longer resolves" and dropped them outright, so a carry larger than
|
|
1830
|
+
* the cap lost its tail every turn under a dbg line that said the files
|
|
1831
|
+
* were gone.
|
|
1832
|
+
*/
|
|
1833
|
+
const heldDeferred = [];
|
|
1834
|
+
/**
|
|
1835
|
+
* The ONE writer of `test-runner-findings` in this block, so the rules
|
|
1836
|
+
* about what survives a write are stated once instead of at six call sites
|
|
1837
|
+
* that a seventh would silently not join (#2522 review round 3, F1; same
|
|
1838
|
+
* net-count rule as `retireTestFindings` in `runtime-context.ts`).
|
|
1839
|
+
*
|
|
1840
|
+
* Three rules, all of them round-4 invariants:
|
|
1841
|
+
* - I1: the LIVE record is re-read here and merged into, never replaced.
|
|
1842
|
+
* A row belonging to another session is untouchable — this turn may
|
|
1843
|
+
* ignore it, never delete it.
|
|
1844
|
+
* - I2: every write merges. A batch that publishes clean, a batch that
|
|
1845
|
+
* publishes failures and a superseded batch all go through this, so
|
|
1846
|
+
* none of them can overwrite a concurrent batch's cut set. Removal is
|
|
1847
|
+
* narrow by construction: an entry leaves only if it is this session's,
|
|
1848
|
+
* this turn settled it, and this write is not re-asserting it.
|
|
1849
|
+
* - the persisted lists are bounded (`TEST_RUNNER_MAX_PERSISTED_TARGETS`)
|
|
1850
|
+
* with this session's rows ordered LAST, so the bound sheds foreign
|
|
1851
|
+
* rows first and can never evict what this turn just recorded.
|
|
1852
|
+
*/
|
|
1853
|
+
const boundPersisted = (entries, label) => {
|
|
1854
|
+
const ordered = [
|
|
1855
|
+
...entries.filter((entry) => !isThisSession(entry)),
|
|
1856
|
+
...entries.filter(isThisSession),
|
|
1857
|
+
];
|
|
1858
|
+
if (ordered.length <= TEST_RUNNER_MAX_PERSISTED_TARGETS)
|
|
1859
|
+
return ordered;
|
|
1860
|
+
dbg(`turn_end: ${label} test target list held ${ordered.length} entries, bounded to the newest ${TEST_RUNNER_MAX_PERSISTED_TARGETS} (oldest foreign-session rows dropped)`);
|
|
1861
|
+
return ordered.slice(-TEST_RUNNER_MAX_PERSISTED_TARGETS);
|
|
1862
|
+
};
|
|
1863
|
+
const writeTestFindings = (record, ownDeferred) => {
|
|
1864
|
+
const live = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
1865
|
+
const own = mergeDeferredTargets(heldDeferred, ownDeferred);
|
|
1866
|
+
const ownKeys = new Set(own.map(entryPathKey));
|
|
1867
|
+
const merged = mergeDeferredTargets(live?.deferredTargets ?? [], own).filter((entry) => {
|
|
1868
|
+
if (!isThisSession(entry))
|
|
1869
|
+
return true;
|
|
1870
|
+
const key = entryPathKey(entry);
|
|
1871
|
+
return !resolvedDeferralKeys.has(key) || ownKeys.has(key);
|
|
1872
|
+
});
|
|
1873
|
+
cacheManager.writeCache("test-runner-findings", {
|
|
1874
|
+
...record,
|
|
1875
|
+
deferredTargets: boundPersisted(merged, "deferred"),
|
|
1876
|
+
retiredTargets: boundPersisted(mergeDeferredTargets(live?.retiredTargets ?? [], retiredCarry), "retired"),
|
|
1877
|
+
}, cwd);
|
|
1878
|
+
};
|
|
1879
|
+
let deadDeferred = 0;
|
|
1880
|
+
let heldOverCap = 0;
|
|
1881
|
+
let foreignDeferred = 0;
|
|
1882
|
+
let redispatchedDeferred = 0;
|
|
1883
|
+
let retiredThisTurn = 0;
|
|
1884
|
+
for (const carried of carriedDeferred) {
|
|
1885
|
+
const testFile = path.resolve(cwd, carried.testFile);
|
|
1886
|
+
const carriedKey = normalizeMapKey(testFile);
|
|
1887
|
+
// I1: another session still owes this run. Skip it, leave it on the
|
|
1888
|
+
// record — `writeTestFindings` never removes a row this turn does not
|
|
1889
|
+
// own.
|
|
1890
|
+
if (!isThisSession(carried)) {
|
|
1891
|
+
foreignDeferred++;
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
if (seen.has(carriedKey)) {
|
|
1895
|
+
resolvedDeferralKeys.add(carriedKey);
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
const attempts = carried.attempts ?? 0;
|
|
1899
|
+
if (attempts >= TEST_RUNNER_MAX_DEFERRALS) {
|
|
1900
|
+
resolvedDeferralKeys.add(carriedKey);
|
|
1901
|
+
if (!retiredKeys.has(carriedKey)) {
|
|
1902
|
+
retiredKeys.add(carriedKey);
|
|
1903
|
+
retiredThisTurn++;
|
|
1904
|
+
retiredCarry.push({
|
|
1905
|
+
testFile,
|
|
1906
|
+
runner: carried.runner,
|
|
1907
|
+
attempts,
|
|
1908
|
+
sessionId: turnSessionId,
|
|
1909
|
+
});
|
|
1910
|
+
// Never silent, and counted rather than once-per-subject: this is
|
|
1911
|
+
// the ledger entry that names WHICH suite is too slow to belong in
|
|
1912
|
+
// a per-turn batch at all.
|
|
1913
|
+
incrementDegradationCount({
|
|
1914
|
+
kind: "test-runner-batch-capped",
|
|
1915
|
+
subject: `${cwd}:deferral-exhausted`,
|
|
1916
|
+
reason: `test target ${path.relative(cwd, testFile)} was cut at the turn-end batch budget ${attempts} turn(s) running and is retired from turn-end selection for the rest of this session — too slow for a per-turn batch, run it explicitly`,
|
|
1917
|
+
});
|
|
1918
|
+
dbg(`turn_end: retiring deferred test target ${path.relative(cwd, testFile)} after ${attempts} cut batch(es) — too slow for the turn-end budget, run it explicitly`);
|
|
1919
|
+
}
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
// A RunnerConfig carries functions, so the cache stores the runner KEY
|
|
1923
|
+
// and the config is re-resolved here from the single registry.
|
|
1924
|
+
const config = RUNNERS[carried.runner];
|
|
1925
|
+
if (!config ||
|
|
1926
|
+
isExcludedTestTarget(testFile, cwd) ||
|
|
1927
|
+
!fs.existsSync(testFile)) {
|
|
1928
|
+
resolvedDeferralKeys.add(carriedKey);
|
|
1929
|
+
deadDeferred++;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
|
|
1933
|
+
// I5: not settled — held, with `attempts` UNCHANGED. It was never
|
|
1934
|
+
// dispatched, so charging it toward retirement would retire a suite
|
|
1935
|
+
// this turn simply had no room for.
|
|
1936
|
+
heldOverCap++;
|
|
1937
|
+
heldDeferred.push({
|
|
1938
|
+
testFile,
|
|
1939
|
+
runner: carried.runner,
|
|
1940
|
+
attempts,
|
|
1941
|
+
sessionId: turnSessionId,
|
|
1942
|
+
});
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
resolvedDeferralKeys.add(carriedKey);
|
|
1946
|
+
seen.add(carriedKey);
|
|
1947
|
+
redispatchedDeferred++;
|
|
1948
|
+
targets.push({
|
|
1949
|
+
testFile,
|
|
1950
|
+
runner: carried.runner,
|
|
1951
|
+
config,
|
|
1952
|
+
strategy: "deferred",
|
|
1953
|
+
deferralAttempts: attempts,
|
|
1954
|
+
});
|
|
1955
|
+
dbg(`turn_end: re-running deferred test target ${path.relative(cwd, testFile)} (${carried.runner}) from the previous turn's cut batch`);
|
|
1956
|
+
}
|
|
1957
|
+
if (deadDeferred > 0) {
|
|
1958
|
+
dbg(`turn_end: dropped ${deadDeferred} deferred test target(s) that no longer resolve (missing file, excluded, or unknown runner)`);
|
|
1959
|
+
}
|
|
1960
|
+
if (heldOverCap > 0) {
|
|
1961
|
+
dbg(`turn_end: held ${heldOverCap} deferred test target(s) over the per-turn cap of ${TEST_RUNNER_MAX_TARGETS} — kept on the deferral list for the next turn, attempts unchanged`);
|
|
1962
|
+
}
|
|
1963
|
+
if (foreignDeferred > 0) {
|
|
1964
|
+
dbg(`turn_end: ignored ${foreignDeferred} deferred test target(s) belonging to another session — left on the record for the session that cut them`);
|
|
1965
|
+
}
|
|
1966
|
+
if (carriedDeferred.length > 0) {
|
|
1967
|
+
// #2522 review round 4, S1: the ONE pushed record for this path. The
|
|
1968
|
+
// MCP / Stop-hook route calls `handleTurnEnd` with `dbg: noop`, so every
|
|
1969
|
+
// line above is invisible there and the deferral machinery — the part
|
|
1970
|
+
// that decides whether a suite runs at all — left no trace whatsoever
|
|
1971
|
+
// on the route that fires it most.
|
|
1972
|
+
logLatency({
|
|
1973
|
+
type: "phase",
|
|
1974
|
+
toolName: "turn_end",
|
|
1975
|
+
filePath: cwd,
|
|
1976
|
+
phase: "test_runner_deferral",
|
|
1977
|
+
durationMs: 0,
|
|
1978
|
+
metadata: {
|
|
1979
|
+
carried: carriedDeferred.length,
|
|
1980
|
+
redispatched: redispatchedDeferred,
|
|
1981
|
+
heldOverCap,
|
|
1982
|
+
retired: retiredThisTurn,
|
|
1983
|
+
dropped: deadDeferred,
|
|
1984
|
+
foreignSession: foreignDeferred,
|
|
1985
|
+
sessionId: turnSessionId,
|
|
1986
|
+
},
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
let overCapTargets = 0;
|
|
1990
|
+
let missingTargetFiles = 0;
|
|
1991
|
+
let excludedTargets = 0;
|
|
1992
|
+
let retiredSkips = 0;
|
|
1494
1993
|
for (const { display, abs, isNeighbor } of candidates) {
|
|
1495
1994
|
const target = testRunnerClient.getTestRunTarget(abs, cwd, runtime.turnIndex);
|
|
1496
|
-
|
|
1497
|
-
|
|
1995
|
+
const targetKey = target ? normalizeMapKey(target.testFile) : "";
|
|
1996
|
+
if (target && !seen.has(targetKey)) {
|
|
1997
|
+
seen.add(targetKey);
|
|
1998
|
+
// #2522 review round 3, F1: THE gate that makes the deferral cap a
|
|
1999
|
+
// cap. Whichever strategy produced this target — related, self,
|
|
2000
|
+
// failed-first — a target already retired this session for
|
|
2001
|
+
// outrunning the whole batch budget is not fired again. Without
|
|
2002
|
+
// this the retire branch above was decorative: it logged, and the
|
|
2003
|
+
// candidate loop three lines down re-resolved the same file and
|
|
2004
|
+
// spawned it anyway, with its attempt counter reset to zero.
|
|
2005
|
+
if (retiredKeys.has(targetKey)) {
|
|
2006
|
+
retiredSkips++;
|
|
2007
|
+
dbg(`turn_end: ${display} → test target retired earlier this session (outran the turn-end batch budget), skipping spawn (${path.relative(cwd, target.testFile)})`);
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
// #2522: built-in exclusion for turn-end SELECTION — a resolved
|
|
2011
|
+
// target under an integration/e2e directory or naming convention
|
|
2012
|
+
// is never auto-fired, whichever strategy (failed-first/related/
|
|
2013
|
+
// self) produced it. No per-project config knob (maintainer
|
|
2014
|
+
// decision); see `TURN_END_EXCLUDED_TEST_GLOBS`.
|
|
2015
|
+
// LATENT HAZARD, deliberately left as-is: this `continue` skips the
|
|
2016
|
+
// candidate entirely, including `retireMissingFailedTargets`. If an
|
|
2017
|
+
// excluded target were ever seeded into the persisted failed set
|
|
2018
|
+
// (it cannot be today — nothing writes that set except a runner
|
|
2019
|
+
// RESULT, and an excluded target is never run, so it can never
|
|
2020
|
+
// produce one), it would sit there unretired forever, chosen by
|
|
2021
|
+
// the failed-first strategy on every turn and dropped here on
|
|
2022
|
+
// every turn. Any future writer of the failed set must retire
|
|
2023
|
+
// excluded entries at the write site, not here.
|
|
2024
|
+
if (isExcludedTestTarget(target.testFile, cwd)) {
|
|
2025
|
+
excludedTargets++;
|
|
2026
|
+
dbg(`turn_end: ${display} → test target excluded (integration/e2e), skipping spawn (${path.relative(cwd, target.testFile)})`);
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
// #2504: a conventional target that no longer exists on disk still
|
|
2030
|
+
// cost a full runner spawn, which came back "Test file not found"
|
|
2031
|
+
// and was then dropped as an expected skip further down. 9 of the
|
|
2032
|
+
// reported turn's 59 spawns were this. One statSync is cheaper
|
|
2033
|
+
// than a vitest process by four orders of magnitude.
|
|
2034
|
+
if (!fs.existsSync(target.testFile)) {
|
|
2035
|
+
missingTargetFiles++;
|
|
2036
|
+
dbg(`turn_end: ${display} → test file missing, skipping spawn (${path.relative(cwd, target.testFile)})`);
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
|
|
2040
|
+
overCapTargets++;
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
1498
2043
|
targets.push(target);
|
|
1499
2044
|
dbg(`turn_end: ${display} → test ${target.runner} ${path.relative(cwd, target.testFile)} (${target.strategy}${isNeighbor ? ", cascade-neighbor" : ""})`);
|
|
1500
2045
|
}
|
|
@@ -1502,11 +2047,30 @@ export async function handleTurnEnd(deps) {
|
|
|
1502
2047
|
dbg(`turn_end: ${display} → no test file found${isNeighbor ? " (cascade-neighbor)" : ""}`);
|
|
1503
2048
|
}
|
|
1504
2049
|
}
|
|
2050
|
+
if (excludedTargets > 0) {
|
|
2051
|
+
dbg(`turn_end: excluded ${excludedTargets} test target(s) under the built-in integration/e2e exclusion list`);
|
|
2052
|
+
}
|
|
2053
|
+
if (retiredSkips > 0) {
|
|
2054
|
+
dbg(`turn_end: skipped ${retiredSkips} test target(s) retired earlier this session for outrunning the turn-end batch budget`);
|
|
2055
|
+
}
|
|
2056
|
+
if (missingTargetFiles > 0) {
|
|
2057
|
+
dbg(`turn_end: skipped ${missingTargetFiles} test target(s) whose file no longer exists`);
|
|
2058
|
+
}
|
|
2059
|
+
if (overCapTargets > 0) {
|
|
2060
|
+
// Never silent: the agent is told that some of this turn's tests were
|
|
2061
|
+
// not run, rather than reading an all-green batch that covered part
|
|
2062
|
+
// of the edit set.
|
|
2063
|
+
recordDegradationOnce({
|
|
2064
|
+
kind: "test-runner-batch-capped",
|
|
2065
|
+
subject: cwd,
|
|
2066
|
+
reason: `turn touched more test targets than one turn may fire; ran ${TEST_RUNNER_MAX_TARGETS}, skipped ${overCapTargets} — re-run the remainder with lens_diagnostics or edit them in a smaller batch`,
|
|
2067
|
+
});
|
|
2068
|
+
dbg(`turn_end: test target count capped at ${TEST_RUNNER_MAX_TARGETS}, ${overCapTargets} skipped`);
|
|
2069
|
+
}
|
|
1505
2070
|
if (targets.length > 0) {
|
|
1506
|
-
dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking)`);
|
|
2071
|
+
dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking, max ${TEST_RUNNER_BATCH_CONCURRENCY} concurrent)`);
|
|
1507
2072
|
const firedAtTurn = runtime.turnIndex;
|
|
1508
|
-
const firedSessionId =
|
|
1509
|
-
const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
2073
|
+
const firedSessionId = turnSessionId;
|
|
1510
2074
|
const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
|
|
1511
2075
|
const provenanceFiles = [
|
|
1512
2076
|
...candidates.map((candidate) => ({
|
|
@@ -1524,13 +2088,91 @@ export async function handleTurnEnd(deps) {
|
|
|
1524
2088
|
generation: testRunGeneration,
|
|
1525
2089
|
files: provenanceFiles,
|
|
1526
2090
|
});
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2091
|
+
writeTestFindings({
|
|
2092
|
+
...(priorTestCache ?? { content: "" }),
|
|
2093
|
+
testRunGeneration,
|
|
2094
|
+
},
|
|
2095
|
+
// Consumed above into `targets` (or retired, or dropped). Anything
|
|
2096
|
+
// this turn HELD is re-asserted by the writer itself; the batch's own
|
|
2097
|
+
// outcome adds the NEW cut set below.
|
|
2098
|
+
[]);
|
|
2099
|
+
runTestTargetsBounded({
|
|
2100
|
+
targets,
|
|
2101
|
+
concurrency: TEST_RUNNER_BATCH_CONCURRENCY,
|
|
2102
|
+
budgetMs: TEST_RUNNER_BATCH_BUDGET_MS,
|
|
2103
|
+
// Both bounds, per AGENTS.md: a wall budget AND the ambient
|
|
2104
|
+
// abort signal the rest of the spawn layer already honours.
|
|
2105
|
+
signal: getAmbientAbortSignal(),
|
|
2106
|
+
run: (t, batchSignal) => testRunnerClient.runTestFileAsync(t.testFile, cwd, {
|
|
2107
|
+
runner: t.runner,
|
|
2108
|
+
config: t.config,
|
|
2109
|
+
turnIndex: firedAtTurn,
|
|
2110
|
+
// #2522 R2 F1: the BATCH's signal, so spending the 20s wall
|
|
2111
|
+
// budget kills this spawn rather than letting it run out its
|
|
2112
|
+
// own 60s timeout behind a batch that has already returned.
|
|
2113
|
+
signal: batchSignal,
|
|
2114
|
+
}),
|
|
2115
|
+
})
|
|
2116
|
+
.then(({ results, deferred, stopReason }) => {
|
|
2117
|
+
const deferredTargets = deferred.map((t) => ({
|
|
2118
|
+
testFile: t.testFile,
|
|
2119
|
+
runner: t.runner,
|
|
2120
|
+
// One more cut batch for this target. Read back by the
|
|
2121
|
+
// selection loop above, which retires it at
|
|
2122
|
+
// TEST_RUNNER_MAX_DEFERRALS rather than carrying it forever.
|
|
2123
|
+
attempts: (t.deferralAttempts ?? 0) + 1,
|
|
2124
|
+
// #2522 R3 F5: stamped with the session that cut it, so the
|
|
2125
|
+
// next session re-arms this target instead of inheriting an
|
|
2126
|
+
// attempt count measured under a load it never saw.
|
|
2127
|
+
sessionId: firedSessionId,
|
|
2128
|
+
}));
|
|
2129
|
+
if (deferred.length > 0) {
|
|
2130
|
+
// #2522 review round 2, F4: `incrementDegradationCount`, not
|
|
2131
|
+
// `recordDegradationOnce`. At a 20s budget this is a
|
|
2132
|
+
// RECURRING event, and a once-per-subject entry reports the
|
|
2133
|
+
// tenth cut batch of a session exactly like the first.
|
|
2134
|
+
incrementDegradationCount({
|
|
2135
|
+
kind: "test-runner-batch-capped",
|
|
2136
|
+
subject: `${cwd}:${stopReason ?? "incomplete"}`,
|
|
2137
|
+
reason: `test batch stopped early (${stopReason ?? "incomplete"}); ${deferred.length} target(s) killed or never dispatched, deferred to the next turn`,
|
|
2138
|
+
});
|
|
2139
|
+
dbg(`turn_end: test batch stopped early (${stopReason ?? "incomplete"}), ${deferred.length} target(s) deferred to the next turn`);
|
|
2140
|
+
}
|
|
2141
|
+
const deferralNote = deferred.length > 0
|
|
2142
|
+
? `${deferred.length} test target(s) did not finish within the turn-end batch budget and are deferred to the next turn (they run first): ${deferredTargets
|
|
2143
|
+
.map((t) => path.relative(cwd, t.testFile))
|
|
2144
|
+
.join(", ")}`
|
|
2145
|
+
: "";
|
|
2146
|
+
/**
|
|
2147
|
+
* A newer batch has already published for this project — this
|
|
2148
|
+
* one's results are stale by construction and must not overwrite
|
|
2149
|
+
* it. Was inlined three times; extracted once round 2 added a
|
|
2150
|
+
* fourth call site.
|
|
2151
|
+
*/
|
|
2152
|
+
const supersededByNewerGeneration = (label) => {
|
|
2153
|
+
const current = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
2154
|
+
const currentGeneration = current?.testRunGeneration;
|
|
2155
|
+
if (currentGeneration !== undefined &&
|
|
2156
|
+
currentGeneration > testRunGeneration) {
|
|
2157
|
+
dbg(`turn_end: ${label}test generation ${testRunGeneration} superseded by ${currentGeneration}`);
|
|
2158
|
+
// #2522 review round 3, F3: returning here USED to drop this
|
|
2159
|
+
// batch's cut targets entirely. The newer batch's own pre-run
|
|
2160
|
+
// write already cleared them, and it never saw this set — so
|
|
2161
|
+
// with two overlapping batches the targets the older one was
|
|
2162
|
+
// cut on were never deferred, never re-selected, and the suite
|
|
2163
|
+
// they belong to simply never ran again. Hand them over
|
|
2164
|
+
// instead. Round 4, I2: the merge is `writeTestFindings`'s own
|
|
2165
|
+
// rule now, not a second copy of it here — every branch below
|
|
2166
|
+
// merges the same way, so a NEWER batch publishing after this
|
|
2167
|
+
// one no longer flattens what this hand-over just recorded.
|
|
2168
|
+
if (deferredTargets.length > 0) {
|
|
2169
|
+
writeTestFindings({ ...(current ?? { content: "" }) }, deferredTargets);
|
|
2170
|
+
dbg(`turn_end: ${label}carried ${deferredTargets.length} cut target(s) into the newer generation's deferral set`);
|
|
2171
|
+
}
|
|
2172
|
+
return true;
|
|
2173
|
+
}
|
|
2174
|
+
return false;
|
|
2175
|
+
};
|
|
1534
2176
|
const publishedAgainst = snapshotAdvisoryProvenance({
|
|
1535
2177
|
cwd,
|
|
1536
2178
|
runtime,
|
|
@@ -1553,13 +2195,40 @@ export async function handleTurnEnd(deps) {
|
|
|
1553
2195
|
const stale = runtime.turnIndex !== firedAtTurn;
|
|
1554
2196
|
const failures = [];
|
|
1555
2197
|
const resultValues = [];
|
|
2198
|
+
let rejectedCount = 0;
|
|
2199
|
+
// #2522: whether ANY reported item is a genuine failing test
|
|
2200
|
+
// (`failed > 0`) as opposed to the runner itself never
|
|
2201
|
+
// completing (timeout, missing provider/binary, a rejected
|
|
2202
|
+
// promise). Drives the delivery framing below — a batch made
|
|
2203
|
+
// up ENTIRELY of runner errors is not something the agent
|
|
2204
|
+
// introduced, so it must not read as "fix before continuing".
|
|
2205
|
+
let hasRealFailure = false;
|
|
1556
2206
|
for (const r of results) {
|
|
1557
2207
|
if (r.status === "rejected") {
|
|
2208
|
+
rejectedCount++;
|
|
2209
|
+
emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:rejected`, {
|
|
2210
|
+
filePath: cwd,
|
|
2211
|
+
durationMs: 0,
|
|
2212
|
+
metadata: {
|
|
2213
|
+
outcome: "runner-promise-rejected",
|
|
2214
|
+
sessionId: firedSessionId,
|
|
2215
|
+
generation: testRunGeneration,
|
|
2216
|
+
targetCount: targets.length,
|
|
2217
|
+
droppedDetailCount: 0,
|
|
2218
|
+
reason: String(r.reason).slice(0, 500),
|
|
2219
|
+
},
|
|
2220
|
+
}, {
|
|
2221
|
+
ledgerKind: "test-runner-delivery",
|
|
2222
|
+
reason: "test runner promise rejected",
|
|
2223
|
+
capPerTurn: { limit: 8, turnIndex: firedAtTurn },
|
|
2224
|
+
});
|
|
1558
2225
|
dbg(`turn_end: test run rejected — ${r.reason}`);
|
|
1559
2226
|
continue;
|
|
1560
2227
|
}
|
|
1561
2228
|
resultValues.push(r.value);
|
|
1562
2229
|
const { file, runner, passed, failed, duration, error } = r.value;
|
|
2230
|
+
if (failed > 0)
|
|
2231
|
+
hasRealFailure = true;
|
|
1563
2232
|
const shortFile = path.basename(file);
|
|
1564
2233
|
// #1479: `(0ms)` used to be printed for a run nobody
|
|
1565
2234
|
// timed — a payload with no suite timestamps, an
|
|
@@ -1581,19 +2250,33 @@ export async function handleTurnEnd(deps) {
|
|
|
1581
2250
|
// `elapsed` is: the pair read as a nested ternary, which
|
|
1582
2251
|
// this line only got flagged for because #1479 touched it.
|
|
1583
2252
|
const verdict = failed > 0 ? "FAIL" : "PASS";
|
|
1584
|
-
|
|
1585
|
-
|
|
2253
|
+
// #2532 review S1: folded onto `isRunnerErrorResult` instead of
|
|
2254
|
+
// re-deriving `error && passed === 0 && failed === 0` here — that
|
|
2255
|
+
// local spelling missed a runner error reported alongside partial
|
|
2256
|
+
// passes (pytest `Interrupted` after some tests already passed),
|
|
2257
|
+
// which read as a clean "PASS Np/0f" line with the error silently
|
|
2258
|
+
// dropped. A partial run says so explicitly rather than reading
|
|
2259
|
+
// as a clean pass.
|
|
2260
|
+
const summary = isRunnerErrorResult(r.value)
|
|
2261
|
+
? passed > 0
|
|
2262
|
+
? `error: ${error} (${passed} passed before)`
|
|
2263
|
+
: `error: ${error}`
|
|
1586
2264
|
: `${verdict} ${passed}p/${failed}f (${elapsed})`;
|
|
1587
2265
|
dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} → ${summary}`);
|
|
1588
2266
|
// #1524: also fires on `error` alone, not just `failed > 0`.
|
|
1589
|
-
// A runner
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
2267
|
+
// A runner error (the suite never started, or was
|
|
2268
|
+
// interrupted before finishing — spawn/config/timeout
|
|
2269
|
+
// failure) can arrive with `failed === 0` even when tests
|
|
2270
|
+
// DID pass before it (#2532 review S2 — not "by
|
|
2271
|
+
// construction": `parsePytestOutput` sets `error` from the
|
|
2272
|
+
// exit code independently of the parsed counts, so
|
|
2273
|
+
// `isRunnerErrorResult` is `failed === 0 && !!error`, not an
|
|
2274
|
+
// invariant elsewhere). Gating on `failed > 0` alone dropped
|
|
2275
|
+
// it silently: the agent got no context at all, and the
|
|
2276
|
+
// empty `failures` array below sent this result down the
|
|
2277
|
+
// "all tests passed" branch, clearing any prior real
|
|
2278
|
+
// test-failure git-guard blocker. `formatResult` already
|
|
2279
|
+
// renders the error-only case as "Could not run tests: ...".
|
|
1597
2280
|
if (failed > 0 || error) {
|
|
1598
2281
|
// #2028: "Test file not found" is an expected skip
|
|
1599
2282
|
// (conventional test path without an actual file),
|
|
@@ -1607,17 +2290,32 @@ export async function handleTurnEnd(deps) {
|
|
|
1607
2290
|
failures.push(formatted);
|
|
1608
2291
|
}
|
|
1609
2292
|
}
|
|
2293
|
+
if (rejectedCount > 0) {
|
|
2294
|
+
failures.push(`Test runner rejected ${rejectedCount} promise(s) before producing a structured result.`);
|
|
2295
|
+
}
|
|
1610
2296
|
if (failures.length > 0) {
|
|
1611
|
-
|
|
1612
|
-
if (currentGeneration !== undefined &&
|
|
1613
|
-
currentGeneration > testRunGeneration) {
|
|
1614
|
-
dbg(`turn_end: test generation ${testRunGeneration} superseded by ${currentGeneration}`);
|
|
2297
|
+
if (supersededByNewerGeneration(""))
|
|
1615
2298
|
return;
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2299
|
+
const content = [
|
|
2300
|
+
stale
|
|
2301
|
+
? "[from a prior turn — the edit that triggered this run had already been superseded by the time results came back]"
|
|
2302
|
+
: "",
|
|
2303
|
+
failures.join("\n\n"),
|
|
2304
|
+
// A batch can BOTH find a real failure and be cut short.
|
|
2305
|
+
// The agent needs to see the failure AND that the run was
|
|
2306
|
+
// incomplete, or it reads a partial list as the full one.
|
|
2307
|
+
deferralNote,
|
|
2308
|
+
]
|
|
2309
|
+
.filter(Boolean)
|
|
2310
|
+
.join("\n\n");
|
|
2311
|
+
// #2522: nothing in this batch is a genuine failing test —
|
|
2312
|
+
// every entry is a runner error (timeout, missing
|
|
2313
|
+
// provider/binary, a rejected promise). `peekTestFindings`
|
|
2314
|
+
// reads this to deliver advisory framing instead of
|
|
2315
|
+
// "fix before continuing", since the agent introduced
|
|
2316
|
+
// nothing here to fix.
|
|
2317
|
+
const runnerErrorOnly = !hasRealFailure;
|
|
2318
|
+
writeTestFindings({
|
|
1621
2319
|
content,
|
|
1622
2320
|
stale,
|
|
1623
2321
|
results: resultValues,
|
|
@@ -1626,7 +2324,20 @@ export async function handleTurnEnd(deps) {
|
|
|
1626
2324
|
publishedAgainst,
|
|
1627
2325
|
provenance: publishedAgainst,
|
|
1628
2326
|
superseded,
|
|
1629
|
-
|
|
2327
|
+
runnerErrorOnly,
|
|
2328
|
+
}, deferredTargets);
|
|
2329
|
+
try {
|
|
2330
|
+
deps.onTestRunnerComplete?.({
|
|
2331
|
+
cwd,
|
|
2332
|
+
sessionId: firedSessionId,
|
|
2333
|
+
generation: testRunGeneration,
|
|
2334
|
+
targetCount: targets.length,
|
|
2335
|
+
hasFindings: true,
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
catch (deliveryErr) {
|
|
2339
|
+
dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
|
|
2340
|
+
}
|
|
1630
2341
|
if (getFlag("lens-guard") &&
|
|
1631
2342
|
firedSessionId === runtime.telemetrySessionId) {
|
|
1632
2343
|
// #1524: `&& !value.error` — a runner-error result has
|
|
@@ -1648,13 +2359,117 @@ export async function handleTurnEnd(deps) {
|
|
|
1648
2359
|
if (cleanFiles.length > 0) {
|
|
1649
2360
|
clearGitGuardTestFailure(cacheManager, cwd, runtime, cleanFiles);
|
|
1650
2361
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2362
|
+
// #2532: `runnerErrorOnly` (computed above for the turn-end
|
|
2363
|
+
// delivery framing) is true exactly when nothing in this
|
|
2364
|
+
// batch is a genuine failing test — every entry is a
|
|
2365
|
+
// runner error the agent did not introduce. Without this
|
|
2366
|
+
// gate, an all-runner-error batch still called
|
|
2367
|
+
// `mergeGitGuardTestFailure` with an EMPTY failed-files
|
|
2368
|
+
// list, which unconditionally sets `hasBlockers: true`:
|
|
2369
|
+
// the identical event the turn-end message reports as
|
|
2370
|
+
// advisory read as "COMMIT BLOCKED" under --lens-guard.
|
|
2371
|
+
if (!runnerErrorOnly) {
|
|
2372
|
+
mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, resultValues
|
|
2373
|
+
.filter((value) => value.failed > 0)
|
|
2374
|
+
.map((value) => value.file));
|
|
2375
|
+
}
|
|
2376
|
+
else {
|
|
2377
|
+
// #2532 review T2: the skip above is otherwise pull-only —
|
|
2378
|
+
// nothing records that a batch with real content
|
|
2379
|
+
// (`failures.length > 0`) was deliberately kept OFF the
|
|
2380
|
+
// `--lens-guard` blocker because every entry was a runner
|
|
2381
|
+
// error. Same phase/ledger as the rejected-promise event
|
|
2382
|
+
// above, so both `--lens-guard` demotions land in one
|
|
2383
|
+
// queryable place. Reached only inside the enclosing
|
|
2384
|
+
// `getFlag("lens-guard") && firedSessionId === …` check —
|
|
2385
|
+
// no point recording a demotion the flag can't act on.
|
|
2386
|
+
emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:runner-error-only`, {
|
|
2387
|
+
filePath: cwd,
|
|
2388
|
+
durationMs: 0,
|
|
2389
|
+
metadata: {
|
|
2390
|
+
outcome: "runner-error-only-not-blocking",
|
|
2391
|
+
sessionId: firedSessionId,
|
|
2392
|
+
generation: testRunGeneration,
|
|
2393
|
+
targetCount: targets.length,
|
|
2394
|
+
droppedDetailCount: 0,
|
|
2395
|
+
},
|
|
2396
|
+
}, {
|
|
2397
|
+
ledgerKind: "test-runner-delivery",
|
|
2398
|
+
reason: "runner-error-only batch kept off the --lens-guard blocker",
|
|
2399
|
+
capPerTurn: { limit: 8, turnIndex: firedAtTurn },
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
1654
2402
|
}
|
|
1655
|
-
dbg(`turn_end: ${failures.length} test failure(s) cached for
|
|
2403
|
+
dbg(`turn_end: ${failures.length} test failure(s) cached for pull diagnostics and post-agent delivery${stale ? " (stale — turn advanced while tests ran)" : ""}`);
|
|
2404
|
+
}
|
|
2405
|
+
else if (deferred.length > 0) {
|
|
2406
|
+
// #2522 review round 2, F1: a PARTIAL batch is not a clean
|
|
2407
|
+
// batch. Pre-round-2 this fell through to the branch below —
|
|
2408
|
+
// `content: ""`, "all tests passed", and a `--lens-guard`
|
|
2409
|
+
// clear — on the strength of a run that never finished. The
|
|
2410
|
+
// record names the deferral instead; the commit gate is left
|
|
2411
|
+
// exactly as it was, because nothing here proves anything
|
|
2412
|
+
// passed that was not already proven.
|
|
2413
|
+
if (supersededByNewerGeneration("deferred "))
|
|
2414
|
+
return;
|
|
2415
|
+
writeTestFindings({
|
|
2416
|
+
content: deferralNote,
|
|
2417
|
+
stale,
|
|
2418
|
+
results: resultValues,
|
|
2419
|
+
testRunGeneration,
|
|
2420
|
+
launchedFrom,
|
|
2421
|
+
publishedAgainst,
|
|
2422
|
+
provenance: publishedAgainst,
|
|
2423
|
+
superseded,
|
|
2424
|
+
// Advisory framing: an unfinished batch is not a
|
|
2425
|
+
// failure the agent introduced, exactly like a runner
|
|
2426
|
+
// error. See `TestRunnerFindingsCache.runnerErrorOnly`.
|
|
2427
|
+
runnerErrorOnly: true,
|
|
2428
|
+
}, deferredTargets);
|
|
2429
|
+
try {
|
|
2430
|
+
deps.onTestRunnerComplete?.({
|
|
2431
|
+
cwd,
|
|
2432
|
+
sessionId: firedSessionId,
|
|
2433
|
+
generation: testRunGeneration,
|
|
2434
|
+
targetCount: targets.length,
|
|
2435
|
+
hasFindings: true,
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
catch (deliveryErr) {
|
|
2439
|
+
dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
|
|
2440
|
+
}
|
|
2441
|
+
dbg(`turn_end: partial test batch — ${results.length} target(s) settled, ${deferred.length} deferred to the next turn; NOT recorded as a clean run`);
|
|
1656
2442
|
}
|
|
1657
2443
|
else if (results.length > 0) {
|
|
2444
|
+
if (supersededByNewerGeneration("clean "))
|
|
2445
|
+
return;
|
|
2446
|
+
writeTestFindings({
|
|
2447
|
+
...(priorTestCache ?? { content: "" }),
|
|
2448
|
+
content: "",
|
|
2449
|
+
stale: false,
|
|
2450
|
+
results: resultValues,
|
|
2451
|
+
testRunGeneration,
|
|
2452
|
+
launchedFrom,
|
|
2453
|
+
publishedAgainst,
|
|
2454
|
+
provenance: publishedAgainst,
|
|
2455
|
+
superseded,
|
|
2456
|
+
},
|
|
2457
|
+
// Nothing was cut. This turn's own settled entries drop out;
|
|
2458
|
+
// I2 keeps every row this batch does not own — including a
|
|
2459
|
+
// concurrent older batch's hand-over.
|
|
2460
|
+
[]);
|
|
2461
|
+
try {
|
|
2462
|
+
deps.onTestRunnerComplete?.({
|
|
2463
|
+
cwd,
|
|
2464
|
+
sessionId: firedSessionId,
|
|
2465
|
+
generation: testRunGeneration,
|
|
2466
|
+
targetCount: targets.length,
|
|
2467
|
+
hasFindings: false,
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
catch (deliveryErr) {
|
|
2471
|
+
dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
|
|
2472
|
+
}
|
|
1658
2473
|
if (getFlag("lens-guard") &&
|
|
1659
2474
|
firedSessionId === runtime.telemetrySessionId) {
|
|
1660
2475
|
clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues.map((value) => value.file));
|
|
@@ -1664,6 +2479,17 @@ export async function handleTurnEnd(deps) {
|
|
|
1664
2479
|
})
|
|
1665
2480
|
.catch(() => { });
|
|
1666
2481
|
}
|
|
2482
|
+
else if (carriedDeferred.length > 0) {
|
|
2483
|
+
// Nothing fired, so no batch outcome will write the list back, but this
|
|
2484
|
+
// turn still settled part of it (targets retired at the cap, or dropped
|
|
2485
|
+
// because their file is gone). Persist that here rather than re-reading
|
|
2486
|
+
// a set of dead paths on every future turn; `writeTestFindings` carries
|
|
2487
|
+
// this turn's retirements alongside, which is what the deferral-cap
|
|
2488
|
+
// branch above depends on when it retires the LAST carried target and
|
|
2489
|
+
// fires nothing. Rows this turn only HELD (over the cap) or does not
|
|
2490
|
+
// own (another session's) are re-asserted by the writer itself.
|
|
2491
|
+
writeTestFindings({ ...(priorTestCache ?? { content: "" }) }, []);
|
|
2492
|
+
}
|
|
1667
2493
|
}
|
|
1668
2494
|
if (runtime.errorDebtBaseline && files.length > 0) {
|
|
1669
2495
|
dbg("turn_end: marking error debt check for next session");
|
|
@@ -1797,10 +2623,79 @@ export async function handleTurnEnd(deps) {
|
|
|
1797
2623
|
fileSeqByPath,
|
|
1798
2624
|
deltaOnly: !getFlag("lens-actionable-warning-all"),
|
|
1799
2625
|
dbg,
|
|
2626
|
+
// #2504: this call is AWAITED on the turn_end hook, so its cost is
|
|
2627
|
+
// terminal-blocking time. The file cap and wall budget are its
|
|
2628
|
+
// bounds; the abort signal is the second one AGENTS.md requires.
|
|
2629
|
+
signal: getAmbientAbortSignal(),
|
|
2630
|
+
// A cold-cache turn hands the fresh-pull loop back here, off the
|
|
2631
|
+
// hook — it lands in the same cache the in-band report goes to.
|
|
2632
|
+
onDeferredReport: (deferred) => {
|
|
2633
|
+
try {
|
|
2634
|
+
// #2504 review round 2 (F2): GUARDED. This callback fires up
|
|
2635
|
+
// to a minute after the turn that armed it, and the report
|
|
2636
|
+
// it carries is stamped with THAT turn's
|
|
2637
|
+
// turnIndex/projectSeq. Writing it unconditionally
|
|
2638
|
+
// overwrote a newer report, which `agent_end` then rejected
|
|
2639
|
+
// as `project_seq_mismatch` (silently skipping the autofix
|
|
2640
|
+
// pass) and `lens_diagnostics` re-served as a stale delta.
|
|
2641
|
+
// #2504 review round 4 (F1): guarded PER FILE, not per
|
|
2642
|
+
// report. The round-3 shape (publish, or discard whole
|
|
2643
|
+
// when something newer is persisted) composed with
|
|
2644
|
+
// incumbent-wins into "publish nothing" — every turn_end
|
|
2645
|
+
// with modified files persists an in-band report with a
|
|
2646
|
+
// strictly increasing turnIndex, and the decline fires
|
|
2647
|
+
// exactly when such a turn runs while a loop is in
|
|
2648
|
+
// flight, so a decline always implied a supersede. The
|
|
2649
|
+
// merge upserts the entries whose file has not moved and
|
|
2650
|
+
// drops only those that have.
|
|
2651
|
+
writeDeferredActionableWarningsReport({
|
|
2652
|
+
cacheManager,
|
|
2653
|
+
cwd,
|
|
2654
|
+
report: deferred,
|
|
2655
|
+
// The LIVE per-file sequence at WRITE time is the
|
|
2656
|
+
// baseline the merge judges each entry against. The
|
|
2657
|
+
// runtime is the only thing that knows it: a file
|
|
2658
|
+
// edited into cleanliness by a later turn is absent
|
|
2659
|
+
// from the persisted report entirely.
|
|
2660
|
+
getFileSeq: getFileSeq
|
|
2661
|
+
? (filePath) => getFileSeq.call(runtime, filePath)
|
|
2662
|
+
: undefined,
|
|
2663
|
+
dbg,
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
catch (deferErr) {
|
|
2667
|
+
dbg(`turn_end: deferred actionable-warnings write failed — ${deferErr}`);
|
|
2668
|
+
}
|
|
2669
|
+
},
|
|
2670
|
+
});
|
|
2671
|
+
// #2504 review round 5 (F1): THE publish choke point. This was a
|
|
2672
|
+
// blind `writeActionableWarningsReport` while the deferred callback
|
|
2673
|
+
// above read-modify-wrote the SAME cache key. The deferred merge can
|
|
2674
|
+
// land anywhere inside this handleTurnEnd -- the cascade settle,
|
|
2675
|
+
// knip, madge, the test batch, the in-band LSP enrichment are all
|
|
2676
|
+
// awaited between the moment it is armed and the moment we get
|
|
2677
|
+
// here -- and the blind write erased it 607 ms later in the
|
|
2678
|
+
// reviewer's trace. The publisher now always reads what is persisted
|
|
2679
|
+
// and merges per file, so the two writers cannot race. It carries
|
|
2680
|
+
// forward only entries a DEFERRAL produced, and only while their
|
|
2681
|
+
// file has not moved, so a `turn_delta` report does not accumulate
|
|
2682
|
+
// every prior turn's findings.
|
|
2683
|
+
const publishResult = publishActionableWarningsReport(cacheManager, cwd, report, {
|
|
2684
|
+
origin: "in-band",
|
|
2685
|
+
getFileSeq: getFileSeq
|
|
2686
|
+
? (filePath) => getFileSeq.call(runtime, filePath)
|
|
2687
|
+
: undefined,
|
|
2688
|
+
dbg,
|
|
1800
2689
|
});
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2690
|
+
// #2504 review round 6 (a): the advisory must read the MERGED report,
|
|
2691
|
+
// not the pre-merge `report` this turn assembled -- a rescued deferred
|
|
2692
|
+
// entry lives only on `publishResult.report`, and formatting the
|
|
2693
|
+
// pre-merge report silently dropped it from the turn_end advisory
|
|
2694
|
+
// even though it was correctly persisted to cache.
|
|
2695
|
+
// #2521: `cwd` is what resolves the store location the advisory
|
|
2696
|
+
// names -- it is the SAME cwd `publishActionableWarningsReport`
|
|
2697
|
+
// just wrote through, so the two cannot disagree.
|
|
2698
|
+
const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd);
|
|
1804
2699
|
// @delivery-surface: runtime-turn:actionable-warnings-advisory
|
|
1805
2700
|
if (advisory)
|
|
1806
2701
|
advisoryParts.push(advisory);
|
|
@@ -1809,7 +2704,13 @@ export async function handleTurnEnd(deps) {
|
|
|
1809
2704
|
sessionId: runtime.telemetrySessionId,
|
|
1810
2705
|
metadata: {
|
|
1811
2706
|
turnIndex: runtime.turnIndex,
|
|
1812
|
-
|
|
2707
|
+
// #2504 review round 7 (F2): the MERGED report, matching the
|
|
2708
|
+
// advisory text above it (round 6, a) -- a rescued deferred
|
|
2709
|
+
// entry lives only on `publishResult.report`, and the pre-merge
|
|
2710
|
+
// `report` this turn assembled undercounts it. This is the value
|
|
2711
|
+
// scripts/analyze-pi-lens-logs.mjs sums, so an undercount here is
|
|
2712
|
+
// a silent miscount there too.
|
|
2713
|
+
unsuppressed: publishResult.report.summary.unsuppressed,
|
|
1813
2714
|
},
|
|
1814
2715
|
});
|
|
1815
2716
|
logLatency({
|
|
@@ -1850,7 +2751,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1850
2751
|
});
|
|
1851
2752
|
writeCodeQualityWarningsReport(cacheManager, cwd, qualityReport);
|
|
1852
2753
|
appendCodeQualityWarningsHistory(cwd, qualityReport);
|
|
1853
|
-
const advisory = formatCodeQualityWarningsAdvisory(qualityReport);
|
|
2754
|
+
const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd);
|
|
1854
2755
|
// @delivery-surface: runtime-turn:code-quality-warnings-advisory
|
|
1855
2756
|
if (advisory)
|
|
1856
2757
|
advisoryParts.push(advisory);
|
|
@@ -1975,6 +2876,10 @@ export async function handleTurnEnd(deps) {
|
|
|
1975
2876
|
let lateAuxExpired = 0;
|
|
1976
2877
|
let lateAuxCeilingExhausted = 0;
|
|
1977
2878
|
let lateAuxAnswered = 0;
|
|
2879
|
+
let lateAuxNotifyStallDemoted = 0;
|
|
2880
|
+
const lateAuxCoverageGapPairs = [];
|
|
2881
|
+
let lateAuxCoverageGapDetailCount = 0;
|
|
2882
|
+
let lateAuxCoverageGapDropCount = 0;
|
|
1978
2883
|
const lateAuxStuckPairs = [];
|
|
1979
2884
|
if (drainedPairs.length > 0) {
|
|
1980
2885
|
const byFile = new Map();
|
|
@@ -2026,6 +2931,40 @@ export async function handleTurnEnd(deps) {
|
|
|
2026
2931
|
lateAuxClientGone += 1;
|
|
2027
2932
|
continue;
|
|
2028
2933
|
}
|
|
2934
|
+
if (cachedEntry.notifyStallDemoted) {
|
|
2935
|
+
if (cachedEntry.demotedAt === undefined ||
|
|
2936
|
+
pair.markedAtMs > cachedEntry.demotedAt) {
|
|
2937
|
+
// A pair marked after teardown belongs to the missing
|
|
2938
|
+
// generation, so it follows ordinary clientGone handling.
|
|
2939
|
+
lateAuxClientGone += 1;
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
// #2356: notify-stall teardown is a transient absence while the
|
|
2943
|
+
// breaker cools down, but only for a pair marked before teardown.
|
|
2944
|
+
lateAuxNotifyStallDemoted += 1;
|
|
2945
|
+
const pastTtl = isPendingAuxiliaryPastRearmTtl(pair);
|
|
2946
|
+
const atCeiling = (pair.rearmCount ?? 0) >= MAX_LATE_AUX_REARMS;
|
|
2947
|
+
if (!pastTtl && !atCeiling) {
|
|
2948
|
+
rearmPendingAuxiliaryCoverage(pair);
|
|
2949
|
+
lateAuxRearmed += 1;
|
|
2950
|
+
if (lateAuxStuckPairs.length < 20)
|
|
2951
|
+
lateAuxStuckPairs.push({
|
|
2952
|
+
filePath: pair.filePath,
|
|
2953
|
+
serverId: pair.serverId,
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
else {
|
|
2957
|
+
if (pastTtl)
|
|
2958
|
+
lateAuxExpired += 1;
|
|
2959
|
+
else
|
|
2960
|
+
lateAuxCeilingExhausted += 1;
|
|
2961
|
+
lateAuxCoverageGapPairs.push({
|
|
2962
|
+
filePath: pair.filePath,
|
|
2963
|
+
serverId: pair.serverId,
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2029
2968
|
const rawDiags = cachedEntry.diags;
|
|
2030
2969
|
if (cachedEntry.publishedAt === undefined ||
|
|
2031
2970
|
cachedEntry.publishedAt <= pair.markedAtMs) {
|
|
@@ -2117,6 +3056,33 @@ export async function handleTurnEnd(deps) {
|
|
|
2117
3056
|
catch (err) {
|
|
2118
3057
|
dbg(`turn_end: late-auxiliary probe failed: ${err}`);
|
|
2119
3058
|
}
|
|
3059
|
+
// #2356: a demoted scanner that never gets replaced remains a coverage
|
|
3060
|
+
// gap. Re-raise it once when the existing bounded late-pair window closes,
|
|
3061
|
+
// preserving the server/file identity in both the ledger and latency row.
|
|
3062
|
+
for (const pair of lateAuxCoverageGapPairs) {
|
|
3063
|
+
const normalizedPairPath = normalizeMapKey(pair.filePath);
|
|
3064
|
+
const emitted = emitBounded("lsp_scanner_coverage_gap", `${pair.serverId}:${normalizedPairPath}`, {
|
|
3065
|
+
filePath: normalizedPairPath,
|
|
3066
|
+
durationMs: 0,
|
|
3067
|
+
metadata: {
|
|
3068
|
+
source: "late-auxiliary",
|
|
3069
|
+
serverIds: [pair.serverId],
|
|
3070
|
+
reason: "notify-stall-replacement-unavailable",
|
|
3071
|
+
reRaised: true,
|
|
3072
|
+
},
|
|
3073
|
+
}, {
|
|
3074
|
+
ledgerKind: "lsp-scanner-coverage-gap",
|
|
3075
|
+
reason: "notify-stall replacement was not available before late-coverage ceiling",
|
|
3076
|
+
capPerTurn: {
|
|
3077
|
+
limit: LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN,
|
|
3078
|
+
turnIndex: runtime.turnIndex,
|
|
3079
|
+
},
|
|
3080
|
+
});
|
|
3081
|
+
if (emitted)
|
|
3082
|
+
lateAuxCoverageGapDetailCount += 1;
|
|
3083
|
+
else
|
|
3084
|
+
lateAuxCoverageGapDropCount += 1;
|
|
3085
|
+
}
|
|
2120
3086
|
logLatency({
|
|
2121
3087
|
type: "phase",
|
|
2122
3088
|
toolName: "turn_end",
|
|
@@ -2137,6 +3103,10 @@ export async function handleTurnEnd(deps) {
|
|
|
2137
3103
|
expired: lateAuxExpired,
|
|
2138
3104
|
ceilingExhausted: lateAuxCeilingExhausted,
|
|
2139
3105
|
answered: lateAuxAnswered,
|
|
3106
|
+
notifyStallDemoted: lateAuxNotifyStallDemoted,
|
|
3107
|
+
coverageGapReRaised: lateAuxCoverageGapPairs.length,
|
|
3108
|
+
coverageGapReRaisedDetailed: lateAuxCoverageGapDetailCount,
|
|
3109
|
+
coverageGapReRaisedDropped: lateAuxCoverageGapDropCount,
|
|
2140
3110
|
capEvicted: lateAuxCapEvicted,
|
|
2141
3111
|
stuckPairs: lateAuxStuckPairs,
|
|
2142
3112
|
},
|
|
@@ -2325,6 +3295,11 @@ export async function handleTurnEnd(deps) {
|
|
|
2325
3295
|
// answers that from latency.log even when the payload is empty, and
|
|
2326
3296
|
// the payload itself carries the retirement note when it is not.
|
|
2327
3297
|
demotedFindingsRetired,
|
|
3298
|
+
// #2275: the widget-footer store's own dependency-drift retirements —
|
|
3299
|
+
// a separate surface/counter from `demotedFindingsRetired` above,
|
|
3300
|
+
// since a widget retirement never touches `advisoryParts` or the
|
|
3301
|
+
// `turn-end-findings-last` suppression cache the block below clears.
|
|
3302
|
+
widgetDemotedFindingsRetired,
|
|
2328
3303
|
},
|
|
2329
3304
|
});
|
|
2330
3305
|
resetFormatService();
|