@herbertgao/pi-extensions 2026.9.0 → 2026.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
- package/node_modules/@czottmann/pi-automode/README.md +20 -0
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
- package/node_modules/pi-lens/CHANGELOG.md +988 -0
- package/node_modules/pi-lens/README.md +3 -0
- package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
- package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
- package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
- package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
- package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
- package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
- package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
- package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
- package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
- package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
- package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
- package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
- package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
- package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
- package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
- package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
- package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
- package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
- package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
- package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
- package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
- package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
- package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
- package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
- package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
- package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
- package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
- package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
- package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
- package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
- package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
- package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
- package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
- package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
- package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
- package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
- package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
- package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
- package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
- package/node_modules/pi-lens/dist/index.js +64335 -54300
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +127 -5
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
- package/node_modules/pi-lens/dist/tools/shared.js +0 -1
- package/node_modules/pi-lens/docs/agent-guide.md +64 -1
- package/node_modules/pi-lens/docs/configuration.md +222 -0
- package/node_modules/pi-lens/docs/dependencies.md +3 -3
- package/node_modules/pi-lens/docs/features.md +44 -5
- package/node_modules/pi-lens/docs/language-coverage.md +2 -2
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
- package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
- package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
- package/node_modules/pi-lens/package.json +15 -23
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +62 -15
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +5 -1
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +242 -1
- package/node_modules/pi-web-access/index.ts +118 -64
- package/node_modules/pi-web-access/mistral-search.ts +281 -0
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/perplexity.ts +14 -1
- package/node_modules/pi-web-access/utils.ts +23 -6
- package/node_modules/pi-web-access/xai-search.ts +96 -33
- package/package.json +5 -5
|
@@ -16,6 +16,994 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
16
16
|
|
|
17
17
|
### Security
|
|
18
18
|
|
|
19
|
+
## [4.1.4] - 2026-09-07
|
|
20
|
+
|
|
21
|
+
Patch release carrying everything merged since 4.1.3 (152 entries, including 12 under Added and 2 under Deprecated — the additions are extension-internal and CI/test-harness surfaces, no new user-facing commands). Release-QA (`scripts/release-qa.mjs`, #2606) reports SHIP on pi 0.80.10 and pi 0.85.1: 11 of 11 rows witnessed, 0 failed, 0 skipped, git-install row included.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Deliver test-runner failures after the agent settles (refs #2366)** — failed test results remain in the pull-diagnostics and commit-guard cache, then appear once through a provenance-validated non-context custom entry after the agent is idle. Automatic delivery no longer injects a synthetic user message into the next model context.
|
|
26
|
+
|
|
27
|
+
- **Shared config core (refs #2425).** `clients/config-core/` is now the one place a pi-lens configuration is validated, merged, and explained: a schema-driven validate/normalize pipeline that warns and drops unknown fields under the stable `PILENS_CFG_0004`/`PILENS_CFG_0005` codes and refuses prototype-modifying keys (`__proto__`, `constructor`, `prototype`) under the new `PILENS_CFG_0006`, a field-wise merger with per-node `x-merge-strategy` (`replace`/`append`/`keyed:<field>`), per-leaf provenance across the seven source tiers, monotonic deny precedence so a repository config can never re-enable what an operator disabled — while a denial pi-lens itself ships stays overridable by the operator's own global config, environment, or command line — and a trust-gated `ProcessSpec` whose `toSpawnArgs` refuses a project-supplied command unless both the recorded and the live host trust decisions say `trusted`. Both halves of the pipeline bound their own recursion, and `resolveConfig` never throws: a config that cannot be resolved degrades to absent with records rather than failing a session. Diagnostic projections are redacted by construction — no env value, no argv tail, and no absolute home path survives them. No loader is wired to it yet, so there is no user-visible behavior change in this release; `docs/public-api-stability.md` section 5 documents the semantics the loaders will adopt.
|
|
28
|
+
|
|
29
|
+
- **`pilens_effective_config` / `effective_config` answer "why is X running" (closes #2427)** — one query returns the resolved pi-lens configuration with the provenance of every setting (source tier, file, key, trust decision), and for a file you name, its canonical language plus every LSP server with the reason it was selected or denied and every runner that would dispatch. `pilens_health` embeds the same provenance as per-tier counts. Redaction is structural rather than a mode: config paths are `~`-relative, a custom server's command line is cut to the binary itself, environment variables appear as names only, and there is no un-redacted view to ask for. The one runtime behavior change this needed — monotonic deny precedence for `lsp.disabledServers` — has its own `Changed` entry.
|
|
30
|
+
|
|
31
|
+
- **pi-lens now learns which third-party tools edit files by watching (closes #2430)** — a tool whose name and argument shape pi-lens does not recognize used to bypass the whole edit pipeline. It now gets a bounded pre/post snapshot of the path that call names — that file, or a directory's own entries, and nothing else, so a change to a neighbouring file is never blamed on it — and anything that actually changed is replayed through the mutation bridge as a deferred `edit` with the changed line ranges. Ranges are reported only when they can be measured against a full baseline; a partial read, a changed line count or an unreadable file falls back to the whole file rather than naming lines that were never touched. The tool is then remembered as mutating for the session, and persisted under the project's data directory once a second observation confirms it, so later calls and later sessions are classified by name with no snapshot at all — while three quiet observations in a row withdraw a session attribution that turned out to be a coincidence — and a withdrawn tool can be learned again from a later real edit, rather than being written off for the session. Whichever path records an edit, only one does: a tool that is both classified and still being watched no longer produces two change-log receipts for the same edit. An observation the net could not finish — a directory with more entries than it may watch, a capture cut short — is reported as unverifiable instead of counted as evidence the tool changes nothing. An `unclassified-mutating-tool` degradation record names every tool the registry was missing. A tool that names no file at all is caught at `agent_settled`, before the deferred drain, by an incremental content check over the files pi-lens has already read, written, diagnosed or opened on a language server: a rotating window of that set each turn, reading only the files whose size or timestamp actually moved, so the check stays affordable as the set grows and reports how far it got. A file pi-lens has never seen has no baseline and is not covered; a file it cannot verify is named as unverifiable rather than reformatted on a timestamp alone. Every capture carries a timeout, an abort race, a file cap, a hash-byte budget and a per-turn wall-clock budget, and exceeding any of them is a visible degradation rather than a silent skip. After the deferred drain, the record of which files pi-lens wrote itself is retired one file at a time as each is re-checked, rather than all at once: a re-check that runs out of time no longer either suppresses a genuine third-party change to a file it did finish, or forgets that the files it never reached were pi-lens's own. That re-check now covers exactly the files the drain wrote, not the whole set of files pi-lens is tracking, so it completes in one pass and every mark retires on the same turn regardless of how large a project is — a large tracked set could previously park at the same handful of files every turn and leave a file's own drain-output mark standing indefinitely, silently suppressing a real edit to that file. If that record hits its own size cap, the file it drops is named in a degradation record instead of quietly becoming the next turn's phantom mutation. The per-turn snapshot budget now also covers the target's line-hash read — previously the largest part of arming a big file was charged to nobody and could not be aborted — and a tool whose edit pi-lens already recorded by watching still gets its retry records, its write/edit ordering receipt and its export-cache refresh, which an over-broad skip used to drop along with the duplicate bookkeeping it was meant to suppress.
|
|
32
|
+
|
|
33
|
+
- **Agent worktree and orphan-process hygiene (closes #2435)** — New
|
|
34
|
+
`npm run hygiene` (`scripts/prune-agent-worktrees.mjs`) removes finished
|
|
35
|
+
`.claude/worktrees/agent-*` checkouts that are clean, pushed and idle for at
|
|
36
|
+
least 30 minutes, and reaps `tests/fixtures/*` helper processes whose parent
|
|
37
|
+
has exited. Idleness is measured from signals the sweep's own inspection
|
|
38
|
+
does not write — the checkout directory, the worktree's `HEAD`, and the HEAD
|
|
39
|
+
reflog's last entry — never from the git index, which `git status` rewrites.
|
|
40
|
+
It never removes a dirty or unpushed tree, never kills a fixture process
|
|
41
|
+
whose parent is still alive or whose parent pid is unreadable, never kills a
|
|
42
|
+
process that merely names a worktree on its command line, and records every
|
|
43
|
+
kill, removal and degraded process scan as bounded JSONL in
|
|
44
|
+
`~/.pi-lens/hygiene.log`. It runs automatically from the `SessionStart` hook
|
|
45
|
+
on `startup` and `resume` only (never on `/clear`, compaction or a fork), and
|
|
46
|
+
removes at most one tree per run; the `SubagentStop` hook only reaps that
|
|
47
|
+
agent's orphaned test helpers and never removes a worktree. Targeted test
|
|
48
|
+
runs gain `npm run test:targeted`, a shared-slot mode of the machine-wide
|
|
49
|
+
test lock that caps concurrent targeted `vitest` batches (default 2) while
|
|
50
|
+
full-suite runs still take the box exclusively; it requires at least one test
|
|
51
|
+
path.
|
|
52
|
+
|
|
53
|
+
- **`BoundedFifoMap<K,V>` sibling of `BoundedLruCache` (refs #2442)** — `clients/bounded-cache.ts` shipped only a true-LRU bounded map, so every FIFO-shaped bounded cache in the repo hand-rolled the same `map.keys().next().value` evict-oldest block instead. #2432's round-3 review named two such sites; a repo-wide sweep found the idiom 32 times. `BoundedFifoMap` never re-inserts on `get` and never reorders an already-present key on `set` — insertion order stays eviction order, matching native `Map` semantics exactly, so a caller that wants a write to refresh recency does its own `delete`+`set` around it. `BoundedLruCache` now EXTENDS it, overriding only `get` and `set`, so eviction and capacity have one implementation. Both classes' `set()` returns the evicted `[key, value]` pairs (oldest first), and `setMaxEntries()` applies a changed ceiling immediately and reports what it dropped — enough for every eviction-side-effect site (telemetry counters, tombstone sets, WASM-tree retirement, compiled-query handles) to stop hand-rolling the idiom. 28 of 32 occurrences are migrated, including `tree-sitter-cache.ts`'s WASM-retiring LRU, `tree-sitter-client.ts`'s two query caches, `partial-edit-apply.ts`'s path-keyed store (`PathKeyedMap` now takes an optional bound) and the three `clients/review-graph/*` sites. The 4 that remain are named with reasons and tracked as follow-up (#2460): three `Set`-shaped membership caps (no `BoundedSet` exists) and `debug-handles.ts`'s protected-prefix eviction policy. `tests/config/bounded-eviction-idiom-sweep.test.ts` forbids a new hand-rolled instance — per OCCURRENCE, not per file, across all three spellings of the idiom.
|
|
54
|
+
|
|
55
|
+
- **`bounded()` and a governance guard for hook awaits (refs #2523)** — one primitive for the two bounds a pi hook needs, whose type refuses either half on its own: `bounded(promise, { ms, signal, hook, label })` races the work against the hook's abort signal and a wall-clock budget together, so a deadline-only call is a compile error rather than a review catch. Which bound fired decides what a reader sees: a blown BUDGET records one rising-edge `hook-await-exceeded` degradation naming the hook, the await, the budget and the elapsed time; a session teardown records an informational `hook-await-abandoned`; and the user's own Escape or a cancelled turn records nothing at all, so a deliberate cancel never surfaces as an unhealthy environment. The per-hook wall budgets now live in one table (`clients/hook-budgets.ts`), and a new sweep fails on any `await` on a hook path — `index.ts`, `clients/runtime-*.ts`, `mcp/server.ts`, `clients/mcp/session.ts` — or any new hand-rolled timeout race anywhere in the shipped tree, unless it is wrapped or written down with a reason and an owning issue. No hook's behavior changes yet; the 187 sites the guard flags today are the recorded worklist.
|
|
56
|
+
|
|
57
|
+
- **Config resolution now proves it happened (closes #2526)** — one `config_resolved` latency phase per session and served project root records the redacted `documents[]` (`{tier, file, legacy}`), `countsByTier`, `recordCount`, `deniedServers`, `resolveMs` and the session identity, with a matching `config resolved …` line in `sessionstart.log`. Written where the resolution already exists — `loadLSPConfig`, the one funnel every production resolution goes through — so nothing is resolved twice and the session-start hook path gains no await. The payload comes from `summarizeConfigResolution`, the same projection `pilens_effective_config` embeds, so the on-demand answer and the session record cannot drift. `loadLSPConfig` also publishes a `session_start config_resolution_pending session=<id> root=<normalized cwd>` mark, as the first thing it does, so the mark fires exactly when a resolution is genuinely attempted rather than predicted from a handler's own flags ahead of time — a session that never reaches `loadLSPConfig` (e.g. a second quick- or minimal-mode session in the same process) publishes no mark and is correctly excluded, not counted against. `npm run logs:smells` gains a `config-resolution` smell that JOINS the two marks on `(session, root)` — not session id alone, since a warm MCP process keeps one session id for its whole life but resolves config once per served root, so one root's row must not silently clear another root's deficit: a session-root pair counts as unresolved only when it has a pending mark and no record bearing that same session and root exists. A legacy config document that produced zero migration records is the smell's second shape.
|
|
58
|
+
|
|
59
|
+
- **`scripts/ci-verdict.mjs`: one REST read of Unit tests + Lint & type-check conclusions on an exact head SHA (closes #2539)** —
|
|
60
|
+
three failure modes in one day came from how humans and agents read CI: the tail of `gh pr checks`
|
|
61
|
+
hid a failed Unit tests behind a passing Lint (#2527 review round 2 merge
|
|
62
|
+
blocked), a fixer fabricated a CI quote from local numbers instead of
|
|
63
|
+
quoting a real run, and 12 agents polling `gh pr checks --watch` in one day
|
|
64
|
+
exhausted the 5000/hr GitHub API budget (a CodeQL 429 on master). Sibling to
|
|
65
|
+
`scripts/check-pr-body.mjs`: `node scripts/ci-verdict.mjs <pr-number|sha>
|
|
66
|
+
[--wait <seconds>]` resolves the head SHA (and, for a PR number, the
|
|
67
|
+
`mergeable` state via the same `gh pr view` call), does ONE
|
|
68
|
+
`gh api repos/<owner>/<repo>/commits/<sha>/check-runs?per_page=100`, prints a
|
|
69
|
+
fixed CHECK/STATUS/CONCLUSION/URL table for `Unit tests` and
|
|
70
|
+
`Lint & type-check` plus a `Merge state:` line printed unconditionally,
|
|
71
|
+
and exits `0` only when both concluded `success`,
|
|
72
|
+
`2` whenever the PR is confirmed merge-conflicted
|
|
73
|
+
(`mergeable === "CONFLICTING"` — a merge-conflicted PR can't build its
|
|
74
|
+
merge-ref, so the real gates are skipped, not failed, AGENTS.md recurring-
|
|
75
|
+
defect shape 11), independent of whether the required checks are present,
|
|
76
|
+
absent, or already showing a stale green from before the head turned
|
|
77
|
+
conflicting (round 3: the dominant real-world DIRTY shape, a live probe on
|
|
78
|
+
#2552), `1` on a completed non-success conclusion, `3` while
|
|
79
|
+
pending (which also covers an absent check that is NOT confirmed
|
|
80
|
+
merge-conflicted — CI simply hasn't registered yet, the common case on a
|
|
81
|
+
fresh push — and a check-runs response GitHub reports as truncated via its
|
|
82
|
+
own `total_count`, which still takes precedence over a confirmed conflict
|
|
83
|
+
since `mergeable` itself is never paginated but the check-runs page can be),
|
|
84
|
+
`64` on a usage error, `70` on a transport/unexpected
|
|
85
|
+
error (distinct sysexits-style codes so neither collides with a verdict
|
|
86
|
+
code). `--wait <seconds>` polls at a fixed, non-configurable >=30s
|
|
87
|
+
interval, clamped to a 20-minute hard cap regardless of the requested
|
|
88
|
+
value, for the orchestrator only; every `gh` call — including
|
|
89
|
+
`resolveRepository`/`resolveHeadSha`, not just the check-runs read — carries
|
|
90
|
+
an explicit timeout derived from the remaining `--wait` budget (or a flat
|
|
91
|
+
60s default), floored at `MIN_GH_TIMEOUT_MS` (5s) so a nearly-exhausted
|
|
92
|
+
budget can no longer starve a healthy call of the time it needs, nor let an
|
|
93
|
+
already-exhausted budget's last call fall through to the full 60s default.
|
|
94
|
+
No dependency beyond `gh` on PATH: the verdict logic (`computeVerdict`) is
|
|
95
|
+
a pure function over the check-runs JSON, exported and unit-tested against
|
|
96
|
+
mocked payloads covering every exit code including the mergeable-aware
|
|
97
|
+
absent-check cases and a fail-closed rerun-deduplication case (shared with
|
|
98
|
+
`scripts/lib/merge-train-warden.mjs` and `scripts/lib/merge-train-lane.mjs`
|
|
99
|
+
via the new `scripts/lib/ci-checks.mjs`, replacing two separately
|
|
100
|
+
maintained copies of the required-checks list and tie policy with one —
|
|
101
|
+
`merge-train-lane.mjs` already consumed `merge-train-warden.mjs`'s copy by
|
|
102
|
+
import rather than maintaining a third of its own);
|
|
103
|
+
the `gh` invocation is a thin, injectable shell around it. The fixer
|
|
104
|
+
(`.claude/agents/pi-lens-fixer.md` step 8 and the "CI: read once" bullet),
|
|
105
|
+
reviewer (`.claude/agents/pi-lens-reviewer.md` step 6 and "CI executed,
|
|
106
|
+
not merely absent"), and merge-train (`.claude/skills/merge-train/SKILL.md`
|
|
107
|
+
step 4) playbooks, plus `AGENTS.md`'s own CI-read guidance, now reference
|
|
108
|
+
this script instead of hand-written `gh api`/`gh pr checks` invocations.
|
|
109
|
+
|
|
110
|
+
- **Add a flake-shape ratchet over every `*.test.ts` under `tests/` (closes #2547)** —
|
|
111
|
+
three deflake PRs in two days (#2531 alone fixed three shared-slot races)
|
|
112
|
+
and nothing counted the contention surface those PRs kept fixing.
|
|
113
|
+
`tests/clients/flake-shape-ratchet.test.ts` now caps FOUR shapes per file,
|
|
114
|
+
content-keyed against `tests/support/flake-shape-baseline.json`: a real
|
|
115
|
+
child process (`child_process` import, `execFileSync`/`spawnSync`/
|
|
116
|
+
`execSync`, or a spawn whose argv mentions `vitest`), a DELTA of two clock
|
|
117
|
+
reads (`Date.now`/`performance.now`/`process.hrtime`) feeding a numeric
|
|
118
|
+
matcher, a raw `setTimeout`/`setInterval` wait outside
|
|
119
|
+
`vi.useFakeTimers()`, and (round 2) a `vi.waitFor(` call outside
|
|
120
|
+
`vi.useFakeTimers()` — the #1767 shape. The ratchet is two-sided: a new
|
|
121
|
+
file, a risen count, AND (round 2) a FALLEN count — a stale ceiling that
|
|
122
|
+
would otherwise silently re-admit regrowth up to the old pin — all fail it.
|
|
123
|
+
Admission needs a `// flake-shape: <detector> — <reason>` header and
|
|
124
|
+
membership in `vitest.config.ts`'s `wallClockBudgetInclude`. AGENTS.md's
|
|
125
|
+
"Test requirements" states the default (fake clocks and the interleaving
|
|
126
|
+
kit; a real spawn or wall-clock assertion is a stated boundary decision),
|
|
127
|
+
and the fixer/reviewer playbooks each carry a matching line.
|
|
128
|
+
|
|
129
|
+
- **Release-readiness QA pass: a witnessed feature × modality matrix against a real pi (refs #2606)** —
|
|
130
|
+
`docs/release-qa-baseline.md` enumerates eleven rows across five modalities
|
|
131
|
+
(published tarball, npm install, headless `pi --mode rpc`, the MCP stdio
|
|
132
|
+
server, and a `git:` install), each with the command or RPC a user path
|
|
133
|
+
actually takes, a concrete pass criterion, and the witness to capture.
|
|
134
|
+
`scripts/release-qa.mjs` runs that matrix against a real pi in a scratch
|
|
135
|
+
`HOME`, writes `release-qa-report.md` plus one witness file per row, and
|
|
136
|
+
reports every row as PASS, FAIL with its cause, UNTESTED with its reason, or
|
|
137
|
+
SKIPPED — with the `discovered / rows / untested` arithmetic printed rather
|
|
138
|
+
than claimed, an expired polling cap counted UNTESTED and never PASS, and no
|
|
139
|
+
ship verdict at all when pi cannot boot. The `release-qa` skill drives the
|
|
140
|
+
pass, and AGENTS.md now requires it before a release is cut.
|
|
141
|
+
|
|
142
|
+
- **Roll up declined concurrent session starts (refs #2130, closes #2249)** — `logConcurrentSessionBind` (#473) previously wrote one `concurrent_session_bind` record per declined bind with nothing to aggregate them. `index.ts`'s `session_shutdown` handler now also logs one bounded `concurrent_session_bind_rollup` row per primary session, counting declines by classification (`concurrent-secondary` / `secondary-root`), a no-op when none occurred. The counter lives behind `getProcessSingleton` with its own family version, not a module-scope `let` — pi evaluates the pi-lens module graph more than once per process, and a bare `let` would only ever see the binds routed through its own copy. It resets both at the primary's own `session_start` (so a crashed prior primary can't leak a stale tally) and after each emission.
|
|
143
|
+
|
|
144
|
+
- **install-smoke gains a PR-gating host-range lane and a nightly `@latest` advisory lane (closes #2613)** — `install-smoke.yml` previously only ever built/packed the current repo against whatever `@earendil-works/pi-coding-agent` devDependency version happened to be pinned in `package.json` (the #2590 tree shape reached CI only by accident, when #2588 bumped that pin). A new `host-range-smoke` job runs on every PR that touches `package.json`, the lockfile, or the install/build scripts, overriding that devDependency to two hosts before running the same tarball-install + `install-selftest` + `build:dist` sequence `smoke` runs: the pinned floor (`PI_HOST_FLOOR_VERSION`, the oldest host this repo promises) and the newest version the declared `peerDependencies` range admits, resolved at run time by `scripts/resolve-newest-in-range-host.mjs` and bounded by a repo-owned `PI_HOST_SUPPORTED_RANGE` ceiling so a wildcard peer range (`"*"`, this repo's current pi-coding-agent spec) can never make the lane silently equal `@latest`. Both cells are blocking, retry a transient registry error (`scripts/npm-retry.mjs`), and label a newest-in-range failure that also reproduces on the last completed master run as upstream drift rather than a PR regression. A separate nightly-only `host-latest-smoke` job installs the unconditional `@latest` dist-tag through the same steps, advisory (`continue-on-error`), and files, refreshes, or closes a single tracking issue via `scripts/notify-install-smoke-drift.mjs` (reusing `scripts/lib/drift-issue.mjs`'s title-matching finder, generalized as a second consumer).
|
|
145
|
+
|
|
146
|
+
### Changed
|
|
147
|
+
|
|
148
|
+
- Exercise the per-server touch verdict with two auxiliaries, so a sibling scanner's findings must survive a slow scanner's lapsed wait. (#1549)
|
|
149
|
+
|
|
150
|
+
- **Bound a multi-file review-graph rebuild's edge removal (refs #2074)** — `removeFileOwnedGraphData` now finds a changed file's owned edges through `edgesByFrom`/`edgesByTo`, batches adjacency-index removal once per touched bucket, and compacts the edge array once per batch. A multi-file rebuild previously scanned the whole edge array `changedFiles` times and rescanned high-fan-in buckets once per edge; it now keeps both costs proportional to touched buckets.
|
|
151
|
+
|
|
152
|
+
- **Registered-or-fail sweep for topology-derived caches (closes #2294)** — a conformance test enumerates every module that calls a workspace-topology probe seam (`getDirectoryMarkers`, `findNearestDirWithAnyBasename`, `findNearestProjectRoot`, and the rest of the canonical list) and asserts each either registers a downstream reset through `registerWorkspaceTopologyReset` or carries a documented freshness-key exemption. A future consumer that memoizes from a topology seam and forgets to register now fails the sweep instead of silently leveraging a push-only registry. No runtime behavior changes; the sweep is the compensating guard.
|
|
153
|
+
|
|
154
|
+
- Exercise workspace-diagnostics sweep attribution and error-path closure through a pull-advertising server on the real JSON-RPC wire. (#2332)
|
|
155
|
+
|
|
156
|
+
- **Exercise full session-start topology re-derivation through the live path (closes #2333)** — an integration test drives the first quick session and a primary replacement equivalent to `/new`, proving the language profile, startup-scan context, and tsconfig paths re-arm through `handleSessionStart`.
|
|
157
|
+
|
|
158
|
+
- **Govern lsp-spawn-heavy lane membership (refs #2344).** A registered-or-fail sweep now derives lsp-spawn-heavy candidacy from the real spawn seams and fails when a new real-LSP-spawn test lands in the default vitest project without a documented exemption. The dispatch LSP real-runner suite and the plain-`npm test` integration suite join the lane, and each detector marker has an independent synthetic proof.
|
|
159
|
+
|
|
160
|
+
- **A broken LSP config now warns once per problem, not once per load attempt (refs #2418)** — a malformed `lsp.json` used to produce a fresh warning on EVERY config load, with no latch at all. It is now deduplicated on the file and the reason instead, alongside the other two config loaders, so a file that starts failing for a NEW reason is reported again rather than being repeated on every load. The ignore is also recorded once per session in the degradation ledger under `config-ignored`, including in every session after the first, which previously recorded nothing.
|
|
161
|
+
|
|
162
|
+
- **Cross-file staleness now works for the languages the registry reconnected (refs #2424)** — the turn-boundary freshness sweep for cached inline blockers (`clients/blocker-freshness.ts`) resolves a blocker file's forward imports through the same extension-to-grammar column, so it had always returned an empty import list for `.java`, `.kt`/`.kts`, `.swift`, `.dart`, `.lua`, `.zig`, `.ml`/`.mli`, `.ex`/`.exs` and `.sh`/`.bash` even though their grammars, import queries and path resolvers all shipped. Only the blocker file's own drift could demote such a verdict; now a blocker on `Main.java` is also demoted when the `Helper.java` it imports changes. The project scan gained the same reach, but a grammar is not a consumer: a file whose language has no `rules/tree-sitter-queries/<lang>/` dir, no fact-rule eligibility and no ast-grep binding is skipped BEFORE it is read, so a Lua or Swift file no longer costs a read and no longer inflates the scan's `fileCount` telemetry.
|
|
163
|
+
|
|
164
|
+
- **One canonical language registry behind LSP, tree-sitter, dispatch, read expansion and the review graph (closes #2424)** — `clients/language-registry.ts` is now the single answer to "what language is this file": stable id, extensions, exact filenames, the coarse `FileKind` it maps onto, the LSP `languageId` the protocol spells differently (`typescriptreact`, `shellscript`, `jsonc`) and the tree-sitter grammar name the wasm uses (`tsx`, `bash`). `lsp/language.ts`, `tree-sitter-shared.ts`, `project-diagnostics/scanner.ts`, `read-expansion.ts`, `review-graph/builder.ts` and `module-report.ts` are projections of it; the four hand-copied tables (read expansion's private ext map, the review-graph switch, module-report's kind map, the scanner's spread) are deleted, and cross-subsystem drift guards plus a committed golden snapshot of every consumer's answer for every known extension now fail CI on a hand-edit. Reconciling the tables changed no existing answer — every one of the 26 reconciled rows is a table that had no answer gaining the registry's: extension to tree-sitter grammar (per-edit runner + project scan) gains `.sh`/`.bash` to bash, `.java` to java, `.kt`/`.kts` to kotlin, `.dart` to dart, `.ex`/`.exs` to elixir, `.lua` to lua, `.ml`/`.mli` to ocaml, `.swift` to swift and `.zig` to zig; read expansion gains `.inl`/`.ipp`/`.tpp`/`.txx`/`.cu`/`.hip` to cpp, `.php3`/`.php4`/`.php5` to php and `.css` to css; the LSP seam announces `.phtml`/`.php3`/`.php4`/`.php5` as `php` and `.tpl` as `helm` instead of falling back to `plaintext` on `didOpen`; and the review graph plus the module-report outline now extract `.cue` symbols, whose grammar and symbol queries already shipped.
|
|
165
|
+
|
|
166
|
+
- **`symbol_search`'s `lang` filter answers from the registry (refs #2424)** — it was a ninth hand-written language-to-extensions table in `clients/lens-engine.ts` and had drifted from every other one. Reconciled: `css` no longer claims `.scss`/`.less` and `json` no longer claims `.jsonc` (those are their own languages, parse under no css/json grammar, and have no symbol queries, so no real hit is hidden); `php` gains `.phtml`/`.php3`/`.php4`/`.php5`, `ruby` gains `.ru`, `bash` gains `.zsh` and `cpp` gains the rest of the C++ extension family; `solidity` is dropped — pi-lens ships no solidity grammar and no ast-grep binding for it, so a `.sol` file could never carry an indexed symbol. A `lang` value may now be spelled as either the tree-sitter grammar name or the canonical language id.
|
|
167
|
+
|
|
168
|
+
- **One canonical config file, one documented lookup order (refs #2426)** — pi-lens config now has exactly two canonical locations: `.pi-lens.json` for a project and `~/.pi-lens/config.json` for the machine, with everything LSP-related under an `lsp` namespace inside both. The three loaders that each had their own discovery walk and their own merge semantics (`loadLSPConfig`, `loadPiLensGlobalConfig`, `loadPiLensProjectConfig`) now resolve through one path over one schema, and keep their existing names and return types. `docs/configuration.md` documents the order once — global, then project root, then nested project configs — and a test walks it; the `builtin`, `env`, `cli` and `host` tiers around them are reserved for #2427/#2416 and the doc now says so, alongside the precedence environment variables and CLI flags actually have today. Four behavior changes come with the consolidation, all deliberate: (1) **the config search stops at `$HOME`** instead of running to the filesystem root, so a stray `pi-lsp.json` in your home directory (or at `C:\`) is no longer adopted by every project on the machine; (2) **the canonical file wins** — a leftover `.pi-lens/lsp.json` used to beat the `.pi-lens.json` beside it, which made the migration impossible to finish; (3) **`.pi-lens.json`'s `lsp` section is actually read** — previously `loadLSPConfig` returned the file's whole top level as if it were LSP config and ignored an `lsp` section entirely, while project-only keys like `ignore` and `maxProjectFiles` leaked into it; (4) **nested project configs layer field-wise** (nearest wins per field, the rule `ignore` has had since #783) instead of the nearest config replacing the outer one wholesale, and `servers`/`serverOverrides` merge per server id across every source rather than one file's map replacing another's. A wrongly-typed `disabledServers` or `warmFiles` is now dropped with a warning instead of being iterated as whatever it happens to be. A project `.pi-lens.json` may carry `lsp.servers`, `lsp.serverOverrides`, `lsp.disabledServers` and `lsp.warmFiles`; the session-wide switches under the same namespace (`lsp.enabled`, the `--no-lsp` toggle) stay global-only and still say so when written in a project file rather than being ignored quietly.
|
|
169
|
+
|
|
170
|
+
- **A globally disabled language server can no longer be re-enabled by a repository (refs #2427)** — `lsp.disabledServers` is now resolved as the UNION of every configuration tier instead of nearest-file-wins, and the union spans both spellings of the key, so a `~/.pi-lens/config.json` that disables `typos` keeps it disabled even when a checked-in `.pi-lens.json` says `lsp.disabledServers: []`, and even when the two files spell the setting differently (the deprecated root `disabledServers` vs. the canonical `lsp.disabledServers`). There is no vocabulary for un-denying an entry, so a nearer file that omits one is expressing nothing rather than an allow. If you relied on a repository config clearing your global disable list, remove the entry from the global file instead. `pilens_effective_config` reports which tier denied each server.
|
|
171
|
+
|
|
172
|
+
- **One shared registration helper for the read and mutation bridges (refs #2437, refs #2423)** — `clients/read-bridge.ts` and `clients/mutation-bridge.ts` each hand-rolled an identical mount body (a first-wins existence check, `Object.freeze`, and a non-writable/non-configurable `Object.defineProperty` at a `globalThis` `Symbol.for` key). New leaf module `clients/process-bridge.ts` (`registerProcessBridge`/`getProcessBridge`) owns that body once; both bridges now delegate and are thin declarations of their own method table and version check. No behavior change — both bridges' existing test suites pass unchanged.
|
|
173
|
+
|
|
174
|
+
- **One process-table seam replaces five hand-rolled snapshotters (closes #2443)** — the Windows `Get-CimInstance Win32_Process` and POSIX `ps` listings used by the orphan reaper, the resource sampler, the worktree-hygiene hook and the compat smoke are now composed and parsed in a single place, `scripts/lib/process-scan.mjs`, with a `fields` projection covering pid, parent pid, age, RSS, kernel/user CPU time, start time and command line, plus platform-side filtering that reports whether it was actually applied. `clients/process-snapshot.ts` is the one crossing point into the extension runtime and keeps its own spawn rails (unref'd child and stdout, an injected tree-kill-and-verify timeout handler, and a status that keeps an empty table distinguishable from a query that never ran). A conformance sweep fails the build if a second file ever spells one of these queries again, so hardening like PR #2438's non-zero-exit check can no longer reach one copy and miss the rest.
|
|
175
|
+
|
|
176
|
+
- **`BoundedSet` sibling in `clients/bounded-cache.ts`; four hand-rolled membership-Set caps migrated (refs #2460)** — `BoundedFifoMap`/`BoundedLruCache` (#2442) cover the K,V shape only, so the process-lifetime membership-only Set caps that #2442 could not migrate stayed hand-rolled and exempted in `tests/config/bounded-eviction-idiom-sweep.test.ts`. `BoundedSet<T>` adds the same insert-order eviction contract (`add()`/`setMaxEntries()` return the evicted values, oldest first) with no dummy-value tax. Migrated: `clients/lsp/session-roots.ts` (`registerSessionRoot`), `index.ts` (`ensureLSPConfigInitialized`), `clients/lsp-mutation.ts` (`bookkeepLspMutation`'s per-batch autofix dedupe), and `clients/observed-mutation.ts` (`noteMutationHandled`'s `handled` set) — all FIFO, matching each site's prior semantics, so no runtime behavior changes.
|
|
177
|
+
|
|
178
|
+
- **Analyzer bootstrap clients load on demand, not at session start (closes #2467)** — the seventeen shell-out analysis clients are no longer built before `handleSessionStart` runs; extension activation and quick-mode session start now complete without loading the graph at all, and the load is paid by the first consumer that proves it needs it. All demands share one retryable in-flight promise, a transient failure is retried by the next demand and fails open for the caller (counted under the new `analyzer-bootstrap-unavailable` degradation kind), the wait is bounded by both a wall-clock ceiling and the caller's abort signal, and primary `session_shutdown` refuses new loads without invalidating a demand already in flight. An irrelevant `tool_call` — a bash command, a vendored file, a file already baselined, or a read of any file type complexity cannot analyze (Markdown, JSON, YAML, CSS, Java, shell) — no longer loads or awaits the graph. A load that fails three times in a row stops being rebuilt for the rest of the session, recorded once under the new `analyzer-bootstrap-latched` degradation kind and re-armed at the next `session_start`, so an environment that cannot resolve an analyzer module no longer pays a full rebuild on every demand.
|
|
179
|
+
|
|
180
|
+
- **Every "deadline AND abort signal" bound now goes through one primitive (refs #2523)** — pi-lens had five private implementations of the same idea, each with its own reasons, its own cleanup and its own silence when it fired. Four are gone: the analyzer-bootstrap wait, the observational-mutation capture, the pre-dispatch LSP resync and the actionable-warnings LSP round trip all run through `bounded()` now, which means every one of them records a `hook-await-exceeded` degradation naming the hook, the await and the budget when it abandons work — visible in `pilens_health` where before it was invisible. A cancelled turn still records nothing, because pressing Escape is not a degraded environment. The actionable-warnings loop also loses an optional abort leg whose presence had to agree with the signal beside it; that pairing was correct at both call sites, so no wait changes behaviour, but the invariant no longer has to be maintained by hand. Two attribution bugs are fixed along the way: the analyzer-bootstrap wait was writing its caller's reason (`session-start-scans`) onto the ledger's hook axis, where nothing could join it against a hook budget, and the actionable-warnings round trip charged its off-hook deferred loop to `turn_end` — so whichever loop blew its budget first silenced the other. `safeSpawnAsync` deliberately keeps its own bound and now says why in as many words: its work is a child process, and a bound that can KILL what it abandons must kill it rather than leave an orphan behind. The governance sweep that forbids new hand-rolled races now derives the helper modules hook work reaches through from the import graph rather than a hand-written list, pins each one's unbounded-await count, and registers every `bounded()` call site with the provenance of its signal. A third bug found in review is fixed too: the actionable-warnings round trip clamped its own per-call timer to whatever was LEFT of the batch's wall budget, so a perfectly healthy pull that simply started late in a big batch could be recorded as exceeding a budget ("65ms") that exists in no configuration — a false `⚠` in `pilens_health` whose rising-edge recording could then silence a genuinely wedged server later in the same session. The loop's own residual now bounds the wait without ever reaching the ledger; only a pull that outlives its own configured per-trip timeout is recorded. The governance sweep's helper walk also no longer counts a module reached from a hook handler ONLY through an `import type` — a type edge is not a call, and nine such modules (`clients/lsp/client.ts` alone accounting for 75 of the 124 awaits removed) were inflating the pinned worklist on every PR that touched them.
|
|
181
|
+
|
|
182
|
+
- **`escapeRegExp` folded onto one runtime export (refs #2558)** — the regex-escaping helper (and its near-namesakes `escapeRegExpChar`/`escapeRegExpLiteral`/a renamed `escapeRegex`) was hand-copied into eight production modules and four test helpers with a byte-identical body. `clients/string-utils.ts` now owns the one runtime copy and `tests/support/sweep-kit.ts` re-exports it for tests; every former copy imports from one of those instead. A new `tests/config/escape-regexp-fold-sweep.test.ts` guard flags a future re-copy by matching the escaping BODY (not the function's name), so a renamed copy is caught the same way the original `escapeRegex` rename in `scripts/lib/astgrep-self-scan.mjs` was. `clients/file-utils.ts`'s `globToRegExp` keeps its own narrower character class (it omits glob wildcards `*`/`?`, which its caller handles separately) as a documented variant, not a copy.
|
|
183
|
+
|
|
184
|
+
- **One dialect-parameterized workspace-member glob matcher (closes #2591)** — `matchesWorkspaceMemberPattern(pattern, relativePath, dialect)` in `clients/path-utils.ts` replaces the two hand-rolled member matchers: cargo's private per-segment regex compiler in `clients/lsp/server.ts` and uv's private minimatch options block in `clients/python-environment.ts`, both now thin callers passing a dialect constant. The dialects diverge on `**` (a cargo pattern containing one still never matches, byte-for-byte unchanged, so Rust-LSP workspace hoisting is untouched), on whether `*`/`?` may cross `/`, on character classes, and on pattern normalization. uv's `exclude` list, pinned to `astral-sh/uv@3c979abda4530fe9bf3d92e9bcf5c5575e3b3126`, matches with `require_literal_separator: false`, so a `*` in an exclusion now crosses `/` (`exclude = ['packages/a*c']` excludes `packages/a/b/c`) instead of being documented as an unimplementable limitation — an excluded nested project falls back to its own `.venv`. npm/pnpm's `expandWorkspacePattern` stays separate by decision: it expands the filesystem rather than matching a candidate path.
|
|
185
|
+
|
|
186
|
+
- Migrated the last 18 hand-rolled `LSPService` test doubles (17 files) onto
|
|
187
|
+
`makeLspServiceDouble`, emptying `tests/support/lsp-double-baseline.json` so
|
|
188
|
+
the #2582 sweep is a plain gate that reds on any new hand-rolled double, and
|
|
189
|
+
removed the `typeof lspService.isSpawnInFlight === "function"` hedge in
|
|
190
|
+
`clients/pipeline.ts` that existed only for that population.
|
|
191
|
+
|
|
192
|
+
- **Test names state behavior, not hope (closes #2602)** — the 58 `it("should …")` / `test("should …")` names under `tests/` (including mid-string, not just leading, `should`) are renamed to declarative present-tense behavior statements ("does X if Y") per AGENTS.md test-authoring screen 11, and one exact duplicate test case is deleted. String-only rename elsewhere: no assertion, fixture, or ordering changes — except seven inert fixture-wrapper strings in `tree-sitter-new-blocker-rules.test.ts`, which are TypeScript source content fed to a tree-sitter query that never inspects them.
|
|
193
|
+
|
|
194
|
+
- **One fixture-workspace bootstrap for the five LSP dev-harness scripts, home-pinned (closes #2670, closes #2658)** — `scripts/lib/lsp-fixture-workspace.mjs`'s `bootstrapFixtureWorkspace` replaces the hand-copied "copy fixture → register session root → optional disable+reload → optional git init → assert registered" preamble in `smoke-tools.mjs`, `characterize-lsp.mjs`, `probe-clean-signal.mjs`, `server-capabilities.mjs`, and `bench-lsp.mjs` (the last one previously skipped the unconditional register entirely). `withScratchHome()` pins `PI_LENS_HOME`/`PILENS_DATA_DIR` to a scratch temp dir for every one of the five scripts, so their now-unconditional `initLSPConfig` calls no longer write `config_resolved` telemetry into a developer's real `~/.pi-lens`.
|
|
195
|
+
|
|
196
|
+
- **CI production-dependency audit is bounded and retried** — `scripts/audit-prod-deps.mjs` wraps `npm audit --omit=dev --audit-level=high` with a per-attempt timeout and backoff; a real high/critical finding still fails the job, while a registry outage (2026-09-04: five-minute hangs then 400 from npm's retired quick audit endpoint) passes with a visible warning instead of redding every PR on an unchanged lockfile.
|
|
197
|
+
|
|
198
|
+
- **`tools/lsp-diagnostics.ts`'s directory-scan language table is now a registry projection (refs #2434)** — the last table `clients/language-registry.ts` (#2424) deliberately left out, because its ORDERED iteration decides which language a mixed directory scans as. `LANG_EXTENSIONS` is gone; `runDirectoryDiagnostics` now walks the new `SCAN_LANGUAGE_PRIORITY: readonly (readonly LanguageId[])[]` — one entry per FAMILY (grouping every registry id the old table's key had split by grammar, e.g. `typescript`+`typescriptreact`), same order as the old table's keys — and unions each family's `extensionsForLanguage` sets into ONE `collectFiles` pass, drift-tested against a golden capture of the old table (`tests/fixtures/lsp-diagnostics-lang-extensions.json`) plus an exhaustive 1-and-2-extension regression sweep (`tests/tools/lsp-diagnostics-scan-family.test.ts`). Also deleted the dead `"solidity"` entry from `clients/lsp/server.ts`'s `AST_GREP_KINDS` and `tools/shared.ts`'s `LANGUAGES` (neither named a language the registry, any grammar, or the ast-grep napi matrix knows — `LANGUAGES` spreads into the `lang` enum of four agent-facing tool schemas: `ast_dump`, `ast_grep_outline`, `ast_grep_replace`, `ast_grep_search`).
|
|
199
|
+
- Every extension the old table could scan is still reachable, and a directory mixing BOTH extensions of a registry-split pair (`.ts`+`.tsx`, `.js`+`.jsx`, `.css`+`.scss`/`.less`, `.json`+`.jsonc`) scans exactly as before — the family grouping is what keeps that true, not a documented exception.
|
|
200
|
+
- Four rows genuinely widen, because the shared registry entry now backing them is used by other consumers too (grammar routing, LSP id) and narrowing it here would refork a table #2424 just merged: `ruby` now also matches `.ru` (Rack config), `php` also matches `.phtml`/`.php3`/`.php4`/`.php5`, `cpp` also matches its full clang alias-extension set (module-interface/Objective-C/OpenCL tail), and `json` also matches `.json5`. A directory containing ONLY one of these newly-included extensions now scans as that language where it previously found "no supported source files".
|
|
201
|
+
- Reserved, not implemented: the scan priority may become a config key under the #2416 lsp namespace; no loader reads `SCAN_LANGUAGE_PRIORITY` today.
|
|
202
|
+
|
|
203
|
+
- **Config parse errors keep `line L col C` locality for every V8 `SyntaxError` shape, derived from the source text instead of the message (refs #2451)** —
|
|
204
|
+
#2431's redaction fix stripped a `JSON.parse` `SyntaxError#message` down to
|
|
205
|
+
the error's own class, plus a position when V8's message stated one
|
|
206
|
+
(`at position N (line L column C)`). V8's OTHER shape — `Unexpected token
|
|
207
|
+
'x', "<snippet>"... is not valid JSON`, the exact shape #2431's own
|
|
208
|
+
evidence hit (a `ghp_`-prefixed token) — states no position at all, so a
|
|
209
|
+
user hand-editing `.pi-lens.json` got strictly LESS locality than before
|
|
210
|
+
#2431: a bare `SyntaxError`, no line, no column. `clients/config-warn.ts`'s
|
|
211
|
+
`normalizeParseErrorReason` now accepts the raw source text the loaders
|
|
212
|
+
already read and locates the error IN IT: V8's own quoted snippet is found
|
|
213
|
+
in the source with `indexOf` (a match that is not unique is not trusted),
|
|
214
|
+
and the offending token's own offset is used when it too is unique inside
|
|
215
|
+
the snippet — an EXACT position, not an approximation, for the common case
|
|
216
|
+
(JSON punctuation, a typo'd letter). Line/col are then computed by
|
|
217
|
+
scanning the source ourselves — for BOTH shapes, so even the
|
|
218
|
+
position-stated one is no longer trusting V8's own digits, only ours.
|
|
219
|
+
Only digits ever escape into the reason string; the snippet and the token
|
|
220
|
+
text never do. `clients/config-resolve.ts`'s `readConfigDocument` now
|
|
221
|
+
carries the text it read alongside a `JSON.parse` failure
|
|
222
|
+
(`ConfigReadOutcome`/`ConfigReadFailure`'s new optional `sourceText`,
|
|
223
|
+
unset for an `fs` read failure — nothing was ever read then), threaded
|
|
224
|
+
through `reportConfigReadFailure` and `ignoredRecordCollector`'s
|
|
225
|
+
`NoteIgnored` to every one of the three loaders (`clients/lsp/config.ts`,
|
|
226
|
+
`clients/lens-config.ts`, `clients/project-lens-config.ts`).
|
|
227
|
+
|
|
228
|
+
Also folds three near-identical inline copies of
|
|
229
|
+
`error instanceof Error ? error.name : "unknown error"`
|
|
230
|
+
(`clients/config-core/normalize.ts`, `clients/config-core/resolve.ts`, and
|
|
231
|
+
a third `clients/lens-config.ts` grew independently since #2431) onto a
|
|
232
|
+
new zero-dependency leaf, `clients/error-class.ts`'s `errorClassName` —
|
|
233
|
+
NOT `normalizeParseErrorReason` itself, which the issue's evidence
|
|
234
|
+
proposed: `config-core/` must never import a sink
|
|
235
|
+
(`clients/config-warn.ts` -> the degradation ledger), which is exactly the
|
|
236
|
+
import-cycle removal #2426 shipped for that module, and importing
|
|
237
|
+
`normalizeParseErrorReason` back in would reopen it. `normalizeParseErrorReason`
|
|
238
|
+
gained a `classOnly` option that delegates to the same leaf, so every
|
|
239
|
+
caller of "class name, never the message" — including `config-warn.ts`'s
|
|
240
|
+
own — now shares one implementation.
|
|
241
|
+
|
|
242
|
+
- **`tests/config/bounded-eviction-idiom-sweep.test.ts`'s exemptions are keyed by content, not line number (closes #2475)** — the sweep's `EXEMPT_SITES` used to key each occurrence `path:line`, which re-keys on every unrelated line a later PR inserts ABOVE a flagged site: #2459, #2449, and #2474 each had to re-pin a line number that had nothing to do with their own change, and a textual merge of two such re-pins can silently land a WRONG number with no conflict marker. New `stableOccurrenceKey` (`tests/support/sweep-kit.ts`) keys each occurrence on its enclosing top-level declaration's name (`findEnclosingSymbol`, a cheap line-scan heuristic, not a parser) plus a short content hash of the flagged line itself (`lineContentHash`, reused from `clients/read-guard.ts`'s line-move relocation) — `path#symbol:hash`, or `path#hash` with no enclosing declaration. A mutation test proves the key survives a line inserted elsewhere in the file and changes when the flagged line's own text (or its enclosing declaration's name) changes, so a deleted or rewritten exemption still fails loud via `auditRegistry`'s existing stale-exemption/unaccounted-item checks — no new failure-reporting machinery. All 5 existing `EXEMPT_SITES` entries were migrated to the new keys.
|
|
243
|
+
- `tests/support/sweep-kit.ts` gained `findEnclosingSymbol`/`stableOccurrenceKey`, additive only — `session-state-scan.ts` (mid-round on PR #2470) imports `stripSource`/`listSourceFiles`/`relativePosix` from the same module and is unaffected.
|
|
244
|
+
- Class sweep of `tests/config/*.test.ts` for the same `path:line` exemption shape found one other hit: `tests/config/sync-child-process-timeout.test.ts`'s `EXEMPT_SITES` is already keyed `path:snippet` (a content substring), so it does not share this defect and was left as-is.
|
|
245
|
+
- Round 2 (review F1): `stableOccurrenceKey`'s content hash reintroduced the laundering one layer down — in a class-shaped file every method's flagged line resolves to the SAME enclosing symbol (the class name), so two sibling methods that each flag a byte-identical line hash to one identical key, and one exemption silently excused both (reviewer-probed live, not hypothetical). `auditRegistry` gained `requireUniqueFlagged` (on by default), which fails loud on a duplicate flagged key, plus a richer `FlaggedEntry` shape (`string | {key, detail}`) so the collision message names each colliding occurrence by its own `file:line` detail. Running it against the real sweeps surfaced one GENUINE collision already shipped: `clients/safe-spawn.ts` has two internal `spawnSync` calls that both spread `...(options as SpawnOptions)`, so `sync-child-process-timeout.test.ts`'s substring-matched exemption key already named both identically. Round 2 shipped `disambiguateFlaggedKeys`, numbering each occurrence sharing a key by SCAN POSITION (`key`, `key#2`, ...) — Round 3 found this unsound (below) and removed it.
|
|
246
|
+
- Round 3 (review F1): `disambiguateFlaggedKeys`' scan-position ordinals were **not** a safe substitute for occurrence identity, and the Round 2 claim that "a THIRD, unreviewed occurrence sharing the same base key lands on an unexempted ordinal and is caught" was false — proven false, not merely suspected. Two probes against the shipped mechanism: inserting a new unbounded `spawnSync` call between the two already-exempted `safe-spawn.ts` sites either (a) reds on a bare ordinal key with no file:line, so the natural fix excuses the WRONG (already-reviewed) site while the new call rides the other ordinal, or (b) — adding `timeout: 5000` to the first original occurrence so the unbounded COUNT happens to stay at 2 — passes fully silently, with the brand-new, never-reviewed call riding an exemption reasoned about a different, now-safe call. `disambiguateFlaggedKeys` is deleted outright. The real fix is in the KEY GENERATOR: `sync-child-process-timeout.test.ts`'s `exemptionKey` now matches each call's own ARGUMENTS ONLY (never a preceding-source preamble, which is what let two textually distinct calls collide on one substring) against a snippet unique to that call — `"/F", "/T", "/PID"` for the taskkill call, `spawnCmd, spawnArgs,` for the Windows-resolved-path fallback, `// Explicit override, not just the spread above` for the plain-command fallback — so two genuinely distinct call sites now derive genuinely distinct keys with no ordinal needed, and `requireUniqueFlagged` remains the backstop for any future generator that still collides. `bounded-eviction-idiom-sweep.test.ts`'s `ATTACK_TWIN_OCCURRENCE_COLLISION` test, which had demonstrated the ordinal "remedy", now demonstrates the actual one: a human distinguishes two colliding occurrences by editing the flagged line's own text (changing its content hash), not by an auto-assigned position. `auditRegistry`'s `unaccounted` problem message now prints each item's `detail` (a real file:line) when the caller supplied one, so a collision or an unaccounted item names the actual call site instead of a bare key.
|
|
247
|
+
- Round 4 (review F1/F2/S1): `sync-child-process-timeout.test.ts`'s `callArguments` depth-scanned RAW source to slice each call's argument list, so a literal `(` inside a string or comment ARGUMENT (e.g. a shell command quoting a paren) unbalanced the count, ran the "argument list" past the real closing paren, and could absorb an unrelated later call's `timeout:` — silently reclassifying a genuinely unbounded call as bounded (probed live against `clients/safe-spawn.ts`'s real callers, not hypothetical). Fixed by depth-scanning the already-computed `masked` (comment/string-blanked, offset-preserving) source and slicing the result from `raw`, so a paren inside a string/comment cannot perturb the count while an in-argument comment stays visible to `exemptionKey`. Also: `auditRegistry`'s `staleExemptions` message dropped its `describe()` call — a stale exemption's key is by construction absent from `flaggedEntries`, so it can never carry a detail, and the call was dead code that always fell through to the bare key; `sweep-kit.test.ts` gained a case pinning `describe`'s real (`unaccounted`-only) behavior, since the review found the whole file stayed green with `describe` regressed to identity. `sync-child-process-timeout.test.ts`'s exemption reasons now name the caller functions (`isCommandAvailable`, `findCommand`, `detectRunner`) instead of citing line numbers, consistent with this PR's own point that line keys go stale.
|
|
248
|
+
|
|
249
|
+
- **`effective_config`/`pilens_effective_config` reject a `file` outside `cwd` instead of silently answering from a foreign tree (closes #2520)** —
|
|
250
|
+
`clients/effective-config.ts`'s per-file half resolved from the FILE's own
|
|
251
|
+
directory whenever `file` was passed, on the documented assumption that
|
|
252
|
+
this root is always a superset of the workspace's — true only when `file`
|
|
253
|
+
actually lies under `cwd`. A `file` in a sibling tree broke that
|
|
254
|
+
assumption silently: the answer resolved a foreign `.pi-lens.json`,
|
|
255
|
+
labelled it tier `project`, omitted the workspace's own document from
|
|
256
|
+
`documents`, and `view.cwd` still named the workspace — a foreign config
|
|
257
|
+
reported as the workspace's own. `effectiveConfig` now confines `file` to
|
|
258
|
+
`cwd` via the shared `isSameOrWithin` containment comparator (the same one
|
|
259
|
+
the LSP session-root registry gates enrollment with): a `file` that does
|
|
260
|
+
not resolve inside `cwd` — including a sibling PACKAGE in the same
|
|
261
|
+
monorepo, not only an unrelated tree — returns `file: { error }` naming
|
|
262
|
+
the `cwd` it was measured against and the remedy (re-query with `cwd` set
|
|
263
|
+
to that file's own workspace) instead of a resolved view, and the
|
|
264
|
+
whole-config half still resolves at `cwd` rather than touching the foreign
|
|
265
|
+
tree at all. A file nested INSIDE `cwd` (the common case —
|
|
266
|
+
`ensureLSPConfigInitialized(path.dirname(filePath))`'s own layering) is
|
|
267
|
+
unaffected. This confinement is a deliberate trade for this PR (issue
|
|
268
|
+
#2520 left the choice open between confining and documenting the prior
|
|
269
|
+
behavior) — `docs/configuration.md` states it plainly, including the
|
|
270
|
+
monorepo-sibling case, so the trade is visible rather than assumed.
|
|
271
|
+
`EffectiveConfigView.file` is now `EffectiveFileView | EffectiveFileViewError`;
|
|
272
|
+
a new type guard `isEffectiveFileViewError` distinguishes the two. Both
|
|
273
|
+
`tools/effective-config.ts` (the `effective_config` pi tool) and
|
|
274
|
+
`mcp/server.ts` (`pilens_effective_config`) render the rejection instead of
|
|
275
|
+
crashing on the narrowed type. Also collapsed the internal `FileQuery`
|
|
276
|
+
struct: its `absolute` field duplicated the already-hoisted `absolute`
|
|
277
|
+
local, so the per-file LSP config is now derived inline at the one call
|
|
278
|
+
site that needs it, and the stale "SUPERSET of the workspace's" doc comment
|
|
279
|
+
now states the invariant as CONFINED (enforced), not assumed.
|
|
280
|
+
New tests in `tests/clients/effective-config.test.ts` drive the real
|
|
281
|
+
production path (real files on disk, real `resolvePiLensConfig`) for both
|
|
282
|
+
the rejection and the still-working nested-file case.
|
|
283
|
+
|
|
284
|
+
- **Turn-end test runner: integration/e2e targets excluded, and an unfinished batch is deferred instead of reported clean (closes #2522)** — Selection now filters every resolved test target (failed-first/related/self) through a built-in, case-insensitive exclusion list (`**/integration/**`, `**/e2e/**`, `**/*.integration.*`, `**/*.e2e.*`) before it can be auto-fired, and fails closed for any target resolving outside the project root, logging which targets were skipped and why. The batch-wide wall budget (`TEST_RUNNER_BATCH_BUDGET_MS`) is reconciled from #2509's 90s down to 20s — well under the turn and distinct from the 60s per-target timeout — and the batch now genuinely defers what it cuts: it owns an abort signal threaded into each spawn, so hitting the bound kills the in-flight runners, and the killed plus never-dispatched targets are persisted by identity and dispatched first on the next turn. A batch that did not finish is never published as a clean run and never clears the `--lens-guard` test-failure blocker; it is delivered as advisory, naming the targets deferred. A target whose own runtime exceeds the whole batch budget is retired after `TEST_RUNNER_MAX_DEFERRALS` cut batches, with a counted degradation naming it, rather than being re-run and cut every turn forever; the retirement is persisted for the rest of the session and honoured by every selection strategy, so a slow suite reachable through `related`/`self` cannot be re-selected the moment its source is edited again, and both the deferral and retirement lists carry a session stamp so one session neither re-fires nor inherits another's cut list. A session ignores the other's entries at selection time and never deletes them — pi and the MCP Stop-hook fire turn-end against the same project under different identities, so a turn on one route used to wipe what the other had recorded and put the slow-suite livelock straight back — and both lists are bounded so they cannot grow without limit. Every write to the deferral list merges against the live record, so a batch publishing a clean result no longer flattens the cut set a concurrent, superseded batch just handed over; a carried target the per-turn cap has no room for is held with its attempt count untouched rather than dropped. Delivering the advisory retires only the advisory: both lists survive the in-process consume and the MCP Stop-hook acknowledgement. Whether a target was cut or simply finished as the bound fired is decided when its run settles, so a target that completed in time keeps its result and is not charged an attempt, while one whose spawn was aborted before it ever started is deferred rather than recorded as a completed run. The "Test failures detected last turn — fix before continuing" context message is likewise no longer emitted for a batch made entirely of runner errors (timeouts, missing provider/binary).
|
|
285
|
+
|
|
286
|
+
- **Consolidate LSP service test doubles (refs #2582)** — Pipeline and runtime-session fixtures now share one `LSPService` double, and the post-autofix LSP resync starts the primary `touchFile` before kicking off auxiliary server warmup so a failure in the best-effort auxiliary path can never abandon the primary sync or its `lsp_sync_abandoned` record.
|
|
287
|
+
|
|
288
|
+
- **LSP**: `lsp_diagnostics`, `lsp_navigation` and the tsserver sync escape hatch no longer probe the LSP service for methods it always defines. `collectDiagnosticsForFile` and `openFileBestEffort` touched the file only `if (typeof lspService.touchFile === "function")`, and `attemptTsserverSyncDiagnostics` bailed unless `getAdvertisedCommands` was a function; the real `LSPService` defines both unconditionally, so those hedges (and the `openFile` fallbacks behind them) were reachable only from a partial test double. Production behaviour is unchanged — the touch and the advertisement read now happen on the one path a real service ever took, and the `getDiagnostics` fallback still covers a touch that resolves no clients.
|
|
289
|
+
|
|
290
|
+
- **Pinned that `git init` is never worktree-mutating, and that an ephemeral test fixture is never a shared checkout (refs #2345)** — #2345 reported the shared-checkout guard (`--lens-checkout-guard`) rejecting `git init -q` for real-Git fixture repos under `os.tmpdir()`. Reproducing from the production call path (a full `npm test` run of the named suites, a direct `evaluateSharedCheckoutGuard` call, and a trace of every git-verb matcher in `clients/git-guard.ts` and `clients/shared-checkout-guard.ts`) found no code path that does this: `init` was never a member of `ALWAYS_MUTATING_VERBS` and has no special case beneath it, so `matchesVerb` already answers `false` for it — `git init` only creates or refreshes `.git` metadata and never rewrites a tracked file, so it was never in the class this guard exists to stop. Separately, `evaluateSharedCheckoutGuard` only declines when a LIVE PEER is registered whose resolved toplevel matches the target's exactly; nothing ever calls `registerInstance()` against an `fs.mkdtempSync` fixture directory, so a test-owned temp repo always resolves zero peers and is allowed unconditionally, regardless of location or dirtiness — no directory-based allowlist is needed for this to hold. Two regression tests pin both invariants directly (one classification test naming `init` for the first time, one end-to-end test against a real, dirty, freshly-`git init`-ed fixture through the unstubbed production registry), each proven red under a targeted mutation. No runtime behavior changed.
|
|
291
|
+
|
|
292
|
+
### Deprecated
|
|
293
|
+
|
|
294
|
+
- **Deprecation windows for the legacy LSP config surfaces (refs #2418)** — the stability policy in `docs/public-api-stability.md` now records, as test-checked data, which config surfaces are deprecated and when they may be removed. Deprecated since 4.1.4, removable no earlier than 5.0.0, and read unchanged until then: the legacy top-level LSP config keys `servers`, `serverOverrides`, `disabledServers`, and `warmFiles`; and the config file locations `.pi-lens/lsp.json`, `pi-lsp.json`, `pi-lens.json`, and `~/.pi-lens/lsp.json`. The canonical locations are unaffected — `.pi-lens.json` and `~/.pi-lens/config.json` stay canonical and are not deprecated; what is deprecated is the legacy keys read from them. Config warnings now end in a stable code — `[PILENS_CFG_0001]` for a config file that could not be read or parsed and was therefore ignored — so they can be matched or suppressed without depending on the message prose, and the same ignore is now counted in the degradation ledger under `config-ignored` instead of vanishing after one notification. No config is read or resolved differently in this release.
|
|
295
|
+
|
|
296
|
+
- **The legacy config locations now say so, once per setting (refs #2426)** — #2418 recorded the removal schedule as data; this release makes it audible. Reading `.pi-lens/lsp.json`, `pi-lsp.json`, `pi-lens.json`, or `~/.pi-lens/lsp.json` — or a `servers` / `serverOverrides` / `disabledServers` / `warmFiles` key at the ROOT of a canonical file — now emits exactly one migration notice per file and key — one, not one per loader that read the file, and no fewer either: every loader that resolves a document reports every record it produced, and the notice's subsystem label is derived from the setting itself rather than from which loader happened to be asking, so a pi-lens-owned key in a file only one loader's discovery walk opens is still announced instead of silently dropped. The notice names where the setting moves to (`move "servers" to ".pi-lens.json" under "lsp.servers"`), and the destination it names is the file the resolver actually reads, including under `PI_LENS_HOME` or a relocated `PI_LENS_CONFIG_PATH`, so following the notice always works. The notices carry the stable codes `[PILENS_CFG_0003]` (deprecated file location) and `[PILENS_CFG_0002]` (deprecated key), so they can be matched or suppressed without depending on the prose, and they are recorded once per session in the degradation ledger under a new `config-deprecated` kind — every session a deprecated setting is still in effect, not only the first one that happened to parse the file — deliberately distinct from `config-ignored`, because a deprecated setting still applies while an ignored one does not. **Deprecated since 4.1.4; read for the last time before 5.0.0.** Nothing stops working in this release: every legacy location and key is still honored, and where a legacy spelling collides with the canonical one the canonical one wins, so a half-migrated config behaves the way finishing the migration will.
|
|
297
|
+
|
|
298
|
+
### Fixed
|
|
299
|
+
|
|
300
|
+
- **Bound re-raised auxiliary coverage-gap detail (#2356)** — notify-stall pairs still count every uncovered server/file in the degradation ledger and turn-end aggregate, while detailed latency rows are capped at 20 per turn. Bounded latest identity records expose their dropped count.
|
|
301
|
+
|
|
302
|
+
- **Formatter config-signature lookup is warm-path cached (closes #1603)** — `findUp` reads each ancestor directory once, validates only matched entries, and rejects dangling links. The first lookup for a cwd computes one session-generation signature; concurrent extensions merge into one bounded cache entry, and warm selections do no configuration polling. The write-result seam invalidates selection for config create, change, and remove events, so formatter detection still re-runs without a per-call filesystem tax.
|
|
303
|
+
|
|
304
|
+
- **Keep word-index persists incremental after a session reload (refs #2068)** —
|
|
305
|
+
`deserializeWordIndex` now seeds dirty-file tracking and the wire cache, so
|
|
306
|
+
the first persist of a reloaded session takes the bounded incremental path
|
|
307
|
+
instead of a full re-serialize, and later per-edit persists in that session
|
|
308
|
+
no longer silently drop edits from the persisted snapshot. Sanitized or
|
|
309
|
+
partial snapshots use a full canonical re-serialize instead of republishing
|
|
310
|
+
discarded wire lanes. Legacy snapshots without the forward lane remain
|
|
311
|
+
compatible, but never seed the raw incremental cache.
|
|
312
|
+
|
|
313
|
+
- **The availability probe consults pi-lens's own managed bin dir, not just PATH (closes #2140)** — `createAvailabilityChecker`'s resolver now asks the installer's `findManagedToolBinary` for a release-managed binary (`~/.pi-lens/bin`, where every GitHub/Maven/archive-strategy tool is installed and which every spawn already sees on its PATH) before falling back to the bare command name. Tools pi-lens had installed itself — actionlint, shellcheck, shfmt, helm, ktlint, taplo, tflint, trivy, terragrunt, golangci-lint, hadolint, swiftlint, vale, cue, gleam, ktfmt, spotbugs — no longer latch a false `unavailable` at every session start and then get "recovered" by the install fallback several hundred milliseconds later. A project venv still outranks both managed directories, and a managed binary that exists but cannot run still falls through to a working PATH binary. The `availability_decision` record now says which directory answered (`evidence.binary`/`evidence.source`, for the npm-shim directory as well as the release one) and how long resolution took before the probe spawn (`evidence.resolveMs`).
|
|
314
|
+
|
|
315
|
+
- **Normalize escaped-newline flattened PR bodies for checking (refs #2145)** — a body can arrive with the literal two-character sequence `\\n` (or `\\r\\n`) standing in for a real line break, burying close keywords and section headings. The shared live-body read seam restores high-confidence joins only in memory, warns that the original body was not edited, and preserves genuine `\\n` content inside inline code spans. Strict live-body fetch failures remain fail-closed for close-keyword checks. This change withdraws the auto-repair behavior shipped in v4.1.3; the checks never edit pull request bodies.
|
|
316
|
+
|
|
317
|
+
- **Stop starving auxiliary LSP resyncs behind a cold primary spawn (closes #2239)** —
|
|
318
|
+
the auxiliary resync queue wait now derives from the same effective
|
|
319
|
+
per-server wait floor `getClientForFile` uses internally, instead of the
|
|
320
|
+
caller's flat budget alone. A cold primary spawn that legitimately runs
|
|
321
|
+
past that flat budget (Ruby's 30s override, and the Bash/JSON/Vue/Svelte/
|
|
322
|
+
Prisma overrides from #2233) no longer zeroes the auxiliary's queue time
|
|
323
|
+
and reports it uncovered on every such touch.
|
|
324
|
+
|
|
325
|
+
- **Bound per-file session-fact growth in dispatch's `FactStore` (refs #2282)** — `session.baseline.*` (delta-mode diagnostic baselines), `session.baseline.cascade.*`, and the review-graph's per-file entity-snapshot/changed-symbols facts minted one `sessionFacts` key per distinct file touched and never evicted, so a several-hundred-file batch retained one to four entries per path for the process lifetime. `FactStore` now routes these through a bounded, LRU-capped sibling map (`setBoundedSessionFact`/`getBoundedSessionFact`) reusing #2243's count-cap-and-report discipline instead of a third mechanism; fixed-vocabulary session facts (tool availability, `session.reviewGraph`) are unaffected. The redundant `cascadeDiagnosticBaselines` shadow map is gone, so the cascade family's entries are actually released rather than retained twice. An evicted entity snapshot now reports no diff instead of "every symbol is new", which had been scheduling blast-radius runs for files that never changed, and the changed-symbols key is written under the same normalization its reader uses. `memory_sample`'s `dispatchCaches` gains a `sessionFactEntries` count and folds its measured bytes into `estimatedBytes`, so this footprint is visible in `latency.log`.
|
|
326
|
+
|
|
327
|
+
- **The ast-grep napi fallback now reports at the LSP's severity floor (closes #2336)** — when `ast-grep-napi` stands in for the ast-grep auxiliary LSP, it evaluates the whole bundled catalog instead of only `severity: error` rules. The per-edit `blockingOnly` floor was a leftover from the runner's pre-#239 always-on role; it dropped 380 of the 481 bundled rules, so the substitute delivered a fifth of the coverage the server it replaces delivers. The runner had reported zero diagnostics in all 255 retained dispatch records, which also left #2329's napi-versus-late-LSP dedupe path with no production validation. Outside the substitute role the floor is unchanged. This narrows an earlier claim: "#2324 findings survive a silent aux LSP" previously held only for error-severity rules.
|
|
328
|
+
|
|
329
|
+
- **Classify machine-emitted text as generated by content shape (refs #2346)** — a scraped or minified page whose name carries no generated marker (`.html`) reached the full scanner stack: one 269 KB saved Google page drew 315 diagnostics. Filename patterns can never cover this class (`.html` has no `.min.`-style convention), so generated-file detection now also tests line shape: mean non-empty line length above a measured threshold (2500, ~5.5x above the 453.2 maximum hand-authored prefix mean measured across all 3,331 repo text files) on already-loaded content. The dispatch short-circuit now fires for these files and emits one deduplicated `dispatch_skipped_generated` phase record per file in latency.log naming the evidence tier and the measured statistic. Header-probe paths (project scans, source walks) classify the same shape through the existing 4 KB header read.
|
|
330
|
+
|
|
331
|
+
- Restore embedded-`<script>` ast-grep coverage under the napi fallback: HTML script bodies are now scanned with `language: JavaScript` rules, matching the ast-grep LSP. (#2347)
|
|
332
|
+
|
|
333
|
+
- **Honor runner language scope inside dispatch groups (closes #2353)** — grouped runners now use the same `appliesTo` contract as registry selection, so a runner cannot execute against a mismatched file kind and is recorded as skipped instead of failed.
|
|
334
|
+
|
|
335
|
+
- **Remove the unused runner default declaration (closes #2354)** — runner availability remains controlled by dispatch groups and explicit feature gates, so definitions no longer claim an `enabledByDefault` behavior that dispatch never enforced.
|
|
336
|
+
|
|
337
|
+
- **Run review-graph changed-symbols and entity snapshots under one normalized key per file (refs #2355)** — the session-fact key for a file's changed symbols was written under the agent-supplied spelling, while the review-graph builder read it under `normalizeMapKey`, so a case- or separator-variant path (`src/Target.ts` written, `src/target.ts` read) never hit its own write and the family sat write-only. The entity snapshot used a raw key on both sides, which forked a second empty snapshot when the same file arrived under a re-spelled path and inverted every symbol to `added`, scheduling a blast-radius run for a file nothing changed. Both keys now fold through `normalizeMapKey` at write and read, matching the #210/#1020 raw-path-key discipline.
|
|
338
|
+
|
|
339
|
+
- **Re-arm late auxiliary coverage after notify-stall teardown (refs #2356)** —
|
|
340
|
+
pending pairs survive temporary client absence, re-raise a bounded scanner
|
|
341
|
+
coverage gap when replacement does not arrive, and correlate demotion state
|
|
342
|
+
with each pair generation.
|
|
343
|
+
|
|
344
|
+
- **Coalesce superseded LSP document notifications (refs #2357)** — same-file
|
|
345
|
+
`didOpen` and `didChange` bursts now keep only the newest unwritten entry,
|
|
346
|
+
preserve started writes and different-file ordering, and record the bounded
|
|
347
|
+
coalesced count in `lsp_document_send` telemetry.
|
|
348
|
+
|
|
349
|
+
- **The LSP breaker's CPU-liveness verdict reads its own sample window (closes #2358)** — the notify-stall discriminator took the higher of its two process-CPU reads, but the first read is only a baseline: it reports a rate since whatever observation the last caller left behind (the heartbeat sampler reads every recorded LSP child once per tick, and pidusage keeps 60 s of per-pid history). A scanner that drained its burst and then wedged therefore looked "busy" on CPU it had already stopped burning, survived every re-arm, and died at the hard cap recorded as `cap-exceeded`. The verdict now comes from the window read alone, so such a server is torn down on its adaptive budget and recorded as `budget-exceeded-cpu-flat`; a genuinely busy scanner still defers, because its window read is what proved it busy in the first place.
|
|
350
|
+
|
|
351
|
+
- **Make the #2358 CPU-liveness regression proof clock-boundary safe (refs #2358).**
|
|
352
|
+
The real-runner test checks the recorded outstanding wedge window with a
|
|
353
|
+
two-millisecond Windows and Node quantization allowance, and runs in the
|
|
354
|
+
serialized wall-clock budget phase.
|
|
355
|
+
|
|
356
|
+
- **Preserve unknown root identity in `session_start_total` (refs #2129).** Quick and full startup records now write `sameRoot: "unknown"` when the root comparison has no usable input, so durable telemetry distinguishes unknown identity from a legacy omitted field.
|
|
357
|
+
|
|
358
|
+
- **Replay exact-SHA master validation after merge-train merges (closes #2380)** - merge-lane merges authenticated with `GITHUB_TOKEN` no longer lose the ordinary push-triggered validation. The lane dispatches a `merge-train-post-merge` event with the merge response's exact SHA, repository, and PR number. Each validation workflow checks out trusted workflow-revision code for its prerequisite, authenticates the payload SHA's resolution and ancestry, then lets downstream jobs check out that exact SHA and report bot-authored terminal state. Durable requested markers, six-hour retry generations, and bounded reconciliation recover process exits, missing runs, and failed validation without treating HTTP acceptance as completion. Missing merge identity and dispatch failures stay visible as landed-but-unverified errors.
|
|
359
|
+
|
|
360
|
+
- **Approve the installed ast-grep CLI lifecycle script (refs #2401)** — Keep npm v12 installs approved when the lockfile updates `@ast-grep/cli`.
|
|
361
|
+
|
|
362
|
+
- **Recognize already-applied edits instead of re-applying them on retry (closes #2402)** —
|
|
363
|
+
a mixed-validity edit batch could commit its valid subset, then report the
|
|
364
|
+
result with the preflight's oldText-not-found verdict after the post-edit
|
|
365
|
+
analysis failed, so the identical retry re-executed the write against content
|
|
366
|
+
the committed edit had already replaced (the reported session duplicated one
|
|
367
|
+
import line 15 times before the loop burned out). Partial apply now carries
|
|
368
|
+
the preflight's snapshot identity and exact non-overlapping spans instead of
|
|
369
|
+
re-searching oldText against a changed buffer (#1053 substrate): the whole
|
|
370
|
+
batch is validated — snapshot hash, span bounds, span text, pairwise
|
|
371
|
+
non-overlap — before any write, and a rejection commits nothing. The commit
|
|
372
|
+
routes through the shared atomic writer under the file lock. A committed
|
|
373
|
+
write and a failed post-edit analysis are separate outcomes, so the block
|
|
374
|
+
reason leads with the committed indexes and never with a RETRYABLE
|
|
375
|
+
oldText-miss header. Every applied pair — partial-apply commits and
|
|
376
|
+
successful native edits alike — lands in a session-scoped bounded record;
|
|
377
|
+
an identical retry is answered with `✅ ALREADY APPLIED` from that record
|
|
378
|
+
plus content evidence (oldText gone, or every remaining occurrence inside
|
|
379
|
+
its own applied newText), never from a global newText-presence heuristic.
|
|
380
|
+
Retry identity uses a fixed-size digest of the exact submitted pair, while
|
|
381
|
+
snapshot identity hashes raw file bytes. Recognition accepts either the
|
|
382
|
+
post-commit file state or the post-afterWrite state, so a deterministic
|
|
383
|
+
formatter pass between the commit and an identical retry no longer defeats
|
|
384
|
+
it; a file left in any other (unrecognized third-party) state matches
|
|
385
|
+
neither and falls back to normal resolution rather than being silently
|
|
386
|
+
accepted. Normalized matches carry raw spans, mixed batches close when a span
|
|
387
|
+
cannot be represented, and atomic writes preserve the existing mode and
|
|
388
|
+
leaf-symlink behavior.
|
|
389
|
+
|
|
390
|
+
- **An unavailable formatter no longer reads as a code failure (closes #2413)** — a selected formatter whose executable is proven absent (the oxfmt `spawn oxfmt ENOENT` trap, and its siblings `ktfmt`/`ruff` whose config-only detection can select a tool that is nowhere installed) now returns a typed `unavailable` outcome. It is never spawned via the static fallback after the resolver proves absence, never counted as a failed file, never requeued as durable `tool-not-found` on every `agent_end`, and never leaves a red `fmt-failed` footer marker. A universal belt catches the remaining formatters: when a resolver that could not prove absence falls back to a bare static command, a typed `tool-not-found` spawn failure is classified as unavailable rather than a formatting failure — no tool-specific string matching. Real failures (timeout, invalid configuration, parser failure, nonzero exit) are preserved. Observability gains a bounded `formatter-unavailable` degradation kind and an `unavailable` count on the deferred-drain records, distinct from `failed`.
|
|
391
|
+
|
|
392
|
+
- **Model-facing analyze counts no longer fold hint/info findings into warnings (closes #2420)** — `pilens_analyze` (and the `pi-lens-analyze` CLI/PostToolUse hook and the MCP server's analyze summary) reported a file whose only findings were `hint`/`info`-tier style opinions (`no-runtime-typeof`, complexity hints) as "N warning(s)". The counts read the dispatch `semantic` axis, which the runner's severity→semantic map folds every non-error tier into (`semantic:"warning"`); the severity-tier projection #2414/#2417 established (`classifyDiagnosticTier` in `widget-state.ts`) never governed this surface. The count-assembly seam now splits an `advisories` count out of the warnings bucket through that same one policy — `hint`/`info` are reported as advisories, real warnings keep their exact count, and advisories stay visible in the diagnostics list and under their own `advisory(ies)` label. The `semantic` axis (which drives blocking) is untouched: this is a display-count split, so nothing that blocked stops blocking.
|
|
393
|
+
|
|
394
|
+
- **File mutations from any tool now reach the analysis pipeline (refs #2423)** — pi-lens decided "is this a mutation" by comparing the tool name to `write` and `edit` at sixteen separate call sites, so a host or extension edit tool under any other name was dropped before the first bookkeeping call: no read-before-edit guard, no `turn-state.json` entry, no deferred autofix, no format at `agent_settled`, no change-log receipt. `clients/mutating-tool.ts` is now the one seam that answers the question, recognizing pi's built-ins by name and third-party tools by INPUT SHAPE through a deterministic adapter registry (`hashline-readmap`, `hashline-edit-pro`). `pi-hashline-edit-pro`'s bare 3-char anchors (`aB3`) are resolved to real lines by reproducing that extension's own line-hash function against the file on disk: because the extension serves anchors from a persisted store rather than recomputing them, an anchor for a duplicate-content line (`}`, a blank line) could otherwise resolve to a confident but WRONG line and hand the read guard a range the agent never touched, so pi-lens answers only when the matched line's content occurs exactly once in the file — narrowing that failure sharply (measured: ~2 wrong lines in ~550,000 anchors across 220 files, ~67% of anchors answered at all) without claiming to eliminate it; `clients/hashline-anchor.ts`'s module header has the measurement and the two residual mechanisms that survive the gate. An anchor pi-lens cannot resolve leaves the mutation classified with its lines unknown rather than blocking the edit. An adapter claims a call only when the input carries every field its schema requires, so an unrelated tool that happens to name an array `operations` is untouched. An unknown edit-shaped tool takes the deferred timing, and its changes are attributed to the tool itself instead of being folded onto `agent-edit`. Extensions can record writes pi-lens cannot see through the new in-process mutation bridge at `Symbol.for("pi-lens:mutation-bridge")`, the write-side sibling of the read bridge; pi-lens's own `ast_grep_replace apply:true` uses it, so a `--update-all` rewrite is analyzed and formatted like any other edit.
|
|
395
|
+
|
|
396
|
+
- **Config notices now name only settings pi-lens recognizes, and one file can no longer flood you with them (refs #2426)** — a legacy config file used to produce one "move it to `.pi-lens.json`" notice for EVERY top-level key it held, whether or not the key was a pi-lens setting, so a 100-key file emitted 198 notifications and told you to migrate 98 keys that have nowhere to go. Migration advice is now emitted only for keys the canonical schema recognizes; the rest are summarised in one whole-file notice naming how many were not recognized, and they keep the ordinary unrecognized-key notice they always deserved. Each of the two notice lists a file can produce — what resolving it rejected (deprecations included), and what the loader reading it refused on its own — is bounded at 20 records, up to 19 notices plus 1 summary, with the suppressed count reported under a new stable code `[PILENS_CFG_0007]` rather than dropped silently. Three related notice defects go with it: a `servers` / `serverOverrides` / `disabledServers` / `warmFiles` key at the root of `~/.pi-lens/config.json` was both honored by the LSP loader and called a typo by the global loader in the same session, and now gets only the migration notice; the warnings about settings a project `.pi-lens.json` dropped (an unknown key, a bad `maxProjectFiles`, a malformed `rules.*`) were re-reported only in the session that first read the file, and are now recorded every session the file is still in effect; and the warnings about a legacy `pi-lens.json` sitting beside or above a canonical `.pi-lens.json` were produced only when the LSP loader ran, so they were silent under `--no-lsp`, under `lsp.enabled: false`, and in subagent sessions — the pi-lens config loader now produces them itself. An internal resolution failure also names the file it happened on and is reported once instead of three times under three different subsystem labels.
|
|
397
|
+
|
|
398
|
+
- **A truncated config-notice list now says how much it truncated, and stops calling a valid file invalid (refs #2426)** — three things about the per-file notice bound were wrong once every producer shared it. The suppressed count was the tail of the truncation rather than the whole of it: a `.pi-lens.json` whose notices were cut twice — once by the resolver's own bound and again when the loader finalized what survived — reported 1 notice suppressed when 21 had been, telling you the list you were shown was nearly complete when it was not. The summary notice itself was rendered as `ignoring invalid project config …` and recorded in the degradation ledger as an ignored config, so a legacy file whose every setting was read and applied was announced as invalid and counted among the sessions that ran on defaults; it now has its own wording and its own `config-notice-suppressed` ledger kind, because a truncated list of notices is a fact about the notices, not about your config. And the unknown-top-level-key scan over `~/.pi-lens/config.json` had no bound at all, so a 100-key global config produced 100 separate notifications while an identical project file produced 19 and a count — it now goes through the same bound as every other producer. A whole-config failure also gets its own stable code, `[PILENS_CFG_0008]`, instead of borrowing `[PILENS_CFG_0005]`, which is registered as a single rejected FIELD: the two need different codes because one means a setting is missing and the other means none of the file is in effect.
|
|
399
|
+
|
|
400
|
+
- **A legacy config file's notices are bounded whichever loader reads it, and a warm config load no longer pays for the enumeration twice (refs #2426)** — the per-file cap of 20 config notices was applied by the shared resolution and by the project loader's unknown-key scan, but not by the third producer: the project loader's own enumeration of legacy `pi-lens.json` files, which shipped its records uncapped. A legacy file spelling all 28 recognized pi-lens settings therefore produced 28 notifications through the project loader and 19-plus-a-count through the LSP loader for the identical file, and five nested legacy files produced 145 notifications with no suppression record anywhere. Bounding a record list — the cap, the reserved slot, and the `[PILENS_CFG_0007]` record that carries the suppressed count — now exists in exactly one place that every producer goes through, so the two loaders agree on the count for a given file and a future producer is bounded without having to remember to be. Two silences go with it: a failure anywhere in the global loader's post-parse resolution dropped the whole of `~/.pi-lens/config.json` under a bare `catch` with no log line, ledger row or notification, and is now reported under `[PILENS_CFG_0008]` as a global-config problem; and a warm project-config load resolved its discovery entry twice and keyed the entry's freshness on every directory up to `$HOME`, so it paid double the filesystem stats and re-walked, re-read and re-parsed every legacy document whenever an unrelated ancestor directory such as `~/Desktop` was touched. A warm load is now three stats, and only the directories that decide which config file wins can invalidate it.
|
|
401
|
+
|
|
402
|
+
- **"None of this config is in effect" is never one of the notices the bound suppresses (refs #2426)** — the per-file cap of 20 config notices counted the whole-config failure record (`[PILENS_CFG_0008]`) as one more notice competing for a slot, so a config file that produced 20 rejected-key notices AND then failed to resolve dropped the one record that mattered into the anonymous suppressed count. You read "19 keys rejected, 11 further notices suppressed" and reasonably concluded the rest of the file had applied, when nothing in it had. The verdict on the whole file now outranks the bound and is kept however full the list already is — at the one place the bound is applied, so it survives the second bound the loaders run over the same list rather than being re-dropped a layer up. `docs/configuration.md` also described the bound as partitioning "deprecations, and rejected values", which is not the split: both lists are rejected-value classes (what resolving the file refused, deprecations included; and what the loader reading it refused on its own), and the doc now says so and states the bound as 20 records, the same number the notice itself quotes.
|
|
403
|
+
|
|
404
|
+
- **A malformed `~/.pi-lens/config.json` now says so, as a global-config problem (closes #2445)** — the global config loader wrapped its read and parse in a bare `catch` that returned "no config", so a syntax error in your machine-global settings produced no log line, no degradation-ledger row, and no notification from the loader that owns the file; every setting in it silently stopped applying. The only signal was the LSP loader's report of the same path, labelled `ignoring invalid LSP config`, which points at the wrong subsystem. The failure is now reported by the global loader itself under `[PILENS_CFG_0001]` with the `ignoring invalid global config` prose, a file that parses but is a list or a scalar warns instead of being dropped without a word, and the subsystem a config-read failure is reported under is derived from the FILE rather than from whichever loader opened it — so `pi-lsp.json` is still an LSP-config problem, `~/.pi-lens/config.json` is a global-config problem, and `.pi-lens.json` is a project-config problem, each reported exactly once no matter how many loaders read it.
|
|
405
|
+
|
|
406
|
+
- **Separate the mutation bridge's recordability from `--no-read-guard` (closes #2465)** — `clients/mutation-bridge.ts`'s `isRecordable` gate (wired in `index.ts`'s `registerMutationBridge`) used to fold `--no-read-guard` into the same check that decides whether a same-process producer's write is in project scope. Under that flag, a recordable write was dropped entirely — no turn-state entry, no change-log receipt, and no `noteMutationHandled` mark for the observed-mutation net — instead of only skipping the read-guard staleness stamp, the canonical split `clients/runtime-tool-result.ts` already applies (`no-read-guard` gates `readGuard.recordWritten` alone). `isRecordable` is now path-scope only; a new `shouldStampReadGuard` dep gates the stamp by itself, so turn-state, the receipt, and the deferred format queue are unaffected by the flag. `#2449`'s and `#2459`'s cross-PR hand-off rules (`noteMutationHandled` outside the `deferAutofix` guard, and the direct LSP path calling it after `addModifiedRange`) had already landed in #2459 round 4 with their own composition test (`tests/clients/lsp-mutation-settled-sweep-composition.test.ts`) — this closes the one item #2459's own review left open. Round 2: `index.ts`'s `runObservedSettledSweepSafely` and `refreshObservedLedgerSafely` had the same conflation one layer up — both blanket-returned under `--no-read-guard`, on the incorrect assumption that the sweep's baseline only exists because the read-guard is active. `recordRead` (`clients/runtime-tool-call.ts`) has no flag gate, so under `--no-read-guard` the sweep still has real third-party drift to catch; the blanket return is removed and the flag now reaches only the replay's read-guard stamp step via the same `shouldStampReadGuard` seam, reused rather than duplicated.
|
|
407
|
+
|
|
408
|
+
- **`recordEntitySnapshotDiff` enforces its absolute-`filePath` invariant (closes #2477)** — the review-graph entity-snapshot / `changedSymbols` facts are keyed by absolute path, and the only production writer (`clients/dispatch/runners/tree-sitter.ts`) always supplies one, so two project roots sharing a relative spelling like `src/index.ts` never collide. A non-absolute `filePath` from a future caller regression is now rejected with a visible, bounded `review-graph-non-absolute-entity-path` degradation record and the diff is skipped, instead of silently mis-keying. No behavior change for the real, always-absolute production path.
|
|
409
|
+
|
|
410
|
+
- **A nested `executeCommand` no longer strips the outer LSP call mutation receipt (closes #2479)** — `runServerCommand` restored `activeMutationContext` only once its depth counter returned to 0, so a nested `workspace/executeCommand` unwinding back to depth 1 left the outer call without its own context for the rest of its window: every server-initiated `workspace/applyEdit` the outer call solicited after that point fell to the mutation-bridge fallback and was recorded as a generic `agent-tool:lsp-workspace-applyEdit` write rather than the operation that actually asked for it (`lsp-rename` / `lsp-execute-command`). The slot is now DERIVED from the frame set rather than saved and restored per frame: the call that opens the outermost window owns the context for its whole lifetime, and the slot exposes that owner only while it is the sole call in flight. Overlapping calls settling in any order therefore can never bookkeep an edit through a call that has already returned, nor through one whose window the edit is not in, and "no command in flight" still means "no context". The executeCommand hardening invariants are unchanged.
|
|
411
|
+
|
|
412
|
+
- **The SubagentStop hook now reaps the stopped agent's worktree (closes #2486)** — `node scripts/prune-agent-worktrees.mjs --hook subagent-stop --quiet`, the line registered in `.claude/settings.json`, derives that agent's `.claude/worktrees/agent-<id>` from the payload's `agent_id` and removes it under the unchanged rails: never a dirty tree, never one whose HEAD is in no `origin/*` ref. Before this the registered line removed nothing at all — only an explicit `--only`, which no caller passed, could remove — so ten finished agents' trees accumulated in one afternoon and were cleared by hand. An agent resumed by SendMessage after its tree was reaped must recreate the checkout; its branch survives, and `--keep-agent-tree` / `PILENS_HYGIENE_KEEP_AGENT_TREES=1` turns the removal off for operators who resume agents. Removal now runs whether or not the process listing succeeded, each hook's sweep budget and its two removal-phase reserves are derived from the timeout registered in `.claude/settings.json` (24s + 1s + 60s for SessionStart, 4s + 1s + 5s for SubagentStop, against a measured 1049ms median removal) instead of a flat 2s that left 6 of 12 worktrees `not-evaluated`, the process-listing ceiling rises from 1200ms to 4000ms against a measured 651ms median on Windows, and a new `--scan-timeout-ms` bounds that listing independently of the sweep. Every invocation writes one `hygiene.run` ledger record — `fired`, or `skipped` with a reason that tells a missing `agent_id` apart from an agent that never had a worktree — and a run that fired but kept its tree records `keptReason` (`dirty` / `unpushed` / `self` / `locked-live` / `removal-not-permitted` / `deferred` / `status-unreadable` / `not-a-worktree`, or `null` when the tree was removed) so a protected dirty tree is no longer indistinguishable from a hook that reaped nothing. `isDirty` is re-checked, on its own small bound, immediately before the actual `git worktree remove` and before the shared `node_modules` junction is unlinked — not just once during enrichment, and not after a mutation that would otherwise silently outlive a tree the recheck decided to keep — so a write that lands in the gap between the enrichment scan and the removal call (`readProcessTable` plus one `terminatePid` per held process) is never destroyed and nothing it holds is touched either; `keptReason` also tells a `git status` that could not be read at all (`status-unreadable`, e.g. a scan budget too tight to ask) apart from a genuinely dirty tree (`dirty`) — for the pre-remove recheck too, not only the enrichment-time call: a recheck that could not read `git status` used to be folded into the same `"became dirty"` verdict as a genuine late write, so a wedged git or a bound too tight to ask read on the ledger exactly like protected work. And an on-disk directory `git worktree list` no longer registers apart from a tree the hook actually decided to keep (`not-a-worktree`). The enrichment-to-removal gap is narrowed by this recheck, not closed: a write landing in the few hundred milliseconds between the recheck itself and `git worktree remove` is not caught by any ledger record.
|
|
413
|
+
|
|
414
|
+
- **Stop cross-project delta-baseline contamination on the warm MCP server (closes #2489)** —
|
|
415
|
+
`dispatcher.ts`'s delta-mode baseline read fell back from an absolute-path
|
|
416
|
+
key to a cwd-blind relative-path key. On the warm `pilens_analyze` route's
|
|
417
|
+
module-scope `FactStore`, which can serve many project roots over one
|
|
418
|
+
process lifetime, two projects dispatching files that share a relative
|
|
419
|
+
path (e.g. both have `src/index.ts`) could collide on that fallback key,
|
|
420
|
+
so a project's first-ever delta baseline read could silently return a
|
|
421
|
+
different project's stored diagnostics. The relative fallback is removed;
|
|
422
|
+
the baseline is now keyed on the absolute, normalized file path only,
|
|
423
|
+
which can never collide across projects. The absolute-path invariant is
|
|
424
|
+
now enforced at this seam too (a hand-built, non-absolute `filePath`
|
|
425
|
+
skips the baseline read/write with a visible degradation record, mirroring
|
|
426
|
+
the review-graph entity-snapshot guard from #2477), and the delta
|
|
427
|
+
baseline hit/miss and warning count are now surfaced on the
|
|
428
|
+
`dispatch_complete` latency record.
|
|
429
|
+
|
|
430
|
+
- **`turn_end` no longer blocks the terminal on a stale worklist (closes #2504)** — a persisted ownerless `.pi-lens/turn-state.json` was adopted by the next session as "modified this turn", so a model-switch turn with zero tool calls fired 59 concurrent test-runner spawns and a 188 s LSP warning sweep over 154 historical paths. `getTurnStateAccess` now evicts an ownerless worklist last written before this session started, `clearTurnState` stamps the clearing owner, and `addModifiedRange` rejects paths outside the project root. The test fan-out gained a concurrency cap of 4, a per-turn target cap, a batch wall budget raced against the ambient abort signal, and skips targets whose test file no longer exists. `buildActionableWarningsReport` gained a project-root filter, a file cap and a wall budget, and defers its fresh LSP pulls off the awaited hook when the turn primed no cache — delivering them through the same cached channel. That deferred loop is owned by a single abort handle that `resetLSPService` fires, so `session_shutdown`, `session_start` and the LSP idle reset all stop it; every round trip carries a per-call timeout as well as the signal, no file is opened once shutdown is signalled, and an `openFile` that times out or aborts is reported unchecked rather than followed by a pull the server cannot answer. While one deferral is in flight a later turn is declined rather than cancelling it — a cold cache is the normal state of an editing turn, so cancelling on every arm meant back-to-back editing turns delivered nothing at all — and the declined turn records the files it left unchecked. Its report is then MERGED into whatever is persisted, one file at a time: entries whose file has not changed since the pull read it are folded in (warnings unioned where both reports hold the same file), and only entries for files that did change are dropped, with the dropped files named on the degradation ledger. Ordering a whole report against another lost both turns' findings whenever a deferral was declined — the declining turn always persists a newer, and often empty, report — while publishing it whole overwrote the next turn's report and silently skipped the `agent_end` autofix pass. Each entry now carries its own file sequence and its own timestamp, so `lens_diagnostics` ages a merged report's halves separately. The enrichment wall budget also bounds work INSIDE one file: it is threaded into every LSP round trip and re-read between a file's code-action pulls, so a single file with hundreds of warnings can no longer spend minutes on the awaited hook after the budget is gone. Truncation is reported through the degradation ledger, and the `turn_end: N file(s) modified` debug line now carries the resolved access and owner. Every write of that report now goes through ONE publisher that reads what is persisted and merges per file: the in-band `turn_end` write used to be a blind overwrite beside the deferred read-modify-write, so a deferred merge landing anywhere inside the next turn's `turn_end` — a window seconds wide — was erased by it, and the findings the deferral exists to deliver were lost. Entries a deferral produced are carried across exactly one following turn so that window is covered, and nothing else is carried, so a turn-delta report does not accumulate earlier turns' findings. Each entry is also stamped when its own LSP pull returned rather than when the report was assembled, so the first file of a minute-long deferred loop no longer claims to have been observed at the end of it and the mtime freshness gate keeps a real window to check. The merge's carry-forward gate now checks BOTH an exact file-sequence match and a same-session match — a file this session has never touched and a file another process (e.g. an MCP-driven analysis) touched without ever bumping this runtime's own sequence can both read as sequence 0, and only the session check tells those apart. The `turn_end` advisory's telemetry event now counts warnings from the same merged report the advisory text itself already read, instead of undercounting a rescued deferred finding as zero. A carried-forward entry dropped because its file changed before this turn's own publish could keep it, or because the publish crossed a session boundary, is now named on the degradation ledger and traced via debug logging, matching the accounting the deferred publish path already had.
|
|
431
|
+
|
|
432
|
+
- **NDJSON log sinks bound themselves mid-session, not only at session start (closes #2505)** — the shared `createNdjsonLogger` write path now caps each drain batch at `maxBytes` and rotates before the write that would cross it, so a coalesced burst can no longer sail past the bound in one shot (rotation only moves pre-existing content aside, so an oversized batch written into an empty or just-rotated file previously landed at any size at all — a 500-line burst against a 1KB bound landed at ~107KB). The real `fs.statSync` reconciliation stays throttled to a bounded cadence (at most every 25 writes or 2s), with one confirming stat immediately before a rename so a second process rotating the same global log cannot have its backup destroyed by a stale cached size. A rotation that FAILS is no longer swallowed: it is counted, backed off (10s) instead of retried on every write, and surfaced as `log-sink-rotate-failed` in `pilens_health`, while a routine rotation reports informationally as `log-sink-rotated`. `diagnostic-logger` (`logs/*.jsonl`), `review-graph-logger`, and `sessionstart-logger` — the three sinks that configured no bound at all — are now bounded like the rest, and a mechanical conformance scan fails any `clients/` sink that omits `maxBytes`. A long-lived process that never re-runs the session-start cleanup sweep (the warm MCP server included) now bounds `read-guard.log`, `latency.log`, `cascade.log`, `actionable-warnings.log`, and `ast-grep-tools.log` the same way a normal session does.
|
|
433
|
+
|
|
434
|
+
- **Send a stray probe's LOGS to a scratch dir instead of the real `~/.pi-lens`, without moving its tools or registry (closes #2506)** —
|
|
435
|
+
an ad-hoc probe against the built `clients/*.js` outside vitest (a bare
|
|
436
|
+
`node -e`, a throwaway `.mjs`, a harness script) has no test-mode gate and no
|
|
437
|
+
`PI_LENS_HOME` pin, so every logger and ledger it touched used to write
|
|
438
|
+
straight into the real `~/.pi-lens`. Confirmed live on 2026-09-02: two review
|
|
439
|
+
probes left 42 rows of fixture garbage in real telemetry — short-lived probe
|
|
440
|
+
processes, not vitest (already hermetic via `tests/support/vitest-setup.ts`'s
|
|
441
|
+
`PI_LENS_HOME` pin). The global-directory resolver in `clients/file-utils.ts`
|
|
442
|
+
is now split in two. `getGlobalPiLensDir()` stays cwd-independent and keeps
|
|
443
|
+
resolving machine-global state — installed tools, `bin/`, the cross-process
|
|
444
|
+
`instances.json` and its orphan-backstop lease, the auto-install probe cache,
|
|
445
|
+
the global `config.json`, LSP server storage, JVM runtimes — so a pi session
|
|
446
|
+
running from an agent worktree or a temp project still finds the tools it
|
|
447
|
+
installed and stays visible to every other pi-lens process on the machine. A
|
|
448
|
+
new `getGlobalPiLensLogDir()` carries the redirect and is used by the log
|
|
449
|
+
family only (`latency`, `extension`, `sessionstart`, `cascade`, `read-guard`,
|
|
450
|
+
`tree-sitter`, `word-index`, `bus-events`, `dispositions`, `dead-code`,
|
|
451
|
+
`ast-grep-tools`, `actionable-warnings`, `review-graph`, the `logs/*.jsonl`
|
|
452
|
+
diagnostic dir, the debug handle/heap dumps, log cleanup and the smells
|
|
453
|
+
rollup's read-side tail). It redirects to `<probe root>/.pi-lens-probe-home`
|
|
454
|
+
when `PI_LENS_HOME` is unset and either `PILENS_PROBE=1` is set or — outside
|
|
455
|
+
test mode — the cwd sits inside a specific `.claude/worktrees/<worktree>/` or
|
|
456
|
+
under `os.tmpdir()`. The redirect resolves once per process and records a
|
|
457
|
+
bounded `global-dir-probe-redirect` degradation; `.pi-lens-probe-home/` is
|
|
458
|
+
gitignored so a worktree that ran a probe does not stay dirty forever.
|
|
459
|
+
|
|
460
|
+
- **A headless pi child no longer exits 0 in the middle of a tool call (closes #2507)** — pi-lens unrefs the LSP child, its stdio pipes and several waiting timers so a settled one-shot `pi --print` can exit without waiting on a lingering language server. In a headless child (`pi --mode json -p --no-extensions`, stdin ignored, no TUI, no other extension holding a handle) that let libuv find nothing referenced mid `lsp_diagnostics`, so Node exited 0 with no error, no result and no `turn_end`. Every registered pi-lens tool now takes a counted event-loop hold for the duration of its own call, armed on the first in-flight call and released — from a `finally`, so a throw or an abort releases too — on the last, with a per-hold max-age failsafe: each call is bounded by its OWN acquire time, the keep-alive re-arms for the soonest outstanding deadline, and only genuinely stale holds are force-released (recorded as `event_loop_hold_force_released`, with one `event_loop_hold_armed` row per session proving the keep-alive was taken at all). Idle behaviour is unchanged: with nothing in flight nothing is referenced, and the process still exits by itself the moment the call is done.
|
|
461
|
+
|
|
462
|
+
- **Deflaked four shared-slot-contention test races (closes #2512, closes #2495)** —
|
|
463
|
+
`tests/index-vanished-instance-wiring.test.ts` no longer bridges the
|
|
464
|
+
fire-and-forget registerInstance/logVanishedInstances/sweepOrphans registry
|
|
465
|
+
chain with a fixed 50ms sleep; it now wraps `sweepOrphans` (the chain's last
|
|
466
|
+
step) to notify an awaitable settle signal, so the test proves the whole
|
|
467
|
+
chain has landed instead of guessing a wall-clock window.
|
|
468
|
+
`tests/clients/runtime-turn-session.test.ts`'s real-child-spawn test now
|
|
469
|
+
carries an explicit 20s timeout (4x the observed worst case), absorbing
|
|
470
|
+
the same shared-slot contention directly instead of exceeding vitest's
|
|
471
|
+
5000ms default under a busy batch; it stays in the default parallel
|
|
472
|
+
project since it asserts no elapsed-time budget, so it does not qualify
|
|
473
|
+
for the wall-clock-budget project's dead-last serialized phase.
|
|
474
|
+
`tests/clients/topology-derived-cache-rearm.test.ts` no longer asserts an
|
|
475
|
+
exact null `projectRoot` for a synthetic `homeDir` that only pins the
|
|
476
|
+
ceiling check and cannot confine `findNearestProjectRoot`'s contractually
|
|
477
|
+
unbounded upward walk — a real project marker anywhere above the test's
|
|
478
|
+
temp directory on the host (matching `tests/clients/
|
|
479
|
+
startup-scan-home-ceiling.test.ts`'s established fixture shape) previously
|
|
480
|
+
produced a found-but-rejected root instead of `null`. The assertions now
|
|
481
|
+
check `projectRoot` identity against the tmp cwd directly, which is
|
|
482
|
+
deterministic regardless of the host's real ancestor tree (checking
|
|
483
|
+
`canWarmCaches` instead was itself vacuous, since the synthetic `homeDir`
|
|
484
|
+
rejects every reachable root before and after a marker is planted).
|
|
485
|
+
`tests/clients/dispatch/runners/runner-helpers.test.ts`'s locale-key dedup
|
|
486
|
+
test now asserts the probe call count only after both racing callers have
|
|
487
|
+
settled, instead of right after a fixed 100ms sleep — the sleep itself
|
|
488
|
+
stays (it lets the second caller join the in-flight probe before it
|
|
489
|
+
resolves), but the assertion moved off that timing-dependent instant.
|
|
490
|
+
|
|
491
|
+
- **Close out the #2508/#2511 ndjson-logger and log-dir leftovers (refs #2515, refs #2516)** — pinned the `actualSize === 0` rotation guard with a regression case (a stale-high cached size plus a sibling process that already rotated and emptied the active file, plus an incoming line alone >= the rotation bound), removed a dead `knownSize` adoption shim, and folded `/lens-perf`'s degradation rendering onto the same `renderDegradationLines()` seam `pilens_health` already uses so the two surfaces agree. Added a conformance test pinning that every `createNdjsonLogger` caller resolves its directory through `getGlobalPiLensLogDir()` and never through the machine-state-only `getGlobalPiLensDir()`. Moved `getGlobalPiLensLogDir()` and the whole probe-home redirect decision out of `clients/file-utils.ts` onto the cycle-free `clients/probe-home-state.ts` leaf, and deleted the `isTestMode()` short-circuit that used to guard it: that guard depended on where an unrelated import happened to sit in `file-utils.ts`, and it sent every `VITEST`-marked process with no `PI_LENS_HOME` pin (vitest's globalSetup and the children it spawns) to the developer's real `~/.pi-lens` instead of a probe home. Round 3: corrected the `global-dir-probe-redirect` degradation row's reason text, which still described the removed `isTestMode()` gate ("unset outside test mode") after the gate itself was deleted — it now names the actual triggers (worktree/tmpdir cwd, or `PILENS_PROBE=1`) and fixed two stale doc-comment attributions of `getGlobalPiLensLogDir()` to `file-utils.ts` instead of `probe-home-state.ts`.
|
|
492
|
+
|
|
493
|
+
- **Turn-end advisories name the resolved cache path, not a hardcoded `.pi-lens/` one (closes #2521)** — thanks to [@Stark-X](https://github.com/Stark-X) for the report. The actionable-warnings and code-quality advisories told the agent the report was at `.pi-lens/cache/<name>.json`, a spelling that is only correct when a project already has a legacy `.pi-lens/` directory; everywhere else the report is written under `~/.pi-lens/projects/<slug>/cache/` (or `PILENS_DATA_DIR`), so following the advisory produced `cat: … No such file or directory` while the report sat unread. Both advisories now lead with `lens_diagnostics mode=delta` — so an agent never needs the cache layout at all — and name the raw file second, rendered by a new shared `displayProjectDataPath(cwd, …)` helper that reuses whatever `getProjectDataDir(cwd)` resolved: relative to the project when the store is inside it, otherwise absolute and `~`-folded so the account name stays out of agent context. A new sweep fails any future string literal that hardcodes a `.pi-lens/` project-data path.
|
|
494
|
+
|
|
495
|
+
- **LSP wait and sweep-warm-up records name only rooted candidates (closes #2525)** — a plain TypeScript project with no `deno.json` logged `serverIds: ["typescript","deno"]` in `lsp_client_wait_timeout`, `lsp_client_wait_skipped` and the `lsp_sweep_warmup_*` records, even though Deno's fallback server never resolved a root and never spawned. All five records now list only candidates with a resolved root, plus a separate `unrootedCandidates` count/id list. Roots are resolved lazily on the cold record paths and memoized per acquisition, so a warm touch resolves nothing extra and the `lsp_client_unavailable` bookkeeping no longer re-walks a root the acquisition attempt already resolved. The `lsp_client_wait_timeout` record's own root resolution is now hardened: a rejecting `root()` degrades that candidate to unrooted (with a `reasons` entry) instead of throwing out of `getClientForFile`, and the resolution itself is bounded so it can never add unbounded latency on top of an already-expired wait budget — it reports `partition: "timed-out"` when its own small ceiling is hit.
|
|
496
|
+
|
|
497
|
+
- **`lens_diagnostics mode=full` and `--lens-guard` now treat a test RUNNER error as advisory, matching the turn-end message (closes #2532)** — a
|
|
498
|
+
timeout, missing provider/binary, or configuration error (the suite itself
|
|
499
|
+
never produced a verdict) rendered as a `blocking` finding in
|
|
500
|
+
`testResultToProjectDiagnostics` and flipped `hasBlockers: true` under the
|
|
501
|
+
experimental `--lens-guard` merge, while #2522 already delivers the
|
|
502
|
+
identical event as advisory in the turn-end context message. Both
|
|
503
|
+
consumers now route through the single `isRunnerErrorResult` classifier
|
|
504
|
+
(`test-runner-client.ts`) the turn-end path already used, so a runner
|
|
505
|
+
error reads as advisory everywhere and a genuine failing test stays
|
|
506
|
+
blocking everywhere.
|
|
507
|
+
|
|
508
|
+
- **Overlap auxiliary LSP warmup with primary server during resync (closes #2540)** — on cold first edit, auxiliary LSP server acquisition is kicked off concurrently and unawaited via deferred LSP work at resync time, overlapping auxiliary warmup with the primary language server. Bounded per server by `bounded()` using the ambient turn signal and caller hook attribution, dropping cold two-server edit latency from the sum of both spawns to the slowest single spawn. Auxiliary readiness duration is pushed to the latency log under the `auxiliary_readiness` phase.
|
|
509
|
+
|
|
510
|
+
- **Label-manifest governance test now derives its full requirement, and `prune: true` is explicit where it lives (refs #2553)** — `tests/config/label-manifest-coverage.test.ts` (added in #2597) only compared `.github/labels.yml` against a hand-copied list plus the AREA set, so removing `bug` or `help wanted` from the manifest stayed green while the label syncer would still delete either. The test now derives every required label from the sources that actually reference it — AGENTS.md's TYPE, AREA and "reuse GitHub defaults" bullets (block-parsed so a wrapped list can't silently shrink), plus every `train:*`/`red-ci`/`conflict`/`priority:*` literal in the merge-train skill doc, `scripts/lib/merge-train-*.mjs`, and every `.github/workflows/*.yml` file. `.github/workflows/labels.yml` also now sets `prune: true` explicitly on the `action-label-syncer` step (the pinned action already defaulted to it) so the cause of #2553's deletions is visible where the workflow runs, not only in the manifest's own comment. This push touches both `labels.yml` files, so the syncer runs again on merge; that run is expected to be a no-op since the manifest already carries everything it requires.
|
|
511
|
+
|
|
512
|
+
- **Label syncer no longer deletes the priority labels on every merge-train lane merge (refs #2553)** — `.github/workflows/labels.yml` runs `micnncim/action-label-syncer` with `prune: true` against `.github/labels.yml`, and the manifest never listed `priority:p1`/`p2`/`p3`, so every sync deleted them and stripped the label from every open issue. The three labels are now in the manifest (matching the live colors/descriptions), a comment above them documents the syncer's prune behavior for the next person adding a label by hand, and a new governance test (`tests/config/label-manifest-coverage.test.ts`) fails loud if the manifest ever again drops a label this repo's own rules require. Re-applying the stripped priorities to the 188 already-triaged issues is tracked separately in #2553.
|
|
513
|
+
|
|
514
|
+
- **compat-smoke Layer A resolves each pinned contract's source file independently (refs #2581)** — `pi-subagents@0.65.0` relocated the file behind the `PI_SUBAGENT_CHILD` env-var contract and removed the `PI_SUBAGENT_RUN_ID`/`PI_SUBAGENT_CHILD_AGENT` identity vars entirely, so the nightly's Layer A read a single hardcoded path inside one shared try/catch and reported an `INFRA FAILURE` (a plain `ENOENT`) as generic contract drift for the whole run, blinding verification of the other six contracts too. `scripts/compat-contracts.mjs` now resolves each contract's source through an ordered candidate-path list (`scripts/lib/compat-contract-locator.mjs`) and reports one of three outcomes per contract — verified / drift / infra — instead of a single pass/fail; `docs/subagent-compat.md` documents all three. The identity-var requirement was dropped from `checkNicobailonChildEnv` since pi-lens's `getSubagentIdentity()` already degrades gracefully when they're absent — no runtime behavior changed.
|
|
515
|
+
|
|
516
|
+
- **`peerDependencies["@earendil-works/pi-tui"]` no longer excludes pi 0.85.x (closes #2586)** — `^0.84.1` on a pre-1.0 host version pins the minor (npm's caret on `0.x.y` only floats the patch), so a real pi-coding-agent/pi-tui 0.85.x host was rejected by declaration even though the nightly real-pi compat smoke already runs green against 0.85.1 and pi-tui 0.85.1 still exports every symbol `clients/deps/pi-tui.ts` consumes (`Text`, `truncateToWidth`, `visibleWidth`). The peer range is now `^0.84.1 || ^0.85.0` — explicit about exactly the two minors this repo has compat evidence for, rather than a `>=`/`<` span that would silently endorse an untested future minor. `devDependencies` and the lockfile move to `@earendil-works/pi-coding-agent@^0.85.1` and `@earendil-works/pi-tui@^0.85.1` so the unit suite itself runs against the newer host. `tests/packaging.test.ts` gains a regression check (`semver.satisfies` against the declared range, tested-versions list derived from the lockfile plus literal `"0.85.1"` and `"0.84.1"` entries pinning both ends) that fails if the declared range ever again excludes a version this repo has actually verified, without asserting acceptance of anything untested.
|
|
517
|
+
- The new OR-form range exposed a latent shell-splitting hazard in `scripts/supply-host-provided-deps.mjs --install-args`: it space-joined its `name@range` entries, and `.github/workflows/ci.yml`'s three callers split that output with `read -ra HOST_PKGS <<< "$(...)"`, which word-splits on ALL whitespace — exploding the pi-tui entry into three argv tokens and failing `npm install` with `notarget No matching version found for undefined@^0.85.0`. The script now newline-joins its entries (a newline can never appear inside one `name@range` string), and all three call sites read it with a portable `while IFS= read -r` loop (never `mapfile -t`, which does not exist on macOS's bash 3.2) that splits on newlines only.
|
|
518
|
+
- `package-lock.json` was regenerated with npm 11.18.0 (`npm install --package-lock-only`, starting from master's current lockfile so only the two bumped devDependencies and their own transitive closure change) to restore the `libc` platform descriptors an older local npm had stripped (#9025), matching the version CI already pins for this reason.
|
|
519
|
+
|
|
520
|
+
- **`pi.skills` no longer escapes the published package (closes #2587)** —
|
|
521
|
+
the manifest entry pointed at `../../skills`, which resolves *outside* the
|
|
522
|
+
installed package on every layout (npm, `git:` clones, managed caches). The
|
|
523
|
+
four shipped skills still registered through the extension's own
|
|
524
|
+
`resources_discover` handler, but where the escaped path landed on an
|
|
525
|
+
existing directory (your project's `skills/`, or `<cache>/npm/skills`) pi
|
|
526
|
+
adopted that foreign tree as a pi-lens package resource and a same-named
|
|
527
|
+
entry there shadowed the real `pi-lens-ast-grep`,
|
|
528
|
+
`pi-lens-lsp-navigation`, `pi-lens-write-ast-grep-rule` or
|
|
529
|
+
`pi-lens-write-tree-sitter-rule`. pi resolves a non-glob manifest entry
|
|
530
|
+
against the package root, so the entry is now `./skills` and only the
|
|
531
|
+
package's own skills are registered.
|
|
532
|
+
|
|
533
|
+
- **Merge gate no longer false-fails on a concurrency-superseded cancelled check-run (closes #2632)** — `merge-train-lane.mjs`'s real `evaluateMergeGate` treated a discovered (non-required) check-run's `cancelled` conclusion the same as any other failure, but this repository's `cancel-in-progress: true` leaves a stale cancelled row as the only evidence for a check name for several minutes before its replacement posts (live-probed on PR #2607's "Record post-merge validation"). That row now holds the merge (a new `check-superseded` reason, re-evaluated on the lane's normal 10-minute/`check_suite` cadence, naming the check in the PR comment) instead of being read as a failure, via the same `isUncertainConclusion` predicate `ci-verdict.mjs` already used (#2618) — a required check's (`Unit tests`, `Lint & type-check`) `cancelled` conclusion still fails outright, and a genuinely failing check (including one superseding an unorderable cancelled duplicate) is never masked.
|
|
534
|
+
|
|
535
|
+
- **`ci-verdict.mjs`'s absent-required-checks reason now says why (refs #2664)** — when both required checks are absent and the PR is not merge-conflicted, the exit code was already 3 (pending, not 0) since #2539/#2609, but the printed reason now ends with a conditional "if the base was retargeted after this PR opened, push a commit or close/reopen to re-arm ci.yml" for the scenario that prompted the report — `ci.yml` never re-registers against a retargeted base since it has no `edited` trigger. Phrased as a conditional, not a directive, since the far more common cause of this same verdict is a fresh push where CI simply has not registered yet, and an unconditional "push a commit" would tell that routine case's reader to restart all of CI for no reason.
|
|
536
|
+
|
|
537
|
+
- **Use project Python environments without shell activation (closes #2407, refs #1513)** — Python language servers, standalone Pyright, and pytest now use a detected `VIRTUAL_ENV`, `CONDA_PREFIX`, `.venv`, or `venv`. Project-local pyright, basedpyright, and ty binaries take precedence over managed fallbacks, while pytest runs through the project interpreter.
|
|
538
|
+
|
|
539
|
+
- **Memoize package-manager global bin dirs (closes #1602)** — `findGlobalBinary` no longer re-spawns `npm config get prefix`/`pnpm bin -g`/`yarn global bin` on every miss; the per-manager bin dir is cached alongside the existing availability latch, shares one in-flight probe across concurrent callers, and rejects stale writes from probes that cross a session reset.
|
|
540
|
+
|
|
541
|
+
- **Log session-start classification alongside `mode` (closes #2129)** — `decideSessionStart` (#2133/#2156) already keeps a subagent temp worktree from stealing a process's primary session and re-running the full startup battery, but the classification decision it made was invisible on the accepted path: `session_start_total`, the record `mode` (quick/full) lives on, carried no root-identity input at all — only a declined start's `concurrent_session_bind` did. `handleSessionStart` now logs `classification` and `sameRoot` on `session_start_total` too, so a log reader can tell `primary` from `sequential-replacement` without cross-referencing a separate record.
|
|
542
|
+
|
|
543
|
+
- **Emit availability_decision telemetry on the LSP-launch managed-bin fast path (refs #2140)** — `resolveAndLaunch`'s `~/.pi-lens/bin` resolution (clojure-lsp, cue, deno, expert, gleam, marksman, opengrep, rust-analyzer, taplo, terraform-ls, typos-lsp, zizmor, zls) now logs the same `availability_decision` record `SecurityScanClient`'s CLI-scan probe already writes: exactly one `available` row when the managed binary launches directly, and the `unavailable`-then-install-override pair when it doesn't, so the LSP-server launch path stops being invisible in `latency.log`.
|
|
544
|
+
|
|
545
|
+
- **Cap the widget footer's dependency-drift re-serves (refs #2275, sibling of #1950)** — A dependency-drift-demoted diagnostic that the pi-lens footer keeps re-serving is now retired after 3 unconfirmed deliveries, instead of re-serving for the rest of the session. Deliveries are counted per footer RENDER (the footer draws one file per pass, so files it never showed are never charged), and retiring HIDES the row from the footer only: it stays in `lens_diagnostics mode=all` and remains addressable by `lens_diagnostic_mark`, with a note saying the footer stopped showing it and a re-run can still confirm it.
|
|
546
|
+
|
|
547
|
+
- **The #2117 arena-recompaction regression test no longer flakes on a scheduling accident (closes #2293)** — `compactPostingsIntoArenaCooperatively` (`clients/word-index-store.ts`) now takes an optional test-only `{ deadline, beforeYield }` seam; production still defaults to the real 8 ms wall-clock `createDeadline(8)`. The old test asserted `grewDuringYield === true`, but that flag was only true when the real cooperative copy happened to cross the 8 ms budget before finishing — a scheduling accident that a busy CI box (or a smaller corpus) could miss, tripping `expected false to be true` on an otherwise-correct fix (3 unrelated PRs paid for this: #2279, #2452, #2456). The rewritten test injects a deadline that expires deterministically on its first check and a `beforeYield` hook that grows the victim list in-line with the real cooperative pause, so the invariant — a list that grows mid-copy is never corrupted — is now proven under a forced yield instead of a hoped-for one.
|
|
548
|
+
|
|
549
|
+
- **Session-state sweep flags process-singleton-backed state and accepts closure-located resets (refs #2319)** — the session-state conformance scan now detects `getProcessSingleton` cells and the registry accepts a reset that lives in `index.ts`'s `session_start` closure, so the declined-bind rollup, the verified-attribution tally, and the in-flight-phase bracket map register honestly instead of hiding in a blind spot. The verified-attribution tally now rides `getProcessSingleton` (the same shape its sibling rollup chose), so module re-evaluation can no longer undercount it.
|
|
550
|
+
|
|
551
|
+
- **Rule tooling handles mixed-case language tags (closes #2331)** — audit duplicate detection and catalog overlap warnings compare canonicalized language IDs without rewriting the corpus.
|
|
552
|
+
|
|
553
|
+
- **Enforce ast-grep-napi's skipTestFiles on the write-group dispatch path (refs #2337)** — the napi runner declared `skipTestFiles: true`, but that flag was only checked in `RunnerRegistry.getForKind`. The jsts write-group path resolves runners by id and runs them through `runGroup`, which never consulted the flag, so the runner still ran and reported findings on `.test.ts`/`.spec.ts` files. `runGroup` now applies the same `isTestFile` gate before invoking a runner, recording a `test_file_skipped` latency status instead of silently dropping the skip. The two consumers that enumerate skip statuses — the coverage notice's `allPrimarySkipped` check and the `lsp.ran` field in `analyze` — now count `test_file_skipped` as a skip, so a file whose only primary runner was gated does not read as clean.
|
|
554
|
+
|
|
555
|
+
- **Breaker teardown tells dead from busy (#2358).** The LSP notify-stall breaker
|
|
556
|
+
grants a wedged scanner an adaptive patience window (per-write drain latency
|
|
557
|
+
x backlog depth) and samples its process CPU before any kill. A server that
|
|
558
|
+
is burning a core while it drains a burst is left alone and re-armed, not
|
|
559
|
+
torn down; only a flat-CPU or cap-exceeded server is killed, and the teardown
|
|
560
|
+
record names which discriminator fired. Windows CPU sampling now resolves
|
|
561
|
+
`powershell.exe` through `System32`, which was silently missing before.
|
|
562
|
+
Streak teardown releases TypeScript idle-timer ownership before the asynchronous
|
|
563
|
+
CPU probe and re-arms it only when the same client remains busy. Notify timers
|
|
564
|
+
now release with their client generation, and stale decisions cannot demote a
|
|
565
|
+
replacement. Windows CPU history includes CIM process-creation identity, so
|
|
566
|
+
PID reuse starts a fresh rate window. Missing targets and failed queries remain
|
|
567
|
+
unmeasured, while busy-defer detail is bounded per client/file identity. CPU
|
|
568
|
+
counters are validated and history is capped; decision rows stay out of
|
|
569
|
+
last-phase attribution.
|
|
570
|
+
|
|
571
|
+
- **Isolate post-agent test delivery (refs #2366)** — Bind staged test findings to their activation and session before quiet-window delivery.
|
|
572
|
+
|
|
573
|
+
- **Nightly tool-smoke**: the `--lsp` lane's five auxiliary rows (opengrep, ast-grep, zizmor, typos, ast-grep-baseline) went to zero on every nightly run since 2026-08-25, and `characterize-lsp.mjs`/`probe-clean-signal.mjs`/`server-capabilities.mjs` silently under-reported fixtures the same way. Each script registered a fixture's temp workspace as a served session root (`initLSPConfig`) only for `disableServers` fixtures; once one of those ran, the session-root registry flipped from empty (fail-open) to non-empty and every later fixture's own (unregistered) workspace was silently declined. All four scripts now register every fixture's workspace unconditionally, and share one guard (`scripts/lib/lsp-fixture-session-guard.mjs`) that throws if a fixture is ever touched unregistered again. Product behavior was never affected — a single real workspace root is always registered in production.
|
|
574
|
+
|
|
575
|
+
- **Unconfigured HTML and YAML files no longer auto-format with Prettier (closes #2384)** — Template markers such as an HTML `<script>{{JS}}</script>` embed or a Helm `{{ .Values.x }}` were reinterpreted as code by the smart-default Prettier selection, silently corrupting runtime templates. `.html`, `.htm`, `.yaml`, and `.yml` now select no formatter without a project Prettier config, following the Markdown precedent; an explicit `.prettierrc` or `package.json` `prettier` field still opts in. Thanks to @aspiers for the report and reproduction.
|
|
576
|
+
|
|
577
|
+
- **Biome lint and autofix no longer produce import-type rewrites for projects without a Biome config (refs #2385)** — the bundled fallback `config/biome/core.jsonc` now disables `useImportType` on both surfaces, which share one config seam (`biomeConfigArgs`): the lint surface loses this one advisory, and the autofix pass (`biome lint --write`) stops rewriting value imports. The rule's safe fix turns an import used only in type positions into `import type`, which erases the runtime binding that experimental decorator metadata still needs: with `emitDecoratorMetadata`, the emitted `design:type` stops referencing the imported class (verified with real Biome 2.5.9 and tsc, it becomes `Function`), breaking decorator-based dependency injection at runtime. Every other recommended rule stays on. An explicit project `biome.json(c)` remains authoritative: if you enable `useImportType` there, pi-lens never overrides it.
|
|
578
|
+
|
|
579
|
+
- **Keep alternate language servers as fallbacks during all-scope diagnostics (closes #2400)** — An aggregate LSP pass no longer launches Deno, Jedi, OmniSharp, or Expert beside a working preferred server. The alternate still starts when its preferred server is unavailable.
|
|
580
|
+
|
|
581
|
+
- **Bound post-merge reconciliation by its time window (refs #2405)** — Reconcile recent merge-train validations without scanning the full closed pull-request history.
|
|
582
|
+
|
|
583
|
+
- **Parse pytest counts only from its terminal summary (closes #2408)** — Traceback and service-error numbers such as `port 55432 failed` can no longer become the reported failed-test count, and ANSI formatting or rerun outcomes no longer corrupt the reported counts and duration.
|
|
584
|
+
|
|
585
|
+
- **Stop presenting hint/info findings as code warnings (refs #2414)** — the TUI footer, `lens_diagnostics` (mode=all/full), and every compact finding-count surface now project severity through one shared classifier: `hint`/`info` tier findings (style opinions such as `no-runtime-typeof`, complexity hints) no longer inflate the `warnings` count or the footer's `!NW` chip. They stay visible everywhere that matters — the detailed diagnostics list, a new `advisories` count so a hint-only file isn't silently dropped from a listing, and a distinct "N hints" line in `lens_diagnostics`' summary — they just never share the warning tally with real, bounded-false-positive-rate warnings.
|
|
586
|
+
|
|
587
|
+
- **Stop leaking config file contents into diagnostics on a JSON parse error (refs #2431)** — Node's `JSON.parse` embeds a slice of the source text in its own error message, so a malformed `.pi-lens.json`/`lsp.json` that happened to carry a credential next to the syntax error leaked it into the notification, `extension.log`, and the degradation ledger. `clients/config-warn.ts`'s shared warn seam now normalizes a caught parse/read error to its class plus a position (`SyntaxError at line 12 col 8`) instead of the raw message, so nothing from the file's contents ever reaches those sinks. The same seam also `redactSecrets`-scrubs every hand-authored reason string, not just caught-error ones — a secret-shaped config KEY or rule id read from the file and interpolated into a validation message (e.g. `unknown key "<key>" is not a recognized setting`) previously reached the same three sinks untouched — and the parse-error discriminator now duck-types on `error.name` instead of `instanceof SyntaxError`, so a `SyntaxError` thrown across a realm boundary (e.g. inside a `vm` context) is still fully normalized instead of falling through to the redaction floor.
|
|
588
|
+
|
|
589
|
+
- **`tests/fixtures/fake-lsp-server.mjs` can no longer outlive a killed parent (refs #2436)** — a real orphan of this fixture was found running an hour after the test that spawned it finished, its parent process long gone, holding its worktree directory open (`git worktree remove` → Permission denied). The fixture now self-terminates on stdin EOF and on a parent-liveness poll (`process.kill(originalPpid, 0)`, ≤1s), so it cannot survive a SIGKILLed parent even when no JS-level teardown ever runs. Every test spawn of the fixture now routes through one helper (`tests/support/fake-lsp-server.ts`) that also registers a guaranteed `onTestFinished` kill as a backstop. `tests/support/fault-injection.ts`'s `spawnWedgedChild` (the same "long-running spawn, caller-owned lifetime" shape) got the same backstop.
|
|
590
|
+
|
|
591
|
+
- **Wire oxlint into `npm run lint` and CI for plain `.mjs`/`.cjs`/`.js` files (refs #2439)** — `oxlint` was a devDependency with no npm script and no CI job, so `npm run lint` (tsc over the TS project only) never touched `scripts/*.mjs`; an undefined identifier there shipped past both local lint and CI, caught only by running the CLI by hand (#2435). New `npm run lint:js` runs oxlint with `--deny-warnings` over `scripts/**/*.mjs`, root `*.mjs`, `tests/**/*.mjs`/`.cjs`/`.js` (genuinely linted now — only the deliberately-broken `autofix-smoke`/`format-smoke`/`tool-smoke` dispatch-fixture payloads and the two intentionally-unresolved parse-fixture files/dirs are excluded, enumerated by name in `.oxlintrc.json` and `package.json`'s `lint:js` script, not a blanket `tests/fixtures/**`), and `.dependency-cruiser.cjs`, with `no-undef` promoted to an error for `.mjs`/`.cjs`/`.js` files via the existing root `.oxlintrc.json` (scoped by `overrides`, not a second config file) — `no-undef` stays off for `.ts`/`.tsx` since tsc is already the source of truth there and TS ambient globals risk false positives. `npm run lint` now chains tsc with `lint:js`, and the CI `Lint & type-check` job's step picks it up for free. Baseline: 19 hits found on the first real run — 17 fixed (dead imports, an unused function, empty catch bindings, three `RegExp#test` calls simplified to `String#startsWith`/`endsWith`, one redundant spread fallback) and 2 allow-listed inline with reasons (an intentional ANSI-escape control character in a regex, an intentional documented nbsp in a comment); re-running now reports 0. A standalone `npx oxlint clients tools` run (not wired — noisy) found 30 warnings, 0 errors — TS wiring is a follow-up if that's driven to zero.
|
|
592
|
+
- Review round 2 (#2452): `lint:js` was missing `--deny-warnings`, so the baseline's own 19 hits (all warning-severity) had exited 0 pre-fix and the self-scope regression test was mutation-blind; added `--deny-warnings` (tree is at 0). The `.oxlintrc.json` `no-undef` override was scoped to `*.mjs`/`*.cjs` only, so `scripts/download-grammars.js` (a plain `.js` file that runs from `prepare`) got no coverage — added `*.js`. `tests/**/*.mjs` coverage had actually been zero (the blanket `tests/fixtures/**` ignore, in both `package.json` and `.oxlintrc.json`, swallowed every real Node fixture program alongside the intentional smoke payloads) — narrowed to the specific payload directories/files, which surfaced and fixed 3 real hits: an unused `path` import in `tests/fixtures/instance-registry-race-worker.mjs`, and two `unicorn/no-new-array` in `tests/fixtures/hashline-edit-pro/generate-anchor-vectors.mjs`.
|
|
593
|
+
|
|
594
|
+
- **LSP-applied edits (rename, `workspace/executeCommand`) now always bookkeep through the existing LSP mutation seam, and name the specific operation (closes #2450)** — `applyWorkspaceEdit`'s bookkeeping (`clients/lsp-mutation.ts` `bookkeepLspMutation`, from #1066) depends on its caller threading a `runtime`/`cacheManager` reference through `LspMutationContext`. One caller structurally cannot: `workspace/applyEdit`'s server-initiated-edit handler (`clients/lsp/client.ts`) builds its own fallback context with neither, reached whenever a nested/re-entrant `executeCommand` call is in flight (`state.activeMutationDepth !== 1`, which clears the outer call's own context for the window) — so a server-solicited write reached disk with zero bookkeeping: no read-guard stamp, no turn-state entry, no change-log receipt. `bookkeepLspMutation` now falls back to the mutation bridge (`clients/mutation-bridge.ts`, #2423) — mounted once at extension activation with live getters onto the SAME runtime/cache-manager singletons — for exactly that case, so the write is always recorded through one seam or the other, never silently dropped. Separately, every LSP-applied edit's change-log receipt now names the operation that produced it (`lsp-rename` / `lsp-execute-command`) instead of a generic `lsp-edit` tag that made a rename indistinguishable from an executeCommand-solicited edit.
|
|
595
|
+
- Review round 2: the bridge fallback is now behaviorally equivalent to the direct bookkeeping path — it threads the real `importsChanged` value (instead of the bridge's historical hardcoded `false`), never enqueues a deferred autofix/format pass for an LSP-applied edit (the direct path never did either — both branches must agree), and applies the SAME ignored/vendor/`no-read-guard` recordability gate the direct path now also applies (previously only the fallback gated). A dropped bridge record is now `dbg`'d with its reason instead of silently discarded, and the MCP server process — which never mounts the bridge — now records a bounded once-per-session degradation instead of silently losing every LSP-applied edit's bookkeeping there. `clients/file-utils.ts` gained `isRecordableProjectPath`, consolidating a gate that was hand-duplicated at 2 call sites in `index.ts` into 1 shared helper, now reused by a 3rd call site (`tools/lsp-navigation.ts`).
|
|
596
|
+
- Review round 3: `tools/lsp-navigation.ts`'s direct-path `isRecordable` had regressed to dropping EVERY LSP-applied write under `--no-read-guard` instead of just skipping the read-guard stamp (that flag gates the stamp only, per `clients/runtime-tool-result.ts`'s canonical split) — fixed, and it now judges a path against the real project root (`LspMutationRuntime.projectRoot`, newly threaded) instead of the request's `cwd`, so a call issued from a sub-package directory no longer misjudges a sibling-package write as external. `clients/lsp/client.ts`'s server-initiated `workspace/applyEdit` handler dropped a redundant `activeMutationDepth === 1` re-check — `runServerCommand` already enforces that invariant when it sets `activeMutationContext`, so re-testing it here was dead code. The MCP-no-bridge degradation's ledger `subject` is now a fixed key instead of varying per LSP operation (was under-deduplicating across operation kinds within one session), and its `dbg` line is now gated to fire once per session instead of once per touched file.
|
|
597
|
+
- Review round 4 (composition with #2449's observational mutation net): an LSP-applied write is now marked as pi-lens-accounted-for on BOTH bookkeeping branches, so the `agent_settled` settled sweep no longer reports it as drift no tool call explains. `recordMutationThroughSeam`'s `noteMutationHandled` stays OUTSIDE the `deferAutofix` guard — the LSP fallback passes `deferAutofix: false`, and "pi-lens recorded this write" is a different question from "pi-lens will format it later" — and `bookkeepLspMutation`'s direct branch, which bypasses the bridge entirely, now calls it itself; without that, every `lsp_navigation` rename was reported as drift.
|
|
598
|
+
- Review round 5 (regression coverage, no behavior change): round 3's `isRecordable` fix (judging a write against `LspMutationRuntime.projectRoot` rather than the request `cwd`) had no test exercising the real production closure — only an integration test that hand-mirrors the same logic, so reverting to plain `cwd` stayed green. Added a test that captures the actual `mutationContext.isRecordable` built inside `tools/lsp-navigation.ts` off a mocked `executeCommand` call and asserts it against a sibling-package path. Separately, round 3's once-per-session "bridge unavailable" `dbg` latch (`clients/lsp-mutation.ts`'s `noBridgeDbgLogged`/`resetLspMutationNoBridgeDbgLatch`) was wired into `handleSessionStart` but never registered in the session-state lifecycle registry, so the reachability conformance check could not tell if that wiring ever went missing; registered it with a probe (a new test-only `_lspMutationNoBridgeDbgLoggedForTests` getter) that drives a no-bridge mutation record, resets the latch, and asserts a second record dbg's again.
|
|
599
|
+
|
|
600
|
+
- **Extend `npm run lint:js` (oxlint `--deny-warnings`) to `clients/`, `tools/`, `mcp/`, and root `index.ts` TypeScript (refs #2439, #2454, #2462)** — `lint:js` blanket-ignored `**/*.ts`/`**/*.tsx` since #2439, so a warning-tier hit in production TypeScript (the bulk of the codebase) shipped past `npm run lint`; a standalone `npx oxlint clients tools` run found 30+ un-triaged warnings at the time. The ignore is now scoped to `tests/**` (a separately-triaged 58-hit tree, tracked by #2462) instead of dropped globally; the `cases/**` ignore was also dropped — those 3 fixture files (`cases/ts/**`) were already lint-clean, so the flag was dead. `clients/`, `tools/`, `mcp/`, and `index.ts` are linted now. `no-undef` stays off for `.ts`/`.tsx` (tsc is the source of truth; unchanged from #2439).
|
|
601
|
+
|
|
602
|
+
Against the correctly pinned `oxlint@1.80.0` devDependency (a review-round-1 pass had run against a stale global `oxlint@1.69.0` instead and mis-enumerated the `no-control-regex` hits): **33 genuine warning-tier hits, all fixed, 0 allow-listed.** `unicorn/no-new-array` (8 sites) → the hot-path sites (`hashline-anchor.ts` ×2, `word-index.ts`, `word-index-store.ts`, `runtime-agent-end.ts`, `module-report.ts`'s Levenshtein DP ×2) use the lint-clean sparse form (`const a: T[] = []; a.length = n;`) rather than `Array.from({length})`, which measurably regresses the per-call cost on `computeHashlineAnchors` (~1.06ms parity with the pre-fix sparse baseline vs. `Array.from`'s ~1.16ms at n=2000 lines); `clients/lsp/aggregation.ts`'s site keeps its original `.fill(undefined)` semantics (dense, not sparse — `results` is read via `.filter((r) => r !== undefined)` and per-index equality checks that don't care about sparse-vs-dense, but explicit fill was the pre-existing contract and there was no reason to weaken it). `unicorn/prefer-string-starts-ends-with` (4 sites) → `String#startsWith`/`endsWith`, each checked for `m`/`i`-flag and anchor semantics that would break equivalence — none present. `unicorn/no-useless-spread` (6 sites, including the 2 in `clients/lsp/edits.ts` originally deferred as "under concurrent edit for #2450" — #2450's PR #2459 does not touch `edits.ts`, so resolved here) → direct `for...of` iteration; `Map`/`Set` iterators reflect deletions safely (deleting the current or a not-yet-visited entry during iteration neither skips nor duplicates any other member), including the one site where the mutating call (`flushUri` → `removeIndex`) deletes from the exact `Set` being iterated. `unicorn/no-useless-fallback-in-spread` (7 sites) → drop the `?? {}`. `unicorn/no-useless-length-check` (1 site) → drop the redundant `.length === 0` (`Array#every` already returns `true` on empty). `eslint/no-unused-expressions` (2 sites) → `cond && call()` to `if (cond) call()`. `eslint/no-unused-vars` (1 site) → array-hole skip on an unused destructured element. `eslint/no-control-regex` (4 genuine control-byte matches: ANSI-escape stripping in `clients/sanitize.ts`'s 2 constants, a NUL-byte telemetry scrub in `tools/ast-grep-search.ts`'s 2 sites — those 2 were re-verified as stale-lint false positives under the pinned binary and had their `oxlint-disable` comments removed rather than kept) → the 2 real ANSI-stripping duplicates (`clients/bash-file-access.ts`, `clients/dispatch/runners/utils/diagnostic-parsers.ts`) now route through `clients/sanitize.ts`'s exported `stripAnsi`, deleting both private/inline copies and their suppressions; the shared helper's CSI pattern was widened to the broader ECMA-48 grammar (`bash-file-access.ts`'s pattern, which also matches private-mode `?` sequences and CSI intermediate bytes) so nothing narrows on the merge.
|
|
603
|
+
|
|
604
|
+
`unicorn/no-new-array`'s overlap with ast-grep's broader `no-array-constructor` rule, and the disjoint `prefer-string-starts-ends-with` rules in oxlint vs. ast-grep, are recorded once in AGENTS.md's #1718 anti-slop catalog section rather than only in this PR body; the `LINTER_OVERLAP` gap (`clients/dispatch/runners/ast-grep-napi.ts` only checks `hasEslintConfig`, not `hasOxlintConfig`) is pre-existing and tracked by #2462, not fixed here (it lives in the dispatcher, not `lint:js`'s scope).
|
|
605
|
+
|
|
606
|
+
Review round 2: a third private ANSI stripper (`clients/formatters.ts`'s `firstDiagnosticLine`, built via `String.fromCharCode(27)` specifically to dodge `no-control-regex` rather than declare it) had been missed by the round-1 sweep — it fed user-visible formatter-failure summaries and, being a narrower `[A-Za-z]`-final-byte pattern than the shared helper, left private-mode CSI sequences (e.g. a terminal cursor-hide, `\x1b[?25l`) in the surfaced diagnostic. Routed through `clients/sanitize.ts`'s `stripAnsi`, deleting the `fromCharCode` dodge; a repo-wide re-grep for `fromCharCode(27)`/`\x1b[` strippers outside `sanitize.ts` found no further instances (`tools/render-compact.ts`'s `\x1b[` usage emits brand-color escapes, it does not strip). Also added: a mutation-proved regression test pinning the `lint:js` npm-script's target scope (a narrowed target, e.g. `scripts` instead of `.`, previously stayed green against the existing "passes clean" assertion alone) and three mutation-proved `stripAnsi` cases (`\x1b[?25l`, `\x1b[1;2 q`, `\x1b[1@x`) that discriminate the widened ECMA-48 CSI grammar from the narrower pre-widening pattern.
|
|
607
|
+
|
|
608
|
+
- **Session-state sweep detector no longer hard-codes container class names, no longer gates on a `clear()`/`delete()` method name, and no longer misses an exported container (refs #2442, #2455)** — `tests/support/session-state-scan.ts` originally recognised module-level state only when bound to `new (Map|Set|WeakMap|WeakSet|PathKeyedMap|BoundedFifoMap|BoundedLruCache)`. Migrating `cache-observability.ts`'s maps to `BoundedFifoMap` (#2442) had already proven the failure mode once — a file drops out of the sweep silently until the new wrapper's name is added to the alternation by hand. A first pass replaced the alternation with a live scan for any exported class owning a `clear()`/`delete()` method, resolved directly or through an `extends` chain — but that method-name gate was itself a miss: `FactStore`'s own clear methods are named `clearAll`/`deleteFileFact`, so two of its five production module-scope instances (`dispatch/integration.ts`, `mcp/analyze.ts`) stayed invisible, and a non-exported class (`mcp/session.ts`'s `TurnEndQueue`) was unreachable by the `export class` regex regardless of method names. The detector now uses the issue's own primary wording directly: a module-level `const`/`let` bound to `new <Ctor>(...)` is flagged when `<Ctor>` is `Map`/`Set`/`WeakMap`/`WeakSet` or ANY class declared anywhere in `clients/`, in every export shape (`export class`, `export default class`, `export abstract class`, a bare non-exported `class` later re-exported via `export { A }` / `export { A as B }`) — which also deletes the now-unneeded `extends`-chain walk and its cycle guard. The declaration itself may now carry an `export` prefix too: whether a module-scope singleton is exported says nothing about whether it holds session state, and `export const goClient = new GoClient()` — the process's only Go availability latch — was invisible for no reason but the keyword in front of it. A class that structurally qualifies but is proven NOT to hold session state gets a documented `CONTAINER_CLASS_EXCLUSIONS` entry instead of a special case at the call site (empty today); both a stale entry (names a class the live scan no longer finds) and a reason-less entry now red on their own.
|
|
609
|
+
|
|
610
|
+
Re-pinned every `SESSION_STATE_SYMBOL_COUNTS` row the widened detection now sees: `dispatch/integration.ts` 8 → 11 (`sessionRunnerRegistry`, an import-time-built lookup with no session lifetime; `sessionFacts`, a `FactStore` already registered and reset; and the exported `CASCADE_GRAPH_KINDS` vocabulary), `widget-state.ts` 3 → 5 (`WriteOrderingGuard` instances already cleared by `clearWidgetState`), `mcp/analyze.ts` 1 → 2 (`warmGraphFacts`, a `FactStore` deliberately reused across calls so `buildOrUpdateGraph`'s incremental tiers keep their instance-keyed reuse (#536), whose entity-snapshot diff self-heals on eviction — exempted with a named cross-project collision follow-up, #2477; it is NOT exempt for want of a session boundary, since `mcp/session.ts`'s `runSessionStart` does call `handleSessionStart`), `mcp/session.ts` 2 → 3 (`turnEndQueue`, already reset by the file's own `_resetTurnEndChain`), and `formatters.ts` 8 → 9 (the exported `FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK` vocabulary). Two files are newly registered, `go-client.ts` and `rust-client.ts` — found by hand while auditing what the widened predicate should have covered, NOT surfaced by the widening itself. The sweep skips any file that exports no reset at all, and none of these files exported one, so no amount of widening the container predicate could have yielded them; they became visible only because this change ADDED `resetGoAvailability`/`resetRustAvailability`, so the fix preceded the detection and the pair-with-reset blind spot is unchanged. The bug the audit found is real either way: the module-scope `GoClient`/`RustClient` singletons each hold their own `createAvailabilityLatch()` outside every generation counter that already covers the go-vet and rust-clippy runners' other caches, and a probe-class "missing" verdict never expires, so a go/cargo toolchain installed mid-process stayed invisible until process restart — the same #1496/#1535 shape `resetZizmorTokenAvailability` already covers for `gh auth token`. Both resets are wired into `handleSessionStart` beside `resetZizmorTokenAvailability`, and both now live in the client module that owns the class beside the process's ONE instance of it: `bootstrap.ts` used to build a SECOND `GoClient`/`RustClient` for the object `handleSessionStart` reads for its "Active tools" line, so the reset re-armed a latch nothing user-visible consulted and the bug survived its own fix. The shared `createToolchainAvailability` seam under them carries a session generation through the repo's `clients/generation-guard.ts` primitive (#1754), so a probe still in flight when a session boundary lands is neither joined by post-reset callers nor able to write its stale verdict — or a path the reset was called because it disappeared — back into the cleared latch when it settles, and every write it drops is recorded as a bounded `generation-guard-stale-write` degradation instead of vanishing silently.
|
|
611
|
+
|
|
612
|
+
- **Analyse every observed (unclassified-tool) mutation — the tool's FIRST call included — and apply the post-pipeline post-conditions on that path (#2464)** — `clients/runtime-tool-result.ts` gave an observed mutation no pipeline lint/diagnostics dispatch at all: the `runPipeline` call lived inside the block that also writes the duplicate `turn-state.json` range and the duplicate change-log receipt the observed early return exists to suppress. It is now factored into `dispatchPipelineAnalysis`, which the classified chain and the observed path both call — analysis without a second recording. Three further defects on that path are fixed in the same change: the observed block sat BELOW the `mutation === undefined` gate, so call ONE of every unknown tool — the call the observational net exists for — was recorded by the mutation bridge and returned unanalysed (the `agent_settled` drain fixed and formatted the file, but nothing ever linted it); the block discarded the pipeline `result`, so the read-guard staleness re-stamp over `result.changedFiles`, the #2402 after-write hash stamp and the already-analysed latch never ran — under `--immediate-format` the pipeline rewrote the file and the agent's next edit was blocked with a spurious `file_modified`; and a pipeline crash was swallowed into a debug line instead of returning the crash notice the classified path returns. All three post-conditions now live inside `dispatchPipelineAnalysis`, so every caller gets them by construction, and `toolResultStart` is hoisted above both provenance exits so `edit_batch_summary.durationMs` is comparable across paths.
|
|
613
|
+
|
|
614
|
+
Red on pre-fix `clients/runtime-tool-result.ts` (checked out at `f55bea7d`, tests kept at fix state), verbatim from `npm run test:targeted -- tests/clients/observed-mutation-integration.test.ts`: `analyses EVERY call of an unknown tool, first one included` — `AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times` at `tests/clients/observed-mutation-integration.test.ts:735:36`; `applies the post-pipeline post-conditions on the observed path too` (driven through call TWO, which already dispatched pre-round, so the red isolates the missing post-conditions) — `AssertionError: expected "vi.fn()" to be called with arguments: [ Array(1) ]` / `Number of calls: 0` at `:884:26`; `surfaces a pipeline crash on the observed path the way the classified path does` (also call two) — `AssertionError: expected undefined to be true` at `:959:29`.
|
|
615
|
+
|
|
616
|
+
Review round 3 closes a concurrency regression the round-2 dispatch introduced: the observed call site consulted NEITHER of the two pre-conditions the classified chain has applied since #1086, so two concurrent observed `tool_result`s for one file+hash both registered and both ran `runPipeline` — under `--immediate-format`, two autofix/format writers racing on one file. The second registration also overwrote the first inside the per-file map, so the first pipeline's release emptied that map and deleted the OUTER registry entry; a classified pipeline that started afterwards re-created the entry under a fresh map, and the second observed pipeline's release — holding a stale reference to the first map — deleted the outer entry again, evicting a live, unrelated registration whose concurrent duplicates then stopped deduping. Both checks now live in one shared `claimPipelineDispatch` consulted by both call sites at the classified chain's existing position (above `addModifiedRange`/`recordProjectChange`, so a duplicate state still writes no turn-state range and no change-log receipt), and `releaseInFlightPipeline` deletes the outer entry only when the outer map still IS the map that registration went into.
|
|
617
|
+
|
|
618
|
+
Round-3 reds, verbatim from `npm run test:targeted -- tests/clients/observed-mutation-integration.test.ts`. Against pre-fix `clients/runtime-tool-result.ts` (checked out at `be8d3cd6`, tests kept at fix state): `dispatches ONE pipeline for two concurrent observed tool_results on the same state` — `AssertionError: expected 2 to be 1 // Object.is equality` at `:1024:31`; `keeps a later classified pipeline deduping after two observed calls settle` — `AssertionError: expected [ [ 'patch_file', …(1) ], …(2) ] to deeply equal [ [ 'patch_file', …(1) ], …(1) ]` at `:1126:6` (a second `patch_file` dispatch at `writeIndex: 2`); `does not re-analyse an observed mutation whose state a classified call already analysed` — `AssertionError: expected "vi.fn()" to be called 1 times, but got 2 times` at `:1194:35`. Two further reds by mutation of the fixed source: deleting the `inFlightPipelines.get(filePath) === registered` conjunct — `never evicts a live registration when a stale release names the same file` fails `AssertionError: expected true to be false` at `:1268:25`; neutralizing the membership guard to `pathsEqual(candidate, candidate)` — `never dispatches the directory an unknown directory-target tool named` fails `AssertionError: expected [ Array(1) ] to not include 'C:\Users\R3LiC\AppData\Local\Temp\pi-…'` at `:1317:32`.
|
|
619
|
+
|
|
620
|
+
- **rustfmt now carries the Cargo package edition (closes #2466)** — rustfmt
|
|
621
|
+
was invoked bare (`rustfmt <file>`), so it parsed under its own default
|
|
622
|
+
edition instead of the file's actual Cargo package edition and could reject
|
|
623
|
+
valid newer-edition syntax (e.g. Rust 2024). The nearest package's
|
|
624
|
+
`[package] edition` (or its inherited `[workspace.package] edition` when the
|
|
625
|
+
package declares `edition.workspace = true`) is now resolved and passed
|
|
626
|
+
through `--edition`.
|
|
627
|
+
|
|
628
|
+
- **ktfmt now carries the project's Gradle `ktfmt { }` style selection (closes #2468)** — ktfmt was invoked bare (`ktfmt <file>`), so it formatted
|
|
629
|
+
under its own default style instead of the project's actual
|
|
630
|
+
`googleStyle()`/`kotlinLangStyle()` selection in `build.gradle(.kts)`'s
|
|
631
|
+
`ktfmt { }` extension block. The declared style is now resolved and passed
|
|
632
|
+
through `--google-style`/`--kotlinlang-style`, scoped the way Gradle scopes
|
|
633
|
+
the block that declares it: a `subprojects { }` style reaches the modules
|
|
634
|
+
and not the root project's own sources, an `allprojects { }` one reaches
|
|
635
|
+
both, a top-level one reaches only the project declaring it, and any other
|
|
636
|
+
enclosing block — `project(":app") { }`, `configure(…) { }`, a convention
|
|
637
|
+
wrapper — reaches neither. The search continues up the ancestors when the
|
|
638
|
+
module's own build file declares no style that applies to it — the
|
|
639
|
+
multi-module layout where the root build file holds the style and each
|
|
640
|
+
module declares only its plugins. A nearer declaration still wins over an
|
|
641
|
+
ancestor's, and a body calling both style functions resolves to the last
|
|
642
|
+
call, as Gradle does. An unreadable/unparseable build file, no declared
|
|
643
|
+
style, a style pi-lens cannot scope, or the ktfmt-gradle plugin's removed
|
|
644
|
+
`dropboxStyle()` falls back to the previous bare invocation unchanged.
|
|
645
|
+
|
|
646
|
+
- **Wait for the persisted snapshot instead of racing it in `word-index-lifecycle.test.ts`'s absent-snapshot test (closes #2471)** — `tests/clients/word-index-lifecycle.test.ts:216`'s "builds and persists when no snapshot exists yet (absent)" test read `loadProjectSnapshot(env.tmpDir)` bare, immediately after `handleSessionStart` resolved and a `vi.waitFor` on `runtime.wordIndex` (in-memory, no I/O) passed — the ONLY `loadProjectSnapshot(` read in the file not wrapped in `vi.waitFor`/a persist-wait helper (the other three, at what were then `:295`, `:389`, `:464`, already are). It flaked on CI for PR #2456 (`AssertionError: expected undefined to be defined` at `:217`) and carries two closed sibling issues for the same class of race (#2108, #2142). The read is now wrapped in the same `vi.waitFor(() => { const snapshot = loadProjectSnapshot(...); expect(snapshot?.wordIndex).toBeDefined(); })` shape the quick-mode warmup sibling test already uses, polling until the async promotion actually lands instead of reading once on a lucky scheduling. Reproduced red-first by reusing the file's own #2142 test seam (`deferredRuntimeSnapshotSave`) to force the word-index-carrying promotion save ~1200ms after `handleSessionStart` resolves: `AssertionError: expected undefined to be defined` at the bare read, quoted verbatim, matching the CI failure shape exactly. The forced delay is kept in the fixed test permanently (not removed after proving red) so future coverage exercises the wait deterministically rather than depending on CI's scheduler ever reproducing the race again — the same "force it, don't hope for it" shape as #2293/PR #2463.
|
|
647
|
+
|
|
648
|
+
Added a static self-scanning guard test to the same file (`describe("static guard: no bare loadProjectSnapshot read after an async build (#2471)")`) that bracket-matches every `vi.waitFor(...)` call and walks back from every other `loadProjectSnapshot(` call site for a persist-wait helper with no un-awaited gap since; it fails loudly (verified against a deliberately reverted bare read) if a future edit reintroduces a bare read in this file, rather than depending on a human noticing during review.
|
|
649
|
+
|
|
650
|
+
Class sweep of every `loadProjectSnapshot(` read in `tests/` that follows an async build: `tests/tools/symbol-search.test.ts:114` (waited, `vi.waitFor`), `tests/clients/reverse-deps.test.ts:196,256` (not-async — direct synchronous `saveProjectSnapshot`/`saveRuntimeProjectSnapshot` calls in the same test body, no background task in between, so the in-process authoritative-snapshot cache makes the immediate read correct by construction), `tests/clients/runtime-session-retro-hydrate-warmup-race.test.ts:257,338` (waited, `vi.waitFor`), `tests/clients/word-index-lifecycle.test.ts` other three sites (waited, as above), `tests/clients/word-index.test.ts` six sites (five waited via `vi.waitFor`; the sixth, `:718`, follows a synchronous refusal path with no build ever scheduled — asserts absence, not a race), `tests/clients/project-snapshot.test.ts` (not-async for the save/load-semantics block, which forces `PI_LENS_SNAPSHOT_PERSIST_SYNC=1` specifically so no async drain is needed; the dedicated "worker persist" describe block below deliberately tests read-your-own-write correctness against the in-process authoritative cache while the async disk persist is still in flight — the intended behavior under test, not a race to fix), `tests/clients/project-snapshot-bounded-caches.test.ts` (no call — only a comment mentions the symbol name). No other file in `tests/` needed a wrap.
|
|
651
|
+
|
|
652
|
+
- **php-cs-fixer now carries its detected ancestor config into `--config` (closes #2472)** — php-cs-fixer's own `ConfigurationResolver` does not walk
|
|
653
|
+
up parent directories looking for `.php-cs-fixer.php`/`.php-cs-fixer.dist.php`
|
|
654
|
+
the way prettier/biome/eslint do, and pi-lens spawns it with the FORMATTED
|
|
655
|
+
FILE's own directory as its working directory — so a config found several
|
|
656
|
+
directories above (the common project-root layout) was invisible to the
|
|
657
|
+
actual `fix` invocation, and php-cs-fixer silently fell back to its
|
|
658
|
+
built-in default ruleset instead of the project's configured one. The
|
|
659
|
+
nearest ancestor config is now resolved and passed explicitly via
|
|
660
|
+
`--config <path>`, with `.php-cs-fixer.php` preferred over
|
|
661
|
+
`.php-cs-fixer.dist.php` in the same directory (matching php-cs-fixer's
|
|
662
|
+
own precedence) — including when the config sits in the file's own
|
|
663
|
+
directory, so correctness no longer depends on php-cs-fixer's own
|
|
664
|
+
(nonexistent) upward search.
|
|
665
|
+
|
|
666
|
+
- **Monorepo module-graph crate names are now read from `[package]` only (closes #2473)** — `clients/review-graph/workspace-modules.ts`'s Cargo.toml
|
|
667
|
+
reader for the cascade module graph scanned the WHOLE manifest for the
|
|
668
|
+
first `name = "..."` line, regardless of which TOML table it fell under. A
|
|
669
|
+
member crate whose `name` key appeared under a table other than
|
|
670
|
+
`[package]` earlier in the file — a `[[bin]] name = "..."` entry or a
|
|
671
|
+
`[package.metadata.*]` block preceding `[package]` — was misread as that
|
|
672
|
+
crate's name, silently mislabeling it in the module graph and cascade
|
|
673
|
+
downstream analysis. The reader is folded onto `clients/cargo-manifest.ts`
|
|
674
|
+
(#2466)'s shared, table-scoped parser (`readCargoPackageName`/
|
|
675
|
+
`readCargoWorkspaceMembers`/`readCargoDependencyNames`), fixing the
|
|
676
|
+
`[package] name` lookup to read only the `[package]` table. Workspace
|
|
677
|
+
`members` array and `[dependencies]` name extraction are unchanged
|
|
678
|
+
(already table-scoped or with no realistic table collision) — pinned by a
|
|
679
|
+
golden fixture (`tests/fixtures/cargo-modules-snapshot.json`). On this
|
|
680
|
+
fold's own fixture set the diff against the pre-fix baseline was exactly
|
|
681
|
+
the two misread-name rows; review round 2 added adversarial fixtures that
|
|
682
|
+
surfaced two FURTHER pre-existing defects also visible in that same golden
|
|
683
|
+
diff against `master` — see below, and do not read "exactly the two rows"
|
|
684
|
+
as a claim about the final committed golden, which changed four rows in
|
|
685
|
+
total.
|
|
686
|
+
|
|
687
|
+
Review round 2 hardened the fold itself, which had regressed four cases the
|
|
688
|
+
pre-fold, per-line reader handled correctly: (1) `parseTomlStringArray`
|
|
689
|
+
harvested a quoted string from a COMMENTED-OUT array entry (`# "member",`)
|
|
690
|
+
because its single multi-line regex scan never stripped comments — a
|
|
691
|
+
commented-out workspace member whose crate directory still exists on disk
|
|
692
|
+
wrongly entered the module graph, and `clients/lsp/server.ts`'s rust-analyzer
|
|
693
|
+
`exclude`/`members` reads shared the same bug; (2) `extractTomlTableSection`
|
|
694
|
+
anchored its heading and terminator regexes at column 0, so a validly
|
|
695
|
+
INDENTED `[package]` heading was never read (crate silently dropped — a
|
|
696
|
+
single-member workspace resolved to `null`) and an indented sub-table
|
|
697
|
+
heading (` [dependencies.tokio]`) never terminated the parent table,
|
|
698
|
+
leaking the sub-table's own keys in as bogus dependency names — the SAME
|
|
699
|
+
`[ \t]*` anchor fix also makes a CRLF manifest read correctly, since
|
|
700
|
+
ECMAScript's multiline `$`/`^` already treat a bare `\r` as a line
|
|
701
|
+
terminator on its own; the fold additionally runs every manifest through a
|
|
702
|
+
CRLF→LF normalize before any regex runs, kept as defensive
|
|
703
|
+
belt-and-braces, not because the anchor fix leaves a real CRLF match
|
|
704
|
+
failure (review round 3 correction — verified by mutation: removing the
|
|
705
|
+
CRLF→LF pass leaves every test green); (3) `detectWorkspaceType` still did
|
|
706
|
+
a bare `content.includes("[workspace]")`, true for a commented-out
|
|
707
|
+
`# [workspace]` heading — a Cargo.toml with only a commented `[workspace]`
|
|
708
|
+
sitting next to a REAL npm/pnpm workspace was misclassified as an (empty)
|
|
709
|
+
cargo workspace, resolving to `null` instead of the real workspace; (4)
|
|
710
|
+
`readCargoWorkspaceMembers` read `members` unscoped, and
|
|
711
|
+
`clients/lsp/server.ts` hand-composed its own
|
|
712
|
+
`extractTomlTableSection`/`parseTomlStringArray` pair for `members`/
|
|
713
|
+
`exclude` instead of reusing it — both readers now go through
|
|
714
|
+
`[workspace]`-scoped `readCargoWorkspaceMembers`/`readCargoWorkspaceExclude`.
|
|
715
|
+
Regenerating the golden against this round's fixtures also surfaced a
|
|
716
|
+
THIRD pre-existing defect in master's OLD (pre-#2473) reader, not
|
|
717
|
+
introduced by the fold: `extractTomlSection` required an EXACT trimmed
|
|
718
|
+
`"[dependencies]"` line match, so a trailing comment on the table heading
|
|
719
|
+
(`[dependencies] # direct deps`) made it silently return zero dependencies
|
|
720
|
+
— fixed for free by the table-scoped, comment-tolerant heading regex; this
|
|
721
|
+
is one of the two rows noted above as changing beyond the original two
|
|
722
|
+
misread-name rows (the other being `detectWorkspaceType`'s
|
|
723
|
+
commented-heading fix, (3) above). All fixes are pinned by new adversarial
|
|
724
|
+
fixtures under `tests/fixtures/cargo-workspace-modules/adv-*` (commented
|
|
725
|
+
member, indented `[package]`, indented sub-table, CRLF, a trailing comment
|
|
726
|
+
on a table heading, and a commented-out `[workspace]` heading next to a
|
|
727
|
+
real npm workspace), direct unit tests in `tests/clients/cargo-manifest.
|
|
728
|
+
test.ts`, and regression tests on the `clients/lsp/server.ts` consumer in
|
|
729
|
+
`tests/clients/lsp/server-policy.test.ts`.
|
|
730
|
+
|
|
731
|
+
Review round 3 fixed a second sentinel-ambiguity defect: `extractTomlTableSection`
|
|
732
|
+
returned `""` for BOTH "table absent" and "table present but empty" (e.g. a
|
|
733
|
+
bare `[workspace]` heading as the last line of the file with no trailing
|
|
734
|
+
newline, or `[workspace] # root`/`[workspace] ` at EOF) — three call
|
|
735
|
+
sites relied on a `!== ""` sentinel to mean "table present": review round
|
|
736
|
+
2's `detectWorkspaceType` fix above (misclassified a root crate's own
|
|
737
|
+
Cargo.toml as an npm workspace) and both of `resolveCargoPackageEdition`'s
|
|
738
|
+
"is this manifest also the workspace root" checks, which predate #2473
|
|
739
|
+
entirely (#2466 — silently fell back to no `--edition` instead of
|
|
740
|
+
resolving the inherited value). `extractTomlTableSection` now returns
|
|
741
|
+
`string | undefined` (`undefined` for absent, `""` for present-but-empty)
|
|
742
|
+
and all three call sites compare `!== undefined`.
|
|
743
|
+
`readCargoDependencyNames` also dropped a redundant, non-quote-aware
|
|
744
|
+
`line.split("#", 1)[0]` re-strip — its input is already comment-stripped by
|
|
745
|
+
`normalizeToml`. Pinned by a new adversarial fixture
|
|
746
|
+
(`adv-i-empty-workspace-eof`), a `resolveCargoPackageEdition` regression
|
|
747
|
+
test, and the golden snapshot.
|
|
748
|
+
|
|
749
|
+
- **Project-config discovery with no config file anywhere is bearing-scoped, not $HOME-wide (refs #2426, closes #2483)** —
|
|
750
|
+
`bearingDirMtimes` in `clients/project-lens-config.ts` scopes a discovery
|
|
751
|
+
cache entry's directory-mtime freshness to the BEARING chain — `startDir` up
|
|
752
|
+
to and including the directory that supplied the winning config file — when
|
|
753
|
+
a config file is found (warm load = 3 stats, unrelated ancestor churn
|
|
754
|
+
ignored, per #2426 round 5, F-C). When no config is found anywhere — the
|
|
755
|
+
majority case for most projects — the fallback returned the FULL chain to
|
|
756
|
+
the `$HOME` ceiling, so an unrelated mtime change in any ancestor (`~/Desktop`,
|
|
757
|
+
`~/projects`) invalidated the entry and forced a full re-walk, re-read and
|
|
758
|
+
re-parse of every legacy document, on every subsequent load. Live evidence
|
|
759
|
+
from the #2456 round-5 review: warm = 6 stats, after ancestor churn = 38.
|
|
760
|
+
Fix: when no config is found, freshness is scoped to `startDir` itself —
|
|
761
|
+
the nearest directory a config file COULD have appeared in, the same
|
|
762
|
+
bearing-directory principle the presence case already applies (a directory
|
|
763
|
+
is always its own degenerate bearing chain). A config file created directly
|
|
764
|
+
in `startDir` bumps its own mtime and is found on the very next load.
|
|
765
|
+
**Round 2**: a file created further up the chain moves nothing `startDir`-scoped
|
|
766
|
+
freshness tracks, and — with no production caller of
|
|
767
|
+
`resetProjectLensConfigCache` and a same-`startDir` load never rediscovering
|
|
768
|
+
— that left staleness unbounded, violating the issue's own acceptance line
|
|
769
|
+
("a new config file created in the chain IS noticed (bounded staleness
|
|
770
|
+
stated)"). Fixed by reusing `file-utils.ts`'s existing negative-cache
|
|
771
|
+
cadence bound (`FRESHNESS_CADENCE_MS`, extracted to the shared leaf
|
|
772
|
+
`clients/freshness-cadence.ts` so both modules read one value without a
|
|
773
|
+
`clients/` import cycle): a no-config discovery entry force-expires at
|
|
774
|
+
most once per 2s cadence window regardless of `startDir`'s own mtime,
|
|
775
|
+
which sends the next load through a full ancestor re-walk — so a config
|
|
776
|
+
created anywhere in the chain is noticed within one cadence window, not
|
|
777
|
+
never.
|
|
778
|
+
Regression tests added to `tests/clients/config-discovery-cost.test.ts`
|
|
779
|
+
alongside the existing presence-case coverage: 500 warm loads with no
|
|
780
|
+
config found cost a constant 1 stat each (proven red pre-fix at
|
|
781
|
+
`[8, 54]`/`[54, 8]`, matching the issue's reported growth), ancestor churn
|
|
782
|
+
above `startDir` does not re-walk (proven red pre-fix at `warm=6` /
|
|
783
|
+
`after=38`, matching the issue's own probe numbers exactly), a config file
|
|
784
|
+
newly created directly in `startDir` is still noticed, and (round 2) a
|
|
785
|
+
config file created two levels above `startDir` is not noticed within the
|
|
786
|
+
cadence window but is noticed once it elapses.
|
|
787
|
+
|
|
788
|
+
- **Rust workspace-root hoisting reads `[workspace]` presence through the shared Cargo.toml reader, not a second hand-rolled regex (closes #2498)** —
|
|
789
|
+
`clients/lsp/server.ts`'s `RustWorkspaceRoot` walk-up carried
|
|
790
|
+
`/^\s*\[workspace\]/m.test(parentCargoContent)` four lines above
|
|
791
|
+
`cargoWorkspaceDeclaresMember`, which #2480 had already converted to
|
|
792
|
+
`clients/cargo-manifest.ts`'s shared, table-scoped readers — a second regex
|
|
793
|
+
TOML reader outside the one file AGENTS.md names as the single source of
|
|
794
|
+
truth for Cargo.toml parsing. Behaviorally identical to
|
|
795
|
+
`extractTomlTableSection(content, "workspace") !== undefined` under every
|
|
796
|
+
probed shape (comments, indentation, CRLF, trailing content on the
|
|
797
|
+
heading), so no runtime behavior changes. New export
|
|
798
|
+
`hasCargoWorkspaceTable(content)` in `clients/cargo-manifest.ts` names the
|
|
799
|
+
presence check; `clients/lsp/server.ts`, both of
|
|
800
|
+
`resolveCargoPackageEdition`'s own "is this manifest also the workspace
|
|
801
|
+
root" checks, and `clients/review-graph/workspace-modules.ts`'s
|
|
802
|
+
`detectWorkspaceType` (round 2 of this PR — it was still calling
|
|
803
|
+
`extractTomlTableSection(content, "workspace") !== undefined` directly)
|
|
804
|
+
now call it instead of repeating the `!== undefined` idiom.
|
|
805
|
+
A new sweep (`tests/clients/cargo-manifest.test.ts`) fails the build if a
|
|
806
|
+
future edit reintroduces a hand-rolled `\[workspace\]` regex anywhere in
|
|
807
|
+
`clients/` outside `cargo-manifest.ts` — proven to actually fire via a
|
|
808
|
+
mutation check against the exact pre-fix defect shape before trusting it
|
|
809
|
+
against the real tree.
|
|
810
|
+
|
|
811
|
+
- **`tests/support/sweep-kit.ts`'s `stripSource` tracks `${` nesting depth in template literals, including nested templates inside the expression (closes #2502)** — the lexer had no notion of a template's `${...}` interpolation at all; it treated the whole span between backticks as opaque text, so the first backtick encountered anywhere inside an interpolation was read as the outer template's own close. A NESTED template inside the interpolation (`` `x ${cond ? `y(` : `z`} w` ``) therefore closed the outer template early, leaving the nested template's own stray delimiters — including any `(`/`)` in its text — to fall through unmasked into the rest of the file. Named output from #2487 round-4 review (probe P1c): that leaked, unblanked `(` corrupts `tests/config/sync-child-process-timeout.test.ts`'s `callArguments` depth scan over `masked` — the scan never returns to depth 0 before the call's real closing paren, runs past it, and can absorb an unrelated LATER call's `timeout:`, silently reclassifying a genuinely unbounded synchronous spawn as bounded. Fixed with a stack of currently-open template literals, each tracking its own `${...}` brace depth, so the closing backtick that actually matches is the one found — nested templates, strings, comments and regexes inside an interpolation are now lexed by the same state machine rather than treated as string content.
|
|
812
|
+
- This required lexing `${...}` interpolations as real code rather than opaque text, which also fixes a `stripSource`-documented false negative: under `strings: "blank"`, a call written inside a template expression (`` `${resetThing()}` ``) used to be blanked along with the rest of the template and was invisible to a reachability walk; it is now visible like any other code, on either string policy.
|
|
813
|
+
- Round-trip regression caught by the kit's own governance suite before push: an early version of this fix kept blanking plain expression code (to imitate the old opaque-template behavior under `strings: "blank"`), which corrupted `regexMayStart`'s backward scan — it walks the already-mutated output array to see past neutralized comments/strings, and tunneled through the newly-blanked identifiers of `clients/safe-spawn.ts`'s `arg.replace(/"/g, '""')` (a real nested-interpolation-adjacent regex, `reason: `${failure.kind}${options.cwd ? ` in ${options.cwd}` : ""}`\`` two lines below it) straight back to the template's own opening backtick, misclassifying the following `/` as division and silently dropping the rest of the file from every downstream sweep. Leaving expression code genuinely unblanked (matching top-level code) fixed it; `sync-child-process-timeout.test.ts`'s own family-count floor (`sites.length >= 5`) caught the regression locally before it reached CI.
|
|
814
|
+
- `sync-child-process-timeout.test.ts`'s `callArguments` doc comment (the #2487 round-4 "a paren inside a string or comment cannot perturb the depth count" claim) now also covers nested-template delimiters, which is what this fix closes.
|
|
815
|
+
|
|
816
|
+
- **A formatter installed only at your home directory is no longer used as a project's formatter (closes #2514)** — the ancestor walk that looks for a
|
|
817
|
+
project-local `node_modules/.bin`, `vendor/bin` or `.venv`/`venv` binary
|
|
818
|
+
climbed all the way to the filesystem root with no ceiling, so a stray
|
|
819
|
+
`~/node_modules/.bin/<tool>.cmd` (for example one a home-level manifest
|
|
820
|
+
installs) could be picked up as the current project's local install even
|
|
821
|
+
though it has nothing to do with the project. The walk now stops at (or
|
|
822
|
+
above) your home directory, the same ceiling project-root discovery already
|
|
823
|
+
applies — a project's own bin still resolves normally as long as it sits
|
|
824
|
+
below your home directory. On Windows the ceiling also now recognises your
|
|
825
|
+
home directory when the path is spelled with the other drive-letter case
|
|
826
|
+
(`c:\Users\…` as VS Code reports it), which previously let the walk climb
|
|
827
|
+
straight past it; the same fix applies to every other search that stops at
|
|
828
|
+
your home directory, including project-root and tool-config discovery. The
|
|
829
|
+
ceiling now covers every project-local tool-bin lookup, not just the
|
|
830
|
+
formatter one: Vite+'s `vp` for the oxlint runner, the ast-grep availability
|
|
831
|
+
sweep's project-local check, and Composer `vendor/bin` tools such as
|
|
832
|
+
phpstan — all of which previously climbed past your home directory. The
|
|
833
|
+
ast-grep availability sweep also checks pi-lens's OWN install directory
|
|
834
|
+
(deliberately NOT ceilinged there — a `pi install npm:pi-lens` install can
|
|
835
|
+
legitimately sit at your home directory itself), so pi-lens's bundled
|
|
836
|
+
`@ast-grep/cli` keeps resolving regardless of where pi-lens itself is
|
|
837
|
+
installed. On Windows, a virtualenv that somehow contains BOTH layouts
|
|
838
|
+
(ruff/sqlfluff) now prefers the runnable `Scripts\<tool>.exe` over
|
|
839
|
+
`bin\<tool>` consistently everywhere. The Python venv lookups behind
|
|
840
|
+
ruff/sqlfluff/vulture stay fixed lookups at the project directory, as
|
|
841
|
+
before — they do not check ancestor directories.
|
|
842
|
+
|
|
843
|
+
- **`prune-agent-worktrees.test.ts`'s R2 gitlink-corruption case no longer races a wall clock (closes #2519), and the SubagentStop orphan sweep's `restrictToPath` scoping is now covered end to end (closes #2501)** — the "tells an unreadable recheck apart from a genuinely dirty one" case used to time a `.git` gitlink corruption at a fixed 150ms wall-clock delay against an artificial 400ms `snapshotProcesses` delay, betting that 150ms was always enough for enrichment's own synchronous `isDirty()` call to have already returned. Under load on Windows it wasn't: the corruption could land DURING enrichment instead of the pre-remove recheck, so the candidate took the enrichment-time `dirtyUnreadable` rail straight to `plan.keep` and never reached the removal loop — `hygiene.worktree-removed` was never written, and the test's second assertion failed against `undefined` (fails reproducibly on Windows under load, green on ubuntu CI and on an unloaded Windows box).
|
|
844
|
+
- Fixed by hooking the SAME module-substitution point review round 3, F1 uses, deterministically instead of by wall clock: the fixture's copy of `process-scan.mjs` now corrupts the gitlink synchronously at `snapshotProcesses`'s own entry, before its `await`. `snapshotProcesses` is only ever called from `readProcessTable`, itself only reached after every candidate's enrichment-time `isDirty()` call has already returned — both run in the same synchronous stretch of the event loop, an ordering JS's run-to-completion guarantee enforces regardless of machine speed. No race window remains to lose under load.
|
|
845
|
+
- Mechanism proof (not just "it compiles"): a throwaway probe injected an artificial 300ms delay before enrichment starts in a copy of the CLI, then ran the OLD wall-clock 150ms test body against it — it reproduced the exact reported failure (`run.keptReason: "status-unreadable"` via the enrichment rail, `hygiene.worktree-removed` record `undefined`). The SAME probe run against the new `patchRecheck` hook, under the identical artificial delay, passed with both records present and correctly attributed to the recheck rail.
|
|
846
|
+
- #2501: `restrictToPath: agentTree` in `main()` — the argument that scopes a SubagentStop's orphan sweep to the stopped agent's own worktree, never a sibling agent's (the #2435 contract) — had no end-to-end coverage; deleting it reds nothing (verified on PR #2493 head `3ac2bcdb`, 49 passed). A new Windows-only e2e case (`--dry-run --json`, so nothing is ever killed) spawns a genuinely orphaned `tests/fixtures/sleeper.mjs` helper under the agent's own tree and a second one under a sibling agent tree — via a short-lived detached-spawn launcher whose own exit is awaited synchronously, so the helper's parent pid is deterministically dead, no race — and asserts the plan's `orphans` array contains the agent-tree pid and never the sibling's. Proven red first: nulling `restrictToPath` in the real script makes both pids appear (`expected [ agentPid, siblingPid ] to not include siblingPid`); restored, the suite is green.
|
|
847
|
+
- Windows-only, deliberately, not as a flake gate: `selectOrphanFixtureProcesses` infers "the parent exited" from the parent's pid being absent from a process-table snapshot. On POSIX the kernel reparents an orphan to pid 1 (or the nearest subreaper) the instant its parent exits, so by the time any snapshot runs the orphan's ppid is already a LIVE pid there — neither helper would ever classify as an orphan on Linux/macOS, which would fail the assertion for a reason unrelated to `restrictToPath` and would stay silently blind to the SAME mutation (the exact vacuous-test shape #2501 itself warns against). `worktree-hygiene.mjs`'s own comment already documents the platform split ("Windows keeps the recorded parent pid after the parent dies"); this test follows the file's existing `it.skipIf(process.platform !== "win32")` precedent for the same reason.
|
|
848
|
+
- Also folds in the #2493 round 4 review comment (named explicitly by #2501): the `SubagentStop hook, end to end` describe block's hand-built repo/origin/worktree setup and its per-case hand patches (the process-scan module-substitution wrapper, the `node_modules` junction, the renamed real module) are now built once by a `createSubagentStopFixture()` builder returning `{ repo, worktree, origin, cli, ledgerDir, patchProcessScan(), patchRecheck(), addNodeModulesJunction(), cleanup() }`, so the platform assumptions baked into that setup (the `.gitignore` fold for `node_modules`, the module-substitution technique) live in one place. The F1 and R2 cases were migrated onto it; the other 15 cases in the block were left untouched (no behavior change).
|
|
849
|
+
|
|
850
|
+
- **Resource-sampler scanner timeouts are recorded under their own kind, not the orphan backstop's (closes #2524)** —
|
|
851
|
+
`terminateScannerChild` (`clients/instance-reaper.ts`) hardcoded
|
|
852
|
+
`kind: "orphan-backstop-scanner-escalated"` and a bare `"scan exceeded its
|
|
853
|
+
timeout"` reason, but has two callers with very different budgets and
|
|
854
|
+
cadences: the registry-independent orphan backstop (one scan per cooldown
|
|
855
|
+
window, `BACKSTOP_SCAN_TIMEOUT_MS` 5000ms) and `resource-sampler.ts`'s
|
|
856
|
+
process-table queries (`RESOURCE_SAMPLE_QUERY_TIMEOUT_MS` 2000ms, fired on
|
|
857
|
+
every heartbeat/spawn-bracket tick). Every sampler-path escalation was
|
|
858
|
+
therefore recorded under the backstop's kind — 8 rows observed live in one
|
|
859
|
+
session while `orphan-backstop.json`'s `lastSweepAt` proved the backstop had
|
|
860
|
+
not run (defect shape: a record's subject must be its producer). Fix:
|
|
861
|
+
`terminateScannerChild` now takes its `kind` and `timeoutMs` from an options
|
|
862
|
+
arg the caller supplies (no duplicated kill/verify machinery); the sampler's
|
|
863
|
+
two call sites now record the new `resource-sampler-scanner-escalated` kind
|
|
864
|
+
with its real 2000ms budget in the reason text, while the orphan backstop
|
|
865
|
+
keeps its own kind and 5000ms budget.
|
|
866
|
+
New kind `resource-sampler-scanner-escalated` is classified informational
|
|
867
|
+
(no `⚠` in `/lens-perf`'s degradation summary), not a warning like the
|
|
868
|
+
backstop's: `terminateScannerChild` fires the instant its caller's timer
|
|
869
|
+
elapses, strictly before `spawnCollectStdoutResult` (`clients/child-unref.ts`)
|
|
870
|
+
knows whether "close" or the timeout handler's own settle will win that
|
|
871
|
+
race — so it can and does fire on a query that goes on to settle
|
|
872
|
+
`status: "ok"`, matching the issue's observation of 8 escalations with no
|
|
873
|
+
`resource-sampler-query-failed` row alongside them. That absence is a
|
|
874
|
+
reliable "settled ok, not raced" signal only up to the FIRST genuine query
|
|
875
|
+
failure per subject: `recordQueryFailure` records through
|
|
876
|
+
`recordDegradationOnce`, which is once-per-kind-per-subject for the whole
|
|
877
|
+
session, so a second-and-later genuine timeout for the same subject adds no
|
|
878
|
+
further `resource-sampler-query-failed` row either — at that point the
|
|
879
|
+
discriminator can no longer tell "raced" from "actually failed again" from
|
|
880
|
+
the summary counts alone; the per-event NDJSON latency log and each
|
|
881
|
+
escalation's own `latestReasons` entry remain the durable record. The
|
|
882
|
+
informational rendering itself also drops the reason text (`renderDegradationLines`
|
|
883
|
+
shows only the kind and count for informational kinds), so a forensic read
|
|
884
|
+
needing the "which race" detail must go to `getDegradationSummary()`'s
|
|
885
|
+
`latestReasons` or the NDJSON log, not the rendered summary line.
|
|
886
|
+
Regression test (`tests/clients/resource-sampler-scanner-attribution.test.ts`)
|
|
887
|
+
drives the REAL production path — `sampleProcesses`'s Windows/guarded-CIM
|
|
888
|
+
branch through a real `RESOURCE_SAMPLE_QUERY_TIMEOUT_MS` timeout into the
|
|
889
|
+
real, unmocked `terminateScannerChild` — and asserts the sampler's kind is
|
|
890
|
+
recorded (with its 2000ms budget) while the backstop's kind is not, plus
|
|
891
|
+
the informational (no `⚠`) rendering; a third case pins the sampler's OTHER
|
|
892
|
+
call site (`findDescendantPidsWindows`, reached via
|
|
893
|
+
`sampleProcessTreeCpuPercent`), which the first two cases left untested
|
|
894
|
+
(round 2 review). Only `node:child_process`'s `spawn` and `process.kill`
|
|
895
|
+
are mocked; no real OS process is touched.
|
|
896
|
+
Round 2 review also found the registry-driven reaper's OWN two scanner
|
|
897
|
+
queries — `queryCommandLines` and `findPidsByMarkerWindows`
|
|
898
|
+
(`clients/instance-reaper.ts`, both reached from `sweepOrphans`) — omitted
|
|
899
|
+
`onTimeout` entirely, so a scanner either spawned that blew
|
|
900
|
+
`BACKSTOP_SCAN_TIMEOUT_MS` fell through to `child-unref.ts`'s default
|
|
901
|
+
handler: one bare, unverified `child.kill()`, no tree kill, no
|
|
902
|
+
identity-carrying record — the exact abandonment `terminateScannerChild`
|
|
903
|
+
exists to prevent, reachable from inside the orphan backstop path itself.
|
|
904
|
+
Both now route through `terminateScannerChild` with the backstop's kind and
|
|
905
|
+
budget, matching `enumerateManagedProcesses`'s existing wiring. New test
|
|
906
|
+
`tests/clients/instance-reaper-registry-scan-escalation.test.ts` drives
|
|
907
|
+
`sweepOrphans()` through both real timeouts.
|
|
908
|
+
|
|
909
|
+
- **Compat-smoke Layer B reads the latency log it actually wrote (fixes #2570)** — the behavioral smoke pins `PI_LENS_HOME` for every child `pi` and reads `latency.log` from that pin; since the tmpdir probe-home redirect (#2534) it had been reading the untouched real `~/.pi-lens`, so the light-mode assertions reported a missing phase and the heavyweight-scan absence checks passed on an empty log. Those checks now require at least one entry from the run.
|
|
910
|
+
|
|
911
|
+
- **Reject literal control bytes in tracked source (closes #2571)** — a governance sweep reads tracked TypeScript, JavaScript, and Markdown as bytes, rejects every control byte below U+0020 except tab, line feed, and carriage return, and reports the file, byte, offset, and escaped remediation such as `\^@` for NUL.
|
|
912
|
+
|
|
913
|
+
- **Deflake `hook-await-fold-bounds` by settling its abandoned bootstrap loads (closes #2574)** — the file's two `clients/bootstrap.ts` cases released their gate and returned while `buildBootstrapClients`'s seventeen-module `Promise.all` was still resolving, so the shared `afterEach`'s `vi.resetModules()` landed on a live import graph. The next case's `await import("../../clients/observed-mutation.js")` then either deadlocked to the 5s suite ceiling (8/20 runs on an idle box) or proceeded with its own `vi.doMock` never applied, which is the `armed: true, scannedCount: 0` in 70ms that CI reported as #2574 and #2596 — a lost mock registration, not a lost timer race. Both cases now await the abandoned load through `loadBootstrapClients()`, which joins the in-flight single-flight rather than starting a second one; no production code or timing changed.
|
|
914
|
+
|
|
915
|
+
- **Extend Python SQL-rule safe-builder recognition with same-file provenance (fixes #2576)** — valid SQLAlchemy imports and `select()` calls no longer receive unrelated diagnostics, and `stmt = select(User); db.execute(stmt)` is quiet when the receiver is a proven `Session`/`AsyncSession`. `Session.query` and psycopg identifier composition are suppressed only when same-file AST evidence proves the safe API; the existing `session.execute(...)` and builder-call exemptions are unchanged. Raw, dynamic, shadowed, and ambiguous SQL remains diagnostic.
|
|
916
|
+
|
|
917
|
+
- **Run pytest in uv-managed project environments (closes #2580)** — pytest now honors `UV_PROJECT_ENVIRONMENT` and uv workspace environments for a directory that is itself a uv project root, with a relative `UV_PROJECT_ENVIRONMENT` resolved from that project's workspace root rather than the current directory. A directory that merely sits *below* a Python project — a `frontend/` beside the `pyproject.toml`, a workspace root's `docs/` — inherits nothing from it and keeps its own `.venv`, an activated `VIRTUAL_ENV`, or the generic `python`. An exported `UV_PROJECT_ENVIRONMENT` (uv's documented CI and Docker recipe) therefore no longer captures unrelated checkouts and subdirectories, and only a uv workspace's declared, non-excluded members inherit its `.venv`. Pytest exit code 4 reports a configuration error, while exit code 2 reports an interrupted run.
|
|
918
|
+
|
|
919
|
+
- **Resolve `bundle:dist`'s esbuild independent of the project tree (closes #2590)** — `scripts/bundle-dist.mjs` ran its `npm exec --package esbuild@<version>` spawn from the project root, so npm's Arborist-based resolution could match a nested transitive copy of esbuild anywhere in `node_modules` (not just the npx cache) and skip the real install, leaving the child with no esbuild binary on PATH (`esbuild: not found`). The spawn still runs from the project root (esbuild emits cwd-relative module comments, so the bundle stays byte-identical) but passes `--prefix <fresh empty temp dir>` to `npm exec`, which pins npm's local-tree lookup to an empty directory so esbuild always resolves from npm's own cache.
|
|
920
|
+
|
|
921
|
+
- **`build:dist` now avoids a network fetch for tsc when the local pinned version is already installed, while still isolating the fallback path from #2590's collision (refs #2590)** — `build:dist`'s `npx --yes -p typescript@7.0.2 tsc ...` used the exact same `npm exec --package` resolution mechanism as #2590's fixed esbuild spawn: it can match a same-version `typescript` copy nested anywhere in the project's dependency tree and skip the real install, leaving the child with no `tsc` binary on PATH. No dependency nests a matching `typescript@7.0.2` copy today, so this was not an active failure — it is a latent-class hardening applied as defense-in-depth against a future dependency bump nesting one, the same way #2590 fixed the collision that actually happened for esbuild. The tsc invocation now runs through a new `scripts/build-dist-tsc.mjs`, which first checks for the local pinned `typescript` devDependency (`node_modules/typescript/bin/tsc`, present under any normal install) and spawns it DIRECTLY — no `npm exec`, no network — when its version matches exactly; only when that local binary is absent or mismatched (the `--omit=dev` from-source install shape) does it fall back to the isolated `npm exec --package`/`--prefix <fresh empty temp dir>` path shared with the esbuild spawn (`scripts/lib/exec-isolation.mjs`). An earlier version of this fix ran the isolated path unconditionally, which forced a registry fetch on every single build even when the correct local binary was already present — a real offline/flaky-network regression, now fixed by checking local-first.
|
|
922
|
+
|
|
923
|
+
- **Workspace-member globs no longer wedge a turn on an interleaved `**` chain (closes #2603)** — `matchesWorkspaceMemberPattern` compiled a pattern to one anchored whole-path regex in which every `**` emitted its own nullable `.+`, so N of them explored 2^N splits of a non-matching path; #2591 collapsed CONSECUTIVE `**`s, but a collapse cannot fire across a separating component, so `("**/*" x12)/zzz` against a 40-component project still took 124900 ms inside the `detectPythonEnvironment` call that `clients/test-runner-client.ts`, `clients/dispatch/runners/pyright.ts` and `clients/lsp/server.ts` await with no timeout. A pattern now compiles to a list of bounded steps decided by one memoized (step, position) table — O(steps x characters), no backtracking — and the same shapes answer in about 2 ms in both uv dialects. Answers are unchanged in all three dialects (cargo, uv `members`, uv `exclude`), including the fail-closed answer for an uncompilable character class.
|
|
924
|
+
|
|
925
|
+
- **`ci-verdict.mjs` gates on every check-run, not just Unit tests/Lint (closes #2609)** — The merge-train's CI-verdict read used to inspect only the fixed `Unit tests`/`Lint & type-check` pair, so a red `Production install build` or `Install test` job (found on PR #2588) was reported as "both required checks concluded success." Every check-run GitHub reports on the head is now a row, and every row gates the exit code unless it is on the shared advisory allowlist (`scripts/lib/ci-checks.mjs`, the same list the real merge-train gate already uses). A required check (`Unit tests`, `Lint & type-check`, or whatever `master`'s live branch protection confirms via `gh api`) must reach a literal success; a discovered check's `skipped`/`neutral` conclusion is a non-failure, and its `cancelled` conclusion pends rather than fails (a concurrency-superseded run leaves a stale cancelled row for a few minutes before its replacement posts). `run()` states which required-check source drove the verdict.
|
|
926
|
+
|
|
927
|
+
- **Skills**: the `resources_discover` handler now reports a bounded
|
|
928
|
+
`skills-dir-missing` degradation (extension warning + `pilens_health`) when
|
|
929
|
+
`<packageRoot>/skills` is absent, unreadable, or holds no `SKILL.md` — an
|
|
930
|
+
installed copy missing `skills/`, or the entry file copied out of the
|
|
931
|
+
package tree by a managed extension cache. Previously this registered zero
|
|
932
|
+
skills with no extension error and empty stderr.
|
|
933
|
+
|
|
934
|
+
- **`package.json` now declares `engines.node`, and the managed installer no longer resolves `typescript-language-server` unpinned (closes #2633)** — `package.json` declared no `engines` field, so a fresh install of pi-lens on a Node below `typescript-language-server@6.0.0`'s own `>=22.22.2` floor printed a confusing `npm WARN EBADENGINE` about a transitive devDependency instead of pi-lens stating its own promise. The pi host itself (`@earendil-works/pi-coding-agent@0.84.x`–`0.85.1`) floors at `node >=22.19.0`, lower than `typescript-language-server@6.0.0`'s `>=22.22.2` — since pi-lens must never demand more than the pi host does, the `typescript-language-server` devDependency is pinned back to `5.3.0` (`engines.node >=20`) and `engines.node` is declared at pi's own floor, `>=22.19.0`, which already exceeds every production dependency's own floor (checked against `>= 10`–`>=18` across `@ast-grep/*`, `pidusage`, `vscode-jsonrpc`, `minimatch`'s disjunctive range, and friends). `clients/installer/index.ts`'s `TOOLS` registry entry for `typescript-language-server` — which the runtime installer resolves independently of the devDependency, into each user's own managed-tools directory — carried the same unpinned `packageName` and is pinned to `5.3.0` too, matching the sibling `typescript@5.9.3` pin already in the registry for the same reason. `tests/config/engines-node-floor.test.ts` is new governance, with three cases: `engines.node` has a real finite lower bound (rejects `"*"`); it is a `semver.subset` of every *production* (non-dev) lockfile dependency's own `engines.node`; and, separately, `semver.satisfies` holds for *every* lockfile dependency (prod and dev) at the declared floor's minimum version — the dev-inclusive case is what actually encodes "a fresh install on Node = declared floor emits no EBADENGINE," since a git install of the whole repo (`pi update --all`, this issue's own reproduction) installs devDependencies too. `tests/clients/installer/managed-tool-ids.test.ts` gains a matching case for the installer registry's own pin. `README.md`'s install section states the floor.
|
|
935
|
+
- The install-smoke / release-qa EBADENGINE row belongs to #2613/#2606, not this issue — refs only, not closed here.
|
|
936
|
+
|
|
937
|
+
- **`tests/packaging-pack-manifest.test.ts` no longer writes into the developer's real `~/.pi-lens/install.log` (closes #2634)** — the test runs a REAL `npm pack` in the live checkout (deliberately: the published manifest can only be observed through the real `prepack`/`postpack` npm lifecycle), and `npm pack` also fires `prepare`, whose last step (`scripts/warm-loader-cache.mjs`) appends a `warm_loader_cache` record to `PI_LENS_INSTALL_LOG` or, failing that, `os.homedir()/.pi-lens/install.log`. The spawn passed no `env`, so the child inherited the ambient environment and every run of the unit suite left one record in the maintainer's real install log — the same #2506-class hazard `scripts/release-qa.mjs`'s `scratchEnv()` was built to close (#2619 review F1). The test now passes `env: scratchEnv(tmp)` (imported from `scripts/release-qa.mjs` rather than re-typing the same env map) to the `npm pack` child, redirecting `PI_LENS_INSTALL_LOG`, `HOME`, `PILENS_DATA_DIR` and `npm_config_cache` into its own throwaway temp dir. A new assertion reads the real `~/.pi-lens/install.log` before and after the pack and requires it byte-identical, so a future regression (the env pin dropped, or a new writer added to `prepare` that ignores it) fails loudly instead of silently. The manifest assertions the test exists for (no `devDependencies` in the tarball, the working `package.json`/`package-lock.json` restored byte-for-byte) are unchanged. A full sweep of `tests/` for other real, unpinned `npm`/`npm pack` spawns (`grep -rln "execFileSync(\"npm\"\|npm pack" tests/`) found one other real spawn site, `tests/scripts/release-qa.test.ts`, which already runs entirely through the pinned `scratchEnv()`/`npm()` helpers — no other test needed a fix.
|
|
938
|
+
|
|
939
|
+
- **ast-grep / tree-sitter rules**: `AstGrepClient`'s bundled `rules/`
|
|
940
|
+
fallback, `RuleCache`'s bundled `rules/tree-sitter-queries` root, and
|
|
941
|
+
`ruleFilesForLanguage`'s bundled query directory now report a bounded
|
|
942
|
+
`ast-grep-rules-dir-missing` / `tree-sitter-queries-dir-missing`
|
|
943
|
+
degradation (extension warning + `pilens_health`) when the resolved
|
|
944
|
+
bundled directory is absent, unreadable, or holds nothing — an installed
|
|
945
|
+
copy missing the resource, or the entry file copied out of the package
|
|
946
|
+
tree by a managed extension cache. A language with no bundled queries
|
|
947
|
+
authored for it by design (bash, dart, elixir, lua, ocaml, swift, zig)
|
|
948
|
+
still reports nothing, since the degradation checks the shared bundled
|
|
949
|
+
ROOT's health, not any one language's subdirectory. The ast-grep report
|
|
950
|
+
now also re-fires on the next scan after a session boundary, since
|
|
951
|
+
`AstGrepClient` is a per-process singleton the ledger reset would
|
|
952
|
+
otherwise make it permanently invisible after. Previously these three
|
|
953
|
+
sites silently resolved to zero rules/queries with no signal, the same
|
|
954
|
+
shape #2626 fixed for `resources_discover`'s `skills/` directory.
|
|
955
|
+
|
|
956
|
+
- **Managed CSS/HTML Language Server installs no longer target dead or mismatched npm packages, and a genuine install failure is no longer silently folded into "toolchain absent" (refs #2638)** — `clients/installer/index.ts`'s `TOOLS` registry pointed `vscode-css-languageserver` at a bare npm package of the same name, unpublished from the registry on 2021-07-22 (`npm view vscode-css-languageserver` → `E404`); `ensureTool("vscode-css-languageserver")` could never succeed. Review of the fix (asserting every entry's `binaryName` is actually in its resolved package's published `bin` map) found the identical defect class on `vscode-html-languageserver-bin`: it still resolves on npm, but hasn't been republished since 2018 and its `bin` is named `html-languageserver`, not the `vscode-html-language-server` the entry's `checkCommand`/`binaryName` expect. Both now ship from `vscode-langservers-extracted` — the same unpinned package the sibling `vscode-json-language-server` entry already installs; `npm view vscode-langservers-extracted engines` reports no floor, so (unlike `typescript-language-server`, #2633) there is no EBADENGINE reason to pin either. The installer's stale, self-contradicting "manual install required" header comment is deleted rather than re-derived — `TOOLS` is the single source of truth. `docs/dependencies.md`'s dependency table is corrected to match.
|
|
957
|
+
|
|
958
|
+
`installNpmTool`/`installPipTool`/`installGemTool`'s own catches swallowed the real installer error (E404, EBADENGINE, "no version satisfies", a network failure) and only logged it, so `getInstallFailureReason`/`getInstallAttempt` fell back to a generic "install failed" for every kind of failure alike; all three now record the actual message through one shared `recordPackageManagerInstallException` helper (the three catches were otherwise byte-identical but for a strategy label — a duplication SonarCloud's new-code gate correctly caught). `scripts/smoke-tools.mjs`'s `--lsp` lane used that same generic bucket to classify EVERY `ensureTool` failure — including a policy decline (`PI_LENS_DISABLE_TOOL_INSTALL=1`), an install-lock timeout, and a project-trust denial, none of which ever ran an install — as "unavailable" (⚠, not a failure), indistinguishable from a genuinely-absent toolchain; the nightly saw `ensureTool(vscode-css-languageserver) → UNAVAILABLE` every night without ever going red. A single `classifyInstallOutcome` decision (replacing an initial version that review found classified those same non-attempts as failures too) now gates on the installer's own `getInstallAttempt(...).outcome === "failed"` FIRST — never inferred from the failure-reason map alone — then excludes a transient/offline registry condition (`ENOTFOUND`/`ETIMEDOUT`/a registry 5xx), then checks the strategy's toolchain (npm is always present — this harness itself runs under Node; pip/gem via the SAME candidate ladder `installPipTool` tries, `pipCommandCandidates()`, not a second guess). Only a genuine defect is now reported as ✗ fail, with a one-line, 200-char-capped detail carrying the installer's real error text; every other case (declined/skipped/toolchain-absent/transient) stays the historical ⚠ skip. `.github/workflows/tool-smoke.yml`'s header comment is updated to state this policy.
|
|
959
|
+
|
|
960
|
+
Review round 2 found the classification itself still wrong in two ways.
|
|
961
|
+
First, `toolchainPresent`'s pip probe accepted a bare `python3 --version` as
|
|
962
|
+
proof of a usable pip toolchain; on a python3-without-pip runner (Debian
|
|
963
|
+
slim, manylinux base images) `installPipTool`'s own `python3 -m pip …`
|
|
964
|
+
candidate fails while that probe reported present — `pipCandidateUsable`
|
|
965
|
+
now runs the exact invocation the installer would (`-m pip --version` for
|
|
966
|
+
a python-family command). Second, `getInstallAttempt` was read live at
|
|
967
|
+
classification time, hundreds of lines and several `await`s after the
|
|
968
|
+
ensure loop populated it — a last-writer-wins race (a later
|
|
969
|
+
`{allowInstall:false}` re-ensure rewrites `failed`→`declined` in place)
|
|
970
|
+
that could turn a real defect into a false skip; `ensureFixtureTools` now
|
|
971
|
+
snapshots each tool's attempt record the instant it is found unavailable.
|
|
972
|
+
SonarCloud's new-code duplication gate also failed on the installer
|
|
973
|
+
file itself (not `scripts/smoke-tools.mjs`, which this project's Sonar
|
|
974
|
+
scan does not analyze at all) — traced to two long inline comments on the
|
|
975
|
+
css/html TOOLS entries sitting inside the registry's already-repetitive
|
|
976
|
+
100+-entry shape; shortened both.
|
|
977
|
+
|
|
978
|
+
New tests: `tests/clients/installer/registry-resolve.integration.test.ts` (network-gated, `PI_LENS_INTEGRATION=1` — no CI/nightly lane runs this gate yet, tracked as #2663) asserts every `installStrategy: "npm"` TOOLS entry resolves its pinned-or-latest version on the real npm registry AND that the entry's `binaryName` is in that version's published `bin` map; `tests/clients/installer/managed-tool-ids.test.ts` gains network-free pin assertions for both corrected entries; `tests/clients/install-attempt-evidence.test.ts` gains cases for the npm and pip real-error-recording fixes; `tests/scripts/smoke-tools-genuine-install-failure.test.ts` table-drives `classifyInstallOutcome` over every `InstallAttempt.outcome` × toolchain-state combination (including the three false-positive probes review found and the transient-network exclusion), hermetically pins the python-without-pip case with real POSIX shim scripts, and pins the attempt-snapshot race directly. All proven red-first (PR body has every transcript, including a live `node scripts/smoke-tools.mjs --lsp css --install` run against the real npm registry going from a false-green ⚠ skip to a correctly-red ✗ fail, then to a genuine ✓ pass once the registry fix is restored).
|
|
979
|
+
|
|
980
|
+
- **Infra-kill auto-rerun now covers master pushes and bot merges, not only PRs (closes #2668)** —
|
|
981
|
+
`.github/workflows/ci-infra-kill-rerun.yml` gated on
|
|
982
|
+
`workflow_run.event == 'pull_request'`, so a Unit-tests job killed by
|
|
983
|
+
infrastructure (exit 137, `[mem-watch] KILLED WITH HEADROOM`, no failing
|
|
984
|
+
assertion) on master was never classified or rerun and stayed red until
|
|
985
|
+
someone reran it by hand. Master reaches CI through two non-PR events, not
|
|
986
|
+
one — an ordinary `push`, and `repository_dispatch` for a bot merge, since
|
|
987
|
+
the merge lane's own `GITHUB_TOKEN` merge suppresses the ordinary `push`
|
|
988
|
+
event — so the job now matches a same-repo `push` OR `repository_dispatch`
|
|
989
|
+
to `master`, and the classifier gained an `allowMissingPr` mode: both
|
|
990
|
+
events' `pull_requests` array is always empty, so with the flag set
|
|
991
|
+
classification and the one-shot rerun (still bounded by the existing
|
|
992
|
+
`run_attempt == 1` gate) proceed while every PR-comment step is skipped —
|
|
993
|
+
there is no issue thread to post to.
|
|
994
|
+
|
|
995
|
+
- **Tests**: `instance-reaper-backstop.test.ts`'s grace-retry test no longer reads its metadata off "the last log row" — the retry it arms on a real timer appends its own row asynchronously, so an assertion running late enough for that timer to fire read the retry's metadata instead of the direct sweep's, failing with `expected undefined to be 5` on loaded CI runners (#2669).
|
|
996
|
+
|
|
997
|
+
- **Tests**: admit `runtime-turn-test-runner-bounds.test.ts` to the flake-shape ratchet after #2528 raised its raw-timer-wait count (the waits are the wall-clock bound under test); master was red on the ratchet since #2528 landed after #2559's baseline.
|
|
998
|
+
|
|
999
|
+
- **Install**: the published `package.json` no longer carries `devDependencies` (stripped by `prepack`, restored by `postpack`). npm's resolver walked the dev peer graph when pi supplies host-provided packages into the installed extension, and `@vitejs/devtools@0.7.1` / `vitest@5.0.0` (published 2026-09-03) crash npm 10.9.8 there (`Cannot read properties of null (reading 'edgesOut')`); every install-test lane went red and npm-10 users would hit it at install. The published package never needed them.
|
|
1000
|
+
|
|
1001
|
+
### Security
|
|
1002
|
+
|
|
1003
|
+
- **Scope workflow permissions per job (closes #2403)** - install-smoke and label-sync workflows now deny token permissions by default and grant only checkout reads or post-merge label/comment writes to the jobs that use them.
|
|
1004
|
+
|
|
1005
|
+
- **An operator's LSP denial survives traffic to other project roots (closes #2518)** — the per-root LSP config is now a value of the session-root registry instead of a separate 32-entry cache beside a 128-entry registry, so a config can no longer be evicted while its root is still served. Analyzing 33 or more other directories in one process used to drop a live root's resolved config while the registry still reported that root ready, which silently lifted `lsp.disabledServers` for it with nothing left to re-initialize it. Both readiness memos — the MCP server's and the extension's — now ask the registry rather than only their own memo. A root dropped at the registry's cap is counted as an `lsp-session-root-evicted` degradation, and the reload it forces publishes its own `config_resolved` row instead of a pending mark nothing answers.
|
|
1006
|
+
|
|
19
1007
|
## [4.1.3] - 2026-08-28
|
|
20
1008
|
|
|
21
1009
|
### Added
|