@herbertgao/pi-extensions 2026.8.4 → 2026.8.6
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/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +32 -24
- package/node_modules/@narumitw/pi-btw/package.json +6 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +2535 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
- package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
- package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
- package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
- package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +51785 -36118
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +219 -46
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +40 -15
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +45 -3
- package/node_modules/pi-lens/docs/globalconfig.md +56 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +18 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +41 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
- package/node_modules/pi-mcp-adapter/README.md +38 -4
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +4 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +176 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +12 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -8,8 +8,1608 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
8
8
|
|
|
9
9
|
### Changed
|
|
10
10
|
|
|
11
|
+
### Deprecated
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
## [4.0.1] - 2026-08-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Live native-TS7 diagnostic fixture (refs [#1412](https://github.com/apmantza/pi-lens/issues/1412))** — an opt-in (`PI_LENS_INTEGRATION=1`) integration suite launches the real `tsc --lsp` server against a Vitest-typed fixture, asserting no `Mock`/`mockResolvedValueOnce` false positives after settling while an intentional type-error control still surfaces, and recording the publication/settle sequence.
|
|
24
|
+
|
|
25
|
+
- **Cascade injection, test-targeting, and cache-effectiveness telemetry (Closes #1446)** — `cascade_injected` (what text and how many neighbors/diagnostics reached `blockerParts` — i.e. were queued for the turn-end advisory, not confirmation of agent delivery, which can still be suppressed later; answering item 1) and `cascade_test_targets` (which tests were suggested for cascade neighbors, including the zero-suggestion case, answering item 2) are new `cascade.log` records. `cascade_result` now also carries `cacheHits`/`recentlyCleanHits`/`coldTouches`/`deferredTouches` (item 5's cache-effectiveness measurement, unblocked now that #1444 has landed) and `neighborBudget`/`budgetTruncated` (item 4's telemetry half). Item 3 (`lsp_diagnostics_timeout` server attribution) was already delivered by #1457. Item 4's budget-vs-settle-time sizing change is re-homed as #1462.
|
|
26
|
+
|
|
27
|
+
- **Four ast-grep rules for the type-laundering smell family (Closes #1447)** — Add `no-unknown-laundering`, `no-conditional-empty-object-spread` (+ `-js`), `no-reflect-apply` / `no-reflect-get` (+ `-js`), and `no-chained-type-assertions` to the shipped rule catalog, alongside the existing `no-as-any` and `no-any-type`.
|
|
28
|
+
|
|
29
|
+
- **Attribute worklog and disposition entries to the model/provider that produced them (closes #1448)** — `WorklogEntry` and `DispositionLogEntry` gain optional `model`/`provider` fields, populated at append time from the runtime's telemetry identity (`RuntimeCoordinator.telemetryModelId`/`telemetryProviderId`) and blank when the runtime doesn't know its identity. Provider is the host's explicit value when reported, else a conservative parse of the model id (`clients/model-provider.ts`: a single `/` or `:` separator, or a small known-prefix table; blank on ambiguity — never guessed). `npm run logs:smells` now prints a per-model rollup (rule × model counts, auto-fixed vs. agent-required rates) from `worklog.jsonl`. Old worklog/disposition-log entries stay valid; readers treat both fields as optional.
|
|
30
|
+
|
|
31
|
+
- **Prefix filters for word-index queries: lang, file, ext (Closes #1450)** — `symbol_search`/`pilens_symbol_search` queries can now mix plain terms with composable `lang:`, `file:`, and `ext:` prefix filters, plus `-` negation, e.g. `lang:jsts file:clients/ -file:test rank`. A hand-rolled tokenizer (`parseWordIndexQuery` in `clients/word-index.ts`) splits filters from terms; `lang:` resolves through `KIND_EXTENSIONS` (`clients/file-kinds.ts`), the single source of truth, so there is no second, hand-maintained language list. Filters apply as a pre-ranking predicate, composing with the existing `paths`/`lang` structured options, before BM25/priors/centrality scoring runs. An unrecognized prefix or `lang:` kind throws a typed `WordIndexQueryError` naming the supported list instead of silently degrading to a literal search term.
|
|
32
|
+
|
|
33
|
+
- **Live classic-TypeScript repair fixture (refs [#1436](https://github.com/apmantza/pi-lens/issues/1436))** — an opt-in (`PI_LENS_INTEGRATION=1`) integration suite stages a real managed-tools tree in the reported broken shape (TypeScript 7 with no `lib/tsserver.js`, cached by a prior probe) inside an isolated `PI_LENS_HOME`, then drives the production spawn through the real installer: it asserts the version-gated repair fires exactly once, the tree self-heals to the pinned classic compiler, and the classic launch serves diagnostics again. A negative control proves a disallowed-install spawn stays discovery-only and mutates nothing.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **docs: document the per-edit autofix routing (refs #1414)** — `docs/agent-guide.md` and `docs/features.md`/`docs/usage.md` now describe the write-immediate vs. edit-deferred autofix split, the authoritative post-fix content attached to `write` tool results (with its size cap and shared multi-file bash budget), and the coalesced `{autofix, format}` deferred-mutation queue that drains at `agent_end`.
|
|
38
|
+
|
|
39
|
+
- **Skill docs realigned with shipped behavior and split for token economy (closes #1424, refs #1423)** — `pi-lens-write-ast-grep-rule`'s stale `-js` twins gotcha now reflects the #657 NAPI runner grammar dedup (`ruleLanguageForFile` in `ast-grep-napi.ts`) instead of advising a fix that already shipped; its NAPI-runner-specifics and hard-to-express-pattern gotchas moved verbatim into a new `reference.md` behind a pointer, keeping `SKILL.md` to the minimal template, language list, rule-condition syntax, and the load-bearing `stopBy` warning. `pi-lens-ast-grep`'s Structural-intent parameters table now documents `hasKind`/`hasDescendantKind` per-tool (`ast_grep_search`'s `hasKind` is immediate-child only, with a separate recursive `hasDescendantKind`; `ast_grep_replace` has only the immediate-child `hasKind`) with a cross-tool divergence warning, and its Agent task recipes table is trimmed to entries not already covered by the Patterns table. A new `tests/skills/skill-doc-drift.test.ts` pins param names against real tool schemas, path references against the filesystem and `package.json`'s published files, both rule-writing skills' language lists against their `rule-schema.json` enums, and `<!-- verified: ... -->` claim-comment format.
|
|
40
|
+
|
|
41
|
+
- **Dead code reports the per-turn delta, not the whole project ([#1477](https://github.com/apmantza/pi-lens/issues/1477), refs [#127](https://github.com/apmantza/pi-lens/issues/127))** — turn_end used to re-inject the entire vulture cache every turn. A fixed list of pre-existing findings burned context, and the one actionable signal — a symbol the current edit just orphaned — never stood out. Dead code now follows knip's contract: a new `owns()` seam re-scans only when the turn touched a file of that language, and the result is diffed against the previous scan and filtered to the edited files. Only that delta is injected. It also feeds `projectDiagnosticsDelta`, and the block moved ahead of the delta-report write, so a dead-code-only turn persists a report `lens_diagnostics` can read. A timed-out or killed scan backs the runner off, and a failed scan never overwrites a good cached one. The project-wide list stays available on demand through `lens_diagnostics`. vulture also gets `--ignore-decorators`, so framework-invoked symbols such as fixtures and route handlers stop appearing as permanent noise.
|
|
42
|
+
|
|
43
|
+
- **The auto-format nudge names the action, not the process** — a cross-process batch used to read "by another pi-lens instance (e.g. a subagent's)", which sent agents investigating what other instance was running. It now reads "by an automatic run outside your turn". Which process formatted the file is an implementation detail the reading agent cannot act on; the origin split stays exact in the telemetry.
|
|
44
|
+
|
|
45
|
+
- **Word-index resume stats run through a bounded parallel walk (refs [#1409](https://github.com/apmantza/pi-lens/issues/1409))** — session-start incremental refresh now stats source files with a bounded indexed cursor pool (8 workers over libuv's 4-slot threadpool) instead of serial synchronous calls, while publishing metadata in original walk order so churn classification and rebuild preflight remain deterministic. Per-file stat failures retain the previous absent-file semantics, supersession stops new claims and settles in-flight work, and phase telemetry separates snapshot load, deserialize, source walk, stat walk, refresh reads, and the synchronous snapshot-save span.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **TypeScript LSP roots follow governing configs (refs [#1412](https://github.com/apmantza/pi-lens/issues/1412))** — TypeScript and JavaScript files now prefer their nearest `tsconfig.json` or `jsconfig.json` (filtered by extension family — a `.ts` file skips a jsconfig-only directory) while preserving package-boundary client isolation, and classic servers emit bounded, read-only project-association telemetry after the first open. Classic-server tool discovery (`typescript-language-server`, `tsserver.js`) now walks up from a nested config root instead of only checking the root itself. Known accepted tradeoff: honoring nested config roots enlarges the population of roots subject to #1373's pre-existing open-order sensitivity.
|
|
50
|
+
|
|
51
|
+
- **Post-fix decisions are observable (Closes #1432)** — Add bounded decision telemetry for advisory provenance, TypeScript project identity, deferred mutations, authoritative-content attachment, bus context resolution, and time-scoped smell rollups.
|
|
52
|
+
|
|
53
|
+
- **Native TS7 diagnostic waits stabilize versionless publication bursts (refs [#1412](https://github.com/apmantza/pi-lens/issues/1412))** — TypeScript wait strategy resolution now follows the launched server variant: classic typescript-language-server retains authoritative first-push seeding, while native `tsc --lsp --stdio` debounces provisional pushes until a quiet window or advertised pull provides the settled result. Bounded latency telemetry records publication shape and settle source without diagnostic text.
|
|
54
|
+
|
|
55
|
+
- **Stale advisories are historical, not current blockers (refs #1413)** — Turn-end and async test caches record immutable capture provenance and SHA-256-confirm every affected file at delivery. Changed, legacy, malformed, unreadable, truncated, and superseded findings remain non-blocking historical context; deleted per-file findings disappear, while unchanged findings retain live blocker framing across turn and project-sequence drift. Monotonic async-test generations prevent older batches from overwriting newer results, and MCP and in-process delivery share one classification without changing acknowledgement, one-shot consumption, or commit-gate state.
|
|
56
|
+
|
|
57
|
+
- **Per-edit autofix no longer mutates files mid-turn (closes #1414)** — `edit` queues pipeline autofix for the owning `agent_end`, where autofix runs before the stable formatting pass; `write` keeps immediate autofix and returns authoritative post-fix file content, while write-then-edit paths stay demoted for the rest of the turn. Deferred mutation records coalesce both phases, preserve session ownership, deduplicate project-wide Rust/Dart fixers, and merge independently requeued phases.
|
|
58
|
+
|
|
59
|
+
- **Bus publishers keep activation context ownership (closes #1415)** — Event producers pair each live emitter with its OWN activation's context (no fallback to a process-global "latest ctx", which could belong to an unrelated sibling activation) and guard lens events through the shared stale-session seam with the same occurrence-scoped failure gating every other producer uses. Lens emit failures now count toward the `bus-stale` degradation smell like every other producer's — intentional: a probe-undefined or resolve/emit race on the lens path is a genuine failure that should be visible, not silently excluded.
|
|
60
|
+
|
|
61
|
+
- **`ast_grep_replace` now matches `ast_grep_search`'s `hasKind`/`hasDescendantKind` surface (closes #1423)** — `ast_grep_replace`'s `hasKind` param description wrongly said "contain a descendant"; it actually restricts to an **immediate child** (ast-grep default `stopBy: neighbor`), same as `ast_grep_search`. The description now matches the real behavior. `ast_grep_replace` also gained the `hasDescendantKind` param `ast_grep_search` already had — the explicit recursive form (`stopBy: end`) for when the target kind is nested below an immediate child; it's mutually exclusive with `hasKind`, surfaced as a clear synthesis error from both tools. `skills/pi-lens-ast-grep/SKILL.md`'s structural-intent table drops the now-obsolete "replace has no `hasDescendantKind`" divergence warning and documents both params as available on both tools.
|
|
62
|
+
|
|
63
|
+
- **pi's own CLI output is no longer swallowed by the console guard (Closes #1434)** — The guard used to replace every `console.*` method globally and permanently once the extension loaded. pi's one-shot commands print through `console.log`, so `pi list` exited 0 with no output in any project whose directory loaded the extension first. The guard is now a dispatcher: it routes a write to the extension log only while pi-lens owns execution, and passes every other write to the original console method. Windows open around module evaluation, the extension activation, and every callback registered through the host API's `on` and `register*` members. `AsyncLocalStorage` carries the window across `await` boundaries. Terminal safety is unchanged for pi-lens code paths. One cost note for Node 22: an active `AsyncLocalStorage` adds per-`await` overhead process-wide on that runtime. `PI_LENS_CONSOLE_GUARD=0` disables the guard and avoids that cost entirely.
|
|
64
|
+
|
|
65
|
+
- **Managed TypeScript self-heals the classic LSP fallback (closes [#1436](https://github.com/apmantza/pi-lens/issues/1436))** — Pi Lens now pins its managed classic compiler to TypeScript 5.9 and repairs existing managed TypeScript 7 trees when `tsserver.js` is absent, while leaving project-local TypeScript 7 on the native `tsc --lsp --stdio` path.
|
|
66
|
+
|
|
67
|
+
- **Deliver late cascade results instead of dropping them (closes #1443)** — A cascade whose compute missed the turn-end settle cap, or whose neighbor diagnostics landed in the quiet window after the turn ended, was carried over and then discarded unread: the turn-end filter rejected every run stamped with an earlier turn, so the carry-over path was dead code (the two measured cases were the highest fan-out cascades of the day). Late runs now merge into the following turn's output, a run superseded by a newer write is dropped with a logged record instead of silently, and the carry is bounded to one turn.
|
|
68
|
+
|
|
69
|
+
- **Settle native TS7 cascade checks (closes #1444)** — Cascade neighbor checks on native TS7 no longer burn the in-lane wait budget on a publication that only arrives later; the result is collected in the quiet window instead, and a late clean result now also clears the neighbor's stale footer errors. On the classic (full-wait) lane, a neighbor whose diagnostics wait lapsed used to produce no output at all — which read as "clean" — and now renders an explicit inconclusive note; unconfirmed checks are still never cached as clean. `cascade_result` records how many neighbors were deferred, so a fully-deferred cascade is distinguishable from a clean leaf.
|
|
70
|
+
|
|
71
|
+
- **Cascade no longer blames the review graph for test-file edits (closes #1445)** — A `missing_node` cascade result had two causes that read the same but meant opposite things: a real gap in the graph, and a test file excluded from the graph by design (#260). Test-file edits now get a distinct `excluded_by_role` reason that stays in telemetry but never reaches the agent as a "review graph was unavailable" advisory. A genuinely missing source file still reports `missing_node` and still triggers the advisory.
|
|
72
|
+
|
|
73
|
+
- **Test-runner results report a real duration and a real skip count (closes #1452)** — Every vitest and jest run logged `(0ms)`: `parseJsonTestOutput` hardcoded `duration: 0` and read `numSkippedTests`, a field neither reporter emits, so a file with skipped tests also always reported 0 skipped. Duration now comes from the per-suite `startTime`/`endTime` the reporters do emit (wall-clock span across suites, falling back to summed per-assertion durations), and skips come from `numPendingTests` + `numTodoTests`. PHPUnit's own `Time:` summary line — both the `00:00.123` clock form and the legacy `1.23 seconds` / `123 ms` forms — is parsed instead of discarded. An unparseable summary reports no duration at all rather than a wrong figure (see #1479 — this sentence read "still reports 0" until that landed, and both entries are still unreleased, so one release's notes would otherwise have contradicted the other).
|
|
74
|
+
|
|
75
|
+
- **Keep the tool list stable across forks and reloads (closes #1453)** — When a session is forked, reloaded, or resumed, pi tells the model about every pi-lens tool again. Pi-lens now puts the list straight back to what it was, including the situational tools the model had activated, so the tool list the model sees does not change and your cached prompt still applies. Use `--no-lazy-tools` (or `tools.lazy: false`) to keep every tool active from the start instead.
|
|
76
|
+
|
|
77
|
+
- **Auxiliary LSP scanners reach per-edit diagnostics (closes #1458)** — Auxiliary waits now use each server's declared budget within a 2-second post-primary ceiling, in both the touch push wait and the `getDiagnostics` aggregation lane. Late findings carry into the next unchanged-content read only when their SHA-256 content binding matches. Each touch logs a per-scanner wait outcome (answered, silent, or cut off — decided from evidence of an actual publication, not just promise settlement) with elapsed time and effective budget. Independently diagnosed by @snowyukitty, who reported the same root cause and both affected call sites in #1471.
|
|
78
|
+
|
|
79
|
+
- **Secret blockers no longer name deleted files (refs #1461)** — Scanner result caches were valid on wall-clock age alone, so a gitleaks finding stayed a 🔴 STOP blocker for the rest of its 30-minute window even after the file it named was deleted. The advisory provenance guard did not catch it: it validates the files the agent edited, not the paths inside the findings. Provenance now also validates finding-cited paths, and gitleaks drops findings whose path is gone rather than demoting them — a deleted file offers no remediation. Findings on files that still exist are delivered unchanged; a finding with no path, an unreadable path, or a path past the stat budget is still delivered. Each drop writes one bounded `finding_dead_path_drop` record to `latency.log` naming the store, the count, and a sample of paths.
|
|
80
|
+
|
|
81
|
+
- **A slow tool check no longer disables an installed tool for the whole session (closes #1467)** — When the check for knip, madge, govulncheck, or vulture timed out, pi-lens remembered the tool as unavailable until you restarted, and told you to install a tool that was already installed. Knip produced no findings for weeks because of this. A timed-out check is now retried after a short wait, the message says the check timed out instead of naming the wrong cause, a failed run keeps the last good cached findings instead of overwriting them, and each decision is recorded in `latency.log` as an `availability_decision` entry with its cause and timing.
|
|
82
|
+
|
|
83
|
+
- **A cut-off auxiliary no longer reads as confirmed clean (closes #1470)** — When
|
|
84
|
+
the aux grace timer cut off a scanner such as opengrep, the touch still reported
|
|
85
|
+
an unqualified confirmation, so a hung security scanner produced a result that
|
|
86
|
+
read as a clean bill of health. The confirmation is now narrowed rather than
|
|
87
|
+
discarded: it names the servers it does not speak for, so `lsp_diagnostics` says
|
|
88
|
+
which coverage is missing, the cascade no longer wipes a live finding on that
|
|
89
|
+
evidence, the workspace sweep stops caching a partially covered result, and the
|
|
90
|
+
per-edit lane stops reporting an empty result as checked — on both the
|
|
91
|
+
incumbent-touch route and the warm-attach route a per-edit check takes in a
|
|
92
|
+
live session. A primary that
|
|
93
|
+
answered stays trustworthy — its findings still reach you. A related gap stays
|
|
94
|
+
open: a scanner that answers within its budget but publishes nothing still reads
|
|
95
|
+
as clean (#1493).
|
|
96
|
+
|
|
97
|
+
- **A slow first probe no longer disables an installed tool (closes #1476)** —
|
|
98
|
+
biome, ast-grep, Go, cargo and govulncheck now tell a probe timeout apart from
|
|
99
|
+
a missing install, the way knip and madge already did. Before this, one stalled
|
|
100
|
+
second at session warm-up latched "tool is not installed" for the life of the
|
|
101
|
+
process, and biome and ast-grep also paid for an auto-install nobody needed. A
|
|
102
|
+
transient verdict now expires and the tool comes back without a restart, its
|
|
103
|
+
cooldown escalates on a host that stays sick, and every verdict is recorded in
|
|
104
|
+
`latency.log` as `availability_decision` with its cause, timing and retry
|
|
105
|
+
schedule. A structural coverage test parses `clients/` and fails when a new
|
|
106
|
+
version probe parks its verdict outside the shared policy; the hand-rolled
|
|
107
|
+
latches that predate it are listed in the test as named, shrink-only gaps
|
|
108
|
+
rather than being quietly excluded.
|
|
109
|
+
|
|
110
|
+
- **The turn-end log no longer prints `(0ms)` for a run it never timed (closes #1479)** — `(0ms)` was the string for a genuinely sub-millisecond run *and* for one nobody measured: a JSON payload with no suite timestamps, a pytest or ExUnit or PHPUnit summary the parser could not read, a runner error, or nothing run at all. A reader could not tell "measured 0" from "unmeasured", which is the confusion #1452 was reported for in the first place. `TestResult.duration` is now absent when the run was not measured — a present `0` means a real reading, because pytest does print `in 0.00s` — and the turn-end line renders `(unmeasured)` for the absent case. The agent-facing `formatResult` string is unchanged.
|
|
111
|
+
|
|
112
|
+
- **Cargo, dotnet, maven, rspec and minitest runs report a real duration (closes [#1480](https://github.com/apmantza/pi-lens/issues/1480))** — `parseGenericRunnerOutput` read an elapsed time only for go, so the seven runner families behind it reported a hardcoded `0`. Each duration is now parsed from the summary block the runner already prints, and each pattern is applied only to the runner it belongs to, so no runner can be scored by another's output — gradle used to satisfy go's probe and report the whole-build wall clock as test time. Gradle stays unmeasured: its console summary carries no test elapsed time. Multi-module maven runs are scored by the sum of their surefire aggregates rather than by a single module, a runner that exits non-zero can no longer be reported as a pass, and the pytest and ExUnit summaries are rounded to whole milliseconds instead of logging `2009.9999999999998ms`. The "was this measured at all" rule now lives in one module (`clients/run-duration.ts`) that both the turn-end log and the agent-facing summary read, rather than being re-derived at each site.
|
|
113
|
+
|
|
114
|
+
## [4.0.0] - 2026-08-14
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- **Bounded LSP pull-failure telemetry and lifecycle test seams (refs [#1292](https://github.com/apmantza/pi-lens/issues/1292), items 1 and 3)** — advertised diagnostic-pull failures retain the latest ten operational failures and surface through LSP status, while unsupported `-32601` responses remain unreported; focused coverage exercises acquisition races, descendant cleanup, shutdown protocol races, and honest push fallback. Strategy-gated `didSave` (item 2) remains out of scope.
|
|
119
|
+
|
|
120
|
+
- **Occupancy and CPU-budget tests stay in the timing-sensitive Vitest project (refs [#1230](https://github.com/apmantza/pi-lens/issues/1230), [#1081](https://github.com/apmantza/pi-lens/issues/1081))** — a meta-test derives the expected membership of `timingSensitiveInclude` from the tests themselves (importing `measureMaxSyncBlockMs` from the perf harness, or calling `process.cpuUsage`) and fails if such a test lands outside the project, so the quiet phased measurement window can't silently drift. It reads the real config object rather than its source text (a commented-out entry is a removal, not a match), and rejects include entries whose file no longer exists so a rename can't leave a dead glob behind. The six sampler tests that had already drifted out are phased back in.
|
|
121
|
+
|
|
122
|
+
- **Claude Code `Stop` hook drives the per-turn pass ([#538](https://github.com/apmantza/pi-lens/issues/538))**. `pi-lens-analyze` gains a warm-server-only turn-end mode (`--turn-end`, or a `Stop` payload on stdin) that runs incremental knip/madge, dependency checks, tests, and actionable-warning aggregation without an explicit `pilens_turn_end` call. PostToolUse records edited files in workspace turn-state and clears inherited pi session stamps before Stop consumes it. The tagged request shares the one-shot workspace IPC endpoint and build-staleness gate with warm analysis. A server-side FIFO orders every analyze and turn-end request, so a timed-out hook cannot let Stop overtake analysis still running in the server or race another turn-end. Missing or incompatible warm servers produce one reason-specific line on stdout and stderr and exit 0 (see the turn-end residuals entry below). `SubagentStop` stays excluded because its edits already feed the shared worklist.
|
|
123
|
+
- **Generic read-recording bridge for co-process extensions ([#1265](https://github.com/apmantza/pi-lens/issues/1265))** — `clients/read-bridge.ts` mounts a `ReadBridge` object at `globalThis[Symbol.for("pi-lens:read-bridge")]` after the runtime initialises. Any co-process Pi extension whose custom tool performs file reads outside pi-lens's normal tracking can call `bridge?.recordRead({ filePath, requestedOffset, requestedLimit })` so that a subsequent `edit` on the same file is not blocked by the read-before-edit guard. The bridge is an advisory, trust-based protocol — it stamps the timestamp itself (`Date.now()`) matching the internal read path, and validates that `filePath` is a non-empty string and offsets/limits are finite positive integers. The bridge respects `no-read-guard`, gitignored paths, and external/vendor paths (consistent with the bash read-guard gate). Registration is a factory-scoped singleton (`_readBridgeRegistered` guard, same pattern as `_turnSummaryEmitRegistered`); `_readBridgeGetFlag` is refreshed on every factory activation so flag changes take effect immediately. Absent bridge is safe — producers guard with `bridge?.recordRead()`.
|
|
124
|
+
|
|
125
|
+
- **tflint respects the project's linter policy (refs #1117)** — every other
|
|
126
|
+
dispatch runner gates on `getLinterPolicyForCwd` before spawning; tflint was
|
|
127
|
+
the one that never consulted it, so a project that elected a different
|
|
128
|
+
terraform linter got tflint's findings on top of its own. It now skips when
|
|
129
|
+
policy does not prefer it, matching golangci-lint and terragrunt. A project
|
|
130
|
+
`.tflint.hcl` is also an explicit opt-in, so `hasTflintConfig` promotes the
|
|
131
|
+
`.tf`/`.tfvars` policy from `smart-default` to `config-first` the same way
|
|
132
|
+
`.golangci.yml` does for Go. tflint ships built-in rules and still runs
|
|
133
|
+
unconfigured, so the no-config default is unchanged. `hasTflintConfig` takes
|
|
134
|
+
the edited file's directory rather than the project cwd, so it agrees with
|
|
135
|
+
what the runner hands tflint: keyed off cwd it would miss every `.tflint.hcl`
|
|
136
|
+
living in a terraform subdirectory, which is the common monorepo layout.
|
|
137
|
+
- **tflint honors a repo-root `.tflint.hcl` (refs #1117)** — tflint resolves
|
|
138
|
+
`.tflint.hcl` from its own working directory and never walks parents (its only
|
|
139
|
+
fallback is `~/.tflint.hcl`), and the runner spawns it from the edited file's
|
|
140
|
+
directory. A config at the repo root therefore governed nothing beneath the
|
|
141
|
+
root: every module under `modules/` or `envs/` was linted with tflint's
|
|
142
|
+
built-in defaults, silently. The runner now walks up for the nearest
|
|
143
|
+
`.tflint.hcl` (via `findNearestDirWithAnyBasename`, so the same home-guarded,
|
|
144
|
+
depth-capped, mtime-invalidated walk every other marker lookup uses) and
|
|
145
|
+
passes it as `--config`. Skipped when `TFLINT_CONFIG_FILE` is set, since
|
|
146
|
+
`--config` outranks the env var in tflint's own precedence and would override
|
|
147
|
+
a deliberate choice. A `.tflint.hcl` that declares an uninitialized plugin
|
|
148
|
+
makes tflint exit non-zero with empty stdout, which the guard fix above turns
|
|
149
|
+
into a SKIPPED rather than a false clean.
|
|
150
|
+
- **Terragrunt linting and formatting** — `terragrunt.hcl` and `root.hcl` now
|
|
151
|
+
get a dedicated `terragrunt` file kind (filename-detected only; a plain
|
|
152
|
+
`.hcl` file stays unmapped). New `terragrunt` dispatch runner wraps
|
|
153
|
+
`terragrunt hcl validate --json`, and a `terragrunt-hcl` formatter wraps
|
|
154
|
+
`terragrunt hcl fmt`. Terragrunt has no LSP, so it's runner/formatter only.
|
|
155
|
+
The `terragrunt` binary installs the same way as tflint (GitHub release,
|
|
156
|
+
bare per-platform binaries). Also added `.tf` (Terraform) to the
|
|
157
|
+
`trivy-config` runner's `appliesTo`, gated behind the existing
|
|
158
|
+
`trivy.enabled` opt-in — terragrunt itself stays excluded from trivy, since
|
|
159
|
+
trivy evaluates the Terraform language, not terragrunt config. The two
|
|
160
|
+
entrypoint filenames live in one exported `TERRAGRUNT_FILENAMES` constant
|
|
161
|
+
(`clients/file-kinds.ts`, same single-source-of-truth pattern as the .NET
|
|
162
|
+
root markers), consumed by kind detection, both tool policies, the formatter,
|
|
163
|
+
and the project/root markers; `tests/clients/terragrunt-filenames.test.ts`
|
|
164
|
+
derives its cases from that list, so a call site that hand-copies it and
|
|
165
|
+
misses a name fails CI. Diagnostic ids carry line, column, and a normalized
|
|
166
|
+
message slug (the actionlint shape), because `hcl validate` reports no rule
|
|
167
|
+
code: a line-only id collapses two findings at one position in the
|
|
168
|
+
dispatcher's dedupe (keyed `filePath:line:column:defectClass:rule||id`) and
|
|
169
|
+
hides a changed finding from delta mode, which keys on `id` alone.
|
|
170
|
+
Diagnostics are attributed by resolving `range.filename` against the unit
|
|
171
|
+
directory and comparing through `pathsEqual`, not by basename: a unit that
|
|
172
|
+
pulls in its parent via `find_in_parent_folders()` gets diagnostics from a
|
|
173
|
+
parent `terragrunt.hcl` whose basename matches the edited file's but whose
|
|
174
|
+
line numbers belong to another file.
|
|
175
|
+
- **Smells self-surfacing (refs #1123 item 3)** — `scripts/analyze-pi-lens-logs.mjs`
|
|
176
|
+
(`npm run logs:smells`) already catalogues a wide set of operational smells,
|
|
177
|
+
but it's MANUAL: the #1123 investigation found 20 stale-ctx `emit_failed`
|
|
178
|
+
rows and 37 opengrep respawns sitting unread in logs for days until an audit
|
|
179
|
+
went looking. `clients/smells-rollup.ts` adds a small ALWAYS-ON rollup
|
|
180
|
+
covering the two smells the issue named, without touching every producer of
|
|
181
|
+
the underlying logs and without re-scanning the size-rotated (up to ~10MB,
|
|
182
|
+
`clients/log-cleanup.ts`) `~/.pi-lens/*.log` files on the `session_start`
|
|
183
|
+
hot path. **Design + cost bound:** a BOUNDED TAIL READ — at most 64KB from
|
|
184
|
+
the end of `bus-events.log` and `latency.log` each (~128KB total I/O per
|
|
185
|
+
check, enforced by construction via a single sized `fs.readSync` at a
|
|
186
|
+
computed offset in `tailReadText`, never a full-file scan) — counts
|
|
187
|
+
stale-ctx `emit_failed` rows (`outcome === "emit_failed"` with an `error`
|
|
188
|
+
containing the SDK's `"stale after session replacement"` fragment,
|
|
189
|
+
`session-lifecycle.ts`'s documented benign-but-worth-watching class) and
|
|
190
|
+
opengrep respawns (`phase === "lsp_server_respawn"` with
|
|
191
|
+
`metadata.serverId === "opengrep"`, `clients/lsp/index.ts`'s existing,
|
|
192
|
+
unmodified respawn log point). Because both source logs are append-only
|
|
193
|
+
NDJSON, the tail is always the most recently written activity, so the same
|
|
194
|
+
bounded read serves as both the cross-session glance and a live/this-session
|
|
195
|
+
proxy — no separate write-time counters were added at the producer call
|
|
196
|
+
sites (kept the change to one new module + three call sites). Both counts
|
|
197
|
+
are gated by trivial threshold constants (`SMELLS_THRESHOLDS`, currently 5
|
|
198
|
+
each) so a single stray event never surfaces. Three surfaces: one
|
|
199
|
+
`session_start` line (only emitted when a threshold trips, via
|
|
200
|
+
`runtime-session.ts`'s `emitSmellsSessionStartLine`), an always-on compact
|
|
201
|
+
`/lens-health` line (current counts regardless of threshold, matching
|
|
202
|
+
#1123 item 2's `formatMemoryHealthLine` style), and a `turn_end` note —
|
|
203
|
+
re-checked every 20 turns (`shouldCheckSmellsThisTurn`), `ctx.ui.notify`d
|
|
204
|
+
at most ONCE per smell per session (`checkSmellsAndNoteOnce`'s gate,
|
|
205
|
+
re-armed by `resetSmellsSessionState()` at the next `session_start`).
|
|
206
|
+
|
|
207
|
+
New tests: `tests/clients/smells-rollup.test.ts` (tail-scan cost bound
|
|
208
|
+
proven by construction — a file far larger than the byte budget only ever
|
|
209
|
+
yields tail content; threshold gating for both the session_start line and
|
|
210
|
+
the always-on health line; the once-per-session notify gate) and
|
|
211
|
+
`tests/index-smells-rollup-wiring.test.ts` (turn_end wiring: nothing before
|
|
212
|
+
the check interval, one notify at the trip turn, no repeat notify on the
|
|
213
|
+
next check turn for an already-notified smell) — fail-then-pass verified
|
|
214
|
+
against a deliberately broken threshold gate.
|
|
215
|
+
|
|
216
|
+
- **Oxfmt formatting support for Svelte (refs #1134)** — `.svelte` is now a
|
|
217
|
+
recognized oxfmt extension, gated by a stricter conditional than oxfmt's
|
|
218
|
+
other extensions. Empirically verified against the real `oxfmt` npm
|
|
219
|
+
package (0.62.0, scratch fixture outside vitest, per this repo's
|
|
220
|
+
verify-the-CLI-contract-empirically rule): oxfmt requires BOTH the
|
|
221
|
+
`svelte` package installed AND the config's `svelte: true` flag enabled —
|
|
222
|
+
either alone exits non-zero ("excluded by ignore rules" or "Cannot find
|
|
223
|
+
module 'svelte/compiler'"), only both together format the file. The new
|
|
224
|
+
`hasOxfmtSvelteConfig` (`clients/tool-policy.ts`) encodes this, consulted
|
|
225
|
+
only for `.svelte` in `formatters.ts`'s `hasExplicitFormatterConfig` — the
|
|
226
|
+
other oxfmt extensions are unaffected. **Class fix**:
|
|
227
|
+
`oxfmtFormatter.extensions` (`clients/formatters.ts`) and
|
|
228
|
+
`OXFMT_SUPPORTED_EXTENSIONS` (`clients/tool-policy.ts`) were two
|
|
229
|
+
hand-maintained parallel lists (the #883 single-source-of-truth class);
|
|
230
|
+
`OXFMT_SUPPORTED_EXTENSIONS` is now exported as the sole source of truth
|
|
231
|
+
and `oxfmtFormatter.extensions` derives from it directly, plus a drift-guard
|
|
232
|
+
test asserting they stay equal. `docs/language-coverage.md`'s Svelte row now
|
|
233
|
+
reports the formatter and its gating condition instead of "—". The real
|
|
234
|
+
format-smoke fixture (`tests/fixtures/format-smoke/`) was NOT extended: no
|
|
235
|
+
fixture in that harness has a dependency-install step (oxfmt itself is only
|
|
236
|
+
found via a global `which` lookup, not `ensureTool`), so a svelte fixture
|
|
237
|
+
would either always skip (oxfmt unavailable) or, worse, hard-fail with a
|
|
238
|
+
"Cannot find module 'svelte/compiler'" error if oxfmt happened to resolve
|
|
239
|
+
without svelte actually installed — a strictly worse risk than omitting it.
|
|
240
|
+
- **Instance health + memory-attribution observability (refs #1123 item 2)** — the #1126 sizing study diagnosed a 1.37 GB pi-lens instance from code + serialized artifacts alone, because nothing recorded a per-subsystem trajectory over time (the same detection-without-attribution gap loop_block had before #1122/#1125). Three pieces:
|
|
241
|
+
1. **Vanished-instance markers.** `deregisterInstance()` (`clients/instance-registry.ts`) synchronously removes a process's own `instances.json` entry on a clean `session_shutdown`, so an entry whose owning pid is confirmed dead is, by construction, proof that process never reached that shutdown path — no new "clean shutdown" flag is needed, the existing `heartbeatAt`/`rssBytes` fields already ARE the "lastSeen"/"rss" pair this needs (`clients/vanished-instance-marker.ts`). `session_start` now reads the registry and logs one `sessionstart.log` line per such entry — `previous instance pid X last seen <ts> (RSS <y>MB) exited without shutdown` — BEFORE `sweepOrphans()` (`clients/instance-reaper.ts`) prunes those same dead-pid entries; the read is sequenced ahead of the sweep (via `.finally()`) rather than let both fire-and-forget calls race, or the vanished set would already be empty by the time the marker ran.
|
|
242
|
+
2. **Periodic memory-attribution sample (`clients/memory-sampler.ts`).** Every 10 turns, one `memory_sample` `latency.log` phase line: `process.memoryUsage()` (rss/heapUsed/heapTotal/external/arrayBuffers) plus O(1)/O(bounded-cache-size) per-subsystem counters — review-graph resident workspace-cache entry count + summed node/edge counts (`getReviewGraphWorkspaceCacheSnapshot`, `clients/review-graph/builder.ts`), word-index doc/posting/forward-entry counts (off `runtime.wordIndex`), loaded tree-sitter grammar/parser/query-cache counts + tree-cache size/bytes (`TreeSitterClient.getRuntimeStats`, `clients/tree-sitter-client.ts`), and the dispatch cascade's turn-bounded cache sizes (`getDispatchCascadeCacheStats`, `clients/dispatch/integration.ts`). Every field is a `Map`/array `.size`/`.length` read or a `process.memoryUsage()` call — nothing iterates a large structure's contents, nothing snapshots the heap. **Documented gap vs the #1126 spec:** the WASM linear-memory byte length (`Module.wasmMemory.buffer.byteLength`) is NOT included — inspecting the installed web-tree-sitter 0.25.10 package confirmed that value lives in a private closure (`bindings.ts`'s `Module` singleton) with no public export, and reaching it would require either internal reflection (brittle across versions/bundling) or overriding Emscripten's `wasmMemory` init option with a hand-built `WebAssembly.Memory` (risks a memory-import mismatch breaking ALL structural analysis, for an observability-only feature) — `process.memoryUsage().arrayBuffers` is used as the process-wide proxy instead (WASM linear memory backs an ArrayBuffer, so it's already included there).
|
|
243
|
+
3. **`/lens-health` memory block**, reusing the same sample: RSS/heap/external plus the tree-sitter cache byte total and review-graph node/edge counts, following #1125's compact single-line health-line style. `instances.json`'s `rssBytes`/`heartbeatAt` refresh cadence needed no new wiring — `runtime-turn.ts`'s existing per-turn `updateHeartbeat()` call (#449 slice 1) already refreshes both every turn, which is what makes marker (1)'s RSS meaningful.
|
|
244
|
+
|
|
245
|
+
New tests: `tests/clients/vanished-instance-marker.test.ts` and `tests/index-vanished-instance-wiring.test.ts` (fail-then-pass: a dead-pid entry logs the marker and is still pruned afterward; a live-pid or already-clean entry logs nothing), `tests/clients/memory-sampler.test.ts` and `tests/index-memory-sample-wiring.test.ts` (cadence assertions: nothing before turn 10, exactly one sample at turn 10 and turn 20, never in between), and an `/lens-health` line assertion in `tests/index-wiring.test.ts`.
|
|
246
|
+
|
|
247
|
+
- **Handle-origin tracer debug flag (refs #1123 item 4)** — institutionalizes the `async_hooks` handle-origin tracer the #1097 print-mode-hang investigation hand-rolled to find a leaked, ref'd `setTimeout` (root-caused/fixed in #1110); the #1097 reporter explicitly asked for a built-in active-resource dump. New `clients/debug-handles.ts`: `PI_LENS_DEBUG_HANDLES=1`, read ONCE at extension load, gates everything — unset (the default), every export is a no-op past that one boolean check (no writer constructed, no `async_hooks` hook installed, zero cost on the hot lifecycle paths that call it). Set at startup, `dumpActiveHandles(label)` writes one ndjson line to `~/.pi-lens/debug-handles.log` (standard `createNdjsonLogger` family — size-bounded, log-cleanup registered) with `process.getActiveResourcesInfo()` counts by resource type, plus per-type creation-site stack attribution from a bounded `async_hooks` tracker (capped at `TRACKER_MAX_ENTRIES`, never unbounded growth even in a timer-storm session) that is itself only installed when the flag was already on before the module first loads (real per-resource-creation overhead, strictly opt-in). **Eviction is protect-the-earliest, not drop-oldest**: a #1097-style leak is typically among the earliest handles created in a session, so a naive drop-oldest policy would let a later burst evict exactly that evidence — the first `TRACKER_PROTECTED_COUNT` insertion-order entries are pinned, eviction targets the oldest entry outside that zone, and a running `evictedCount` is always present on the dump entry once the tracker is installed (even at `0`) so an attribution gap from a burst past the cap is explicit rather than silent. Wired into `index.ts` at two points: `agent_settled` (after the quiet-window work is scheduled — the #1097-class leak is only visible once whatever settle itself queued is in flight) and `session_shutdown` (after teardown — whatever survives IS the leak). New tests: `tests/clients/debug-handles.test.ts` (flag unset → no writes/no tracker; flag set → dump emitted with type counts + creation-site attribution; a 600-entry burst against the 500 cap proves the first `TRACKER_PROTECTED_COUNT` and the most-recent entries both survive with `evictedCount` correctly reported; a real `async_hooks.createHook`-driven test creates and clears a genuine `setTimeout` and asserts it appears in, then is pruned from, a real dump) and two fail-then-pass wiring cases in `tests/index-integration.test.ts` asserting the dump fires strictly after LSP teardown / after quiet-window scheduling at the real `index.ts` seams.
|
|
248
|
+
|
|
249
|
+
- **Source-walk generated-artifact escape hatch (closes #1107, phase 2 of 2)** — three pieces, building on phase 1's
|
|
250
|
+
counters (#1111):
|
|
251
|
+
1. **Directory-level skip counting.** `shouldRecurseIntoDir`'s
|
|
252
|
+
`isGeneratedArtifactDirectoryName` branch (`clients/source-walker.ts`)
|
|
253
|
+
pruned whole directories (`generated/`, `codegen/`, `__generated__/`, …)
|
|
254
|
+
with zero counting — an entire directory of real files reported zero.
|
|
255
|
+
`SourceCollectionResult` gains `generatedDirSkips`, threaded through the
|
|
256
|
+
same `SourceWalkSkipCounters` seam and the `source_walk_skip_summary`
|
|
257
|
+
log line: one count per PRUNED DIRECTORY, never per file inside it
|
|
258
|
+
(enumerating the contents would defeat the pruning). The
|
|
259
|
+
`SourceCollectionResult` docblock's former "KNOWN GAP" note is now the
|
|
260
|
+
real field doc.
|
|
261
|
+
2. **Tool-facing surfacing.** `ProjectDiagnosticsSnapshot` gains
|
|
262
|
+
`generatedFileSkips` (the raw skip total, all evidence tiers),
|
|
263
|
+
`generatedNameOnlySkips` (the narrower at-risk subset — see below), and
|
|
264
|
+
`generatedDirSkips` (only present, like `scanTruncated`, on a walk that
|
|
265
|
+
actually ran); `lens-engine.ts`'s new `generatedSkipNotice` renders a
|
|
266
|
+
one-line notice mirroring `scanTruncationNotice`'s (#784) style, wired
|
|
267
|
+
into both `pilens_project_scan` (`mcp/server.ts`) and `lens_diagnostics`
|
|
268
|
+
(`tools/lens-diagnostics.ts`). **Review fix (P1, empirically proven):**
|
|
269
|
+
the notice originally keyed off the raw `generatedFileSkips` total,
|
|
270
|
+
which includes STRONG evidence (lockfiles, declaration files,
|
|
271
|
+
minified/bundle/chunk output — expected on almost every real repo) —
|
|
272
|
+
a repo with just `index.ts` + an ambient `.d.ts` + `package-lock.json`
|
|
273
|
+
showed "2 file(s) excluded" on every single scan, forever. It now keys
|
|
274
|
+
off `generatedNameOnlySkips`: the narrower bucket of WEAK name matches
|
|
275
|
+
trusted with NO corroborating evidence check at all (only reachable
|
|
276
|
+
when a caller opts out of the header probe; the default project-walk
|
|
277
|
+
path always enables it, so this is rare-to-zero in practice — meaning
|
|
278
|
+
the notice is meaningful again when it does fire) plus `generatedDirSkips`
|
|
279
|
+
(directory pruning has no escape hatch, so it stays a genuine
|
|
280
|
+
unverified-content signal). Both `ProjectDiagnosticsScanOptions` and
|
|
281
|
+
both tool surfaces also gain `includeGenerated` (default `false`) so
|
|
282
|
+
the notice's opt-out advice is now actually actionable, not just prose.
|
|
283
|
+
`module_report` has no walk-backed section of its own to thread this
|
|
284
|
+
through (it reads the cached review graph, built asynchronously
|
|
285
|
+
elsewhere) — surfaced in the project-diagnostics path only.
|
|
286
|
+
3. **Content-probe escape hatch (the actual behavior change).** A file
|
|
287
|
+
matching a generated-artifact NAME pattern but with no other evidence is
|
|
288
|
+
now KEPT instead of silently dropped. `generated-artifacts.ts` splits
|
|
289
|
+
its path check into STRONG evidence (a generated directory segment, a
|
|
290
|
+
lockfile, minified/bundle/chunk output — always conclusive, never
|
|
291
|
+
rescued) and WEAK evidence (the remaining filename-regex patterns, e.g.
|
|
292
|
+
`gen.ts`, `foo_generated.go`); `classifyGeneratedOrArtifactDetailed`
|
|
293
|
+
requires a WEAK match to be corroborated by a generated-code header in
|
|
294
|
+
the first 4 KB (or the sibling-source probe already checked upstream by
|
|
295
|
+
every walk-driven caller) before treating it as an artifact —
|
|
296
|
+
cheapest-first. With NEITHER piece of evidence, the file is KEPT and
|
|
297
|
+
counted under the new `generatedNameOverrides` counter (also in the
|
|
298
|
+
rollup log line), so the heuristic's rescues stay observable.
|
|
299
|
+
**Review fix (P2, maintainer decision):** minified/bundle/chunk output
|
|
300
|
+
(`bar.min.js`, `vendor.bundle.js`, `runtime.chunk.js`) is STRONG tier,
|
|
301
|
+
not WEAK — the escape hatch's evidence checks are structurally dead for
|
|
302
|
+
it (minifiers strip banners, so the header leg never confirms; the
|
|
303
|
+
sibling probe looks for `app.min.ts`, never `app.js` at a different
|
|
304
|
+
stem, so that leg is unreachable too), which would have made it a
|
|
305
|
+
PERMANENT override rather than an occasional rescue. `app.min.js` next
|
|
306
|
+
to `app.js` is skipped unconditionally, like a lockfile.
|
|
307
|
+
Shipped **ungated**: the existing `LENS_FLAGS`/`.pi-lens.json` toggle
|
|
308
|
+
registry is sized for whole-subsystem behavior (lsp/tests/delta/…), not
|
|
309
|
+
a narrow heuristic refinement, and the new `includeGenerated` scan
|
|
310
|
+
option (item 2) already gives callers a full, actionable opt-out.
|
|
311
|
+
Documented tradeoff: a false-KEEP (one extra file an agent must judge)
|
|
312
|
+
is preferred over the prior silent false-DROP (a real file invisibly
|
|
313
|
+
never analyzed) — this changes walk output for review-graph/word-index/
|
|
314
|
+
project-diagnostics/call-graph: a repo with a real `gen.ts` gains
|
|
315
|
+
coverage (intended), and a repo with a headerless generated file with
|
|
316
|
+
no source twin also gains coverage (accepted per the issue). Invariant
|
|
317
|
+
preserved for lockfiles, declaration files, minified/bundle/chunk
|
|
318
|
+
output, and anything the ignore-matcher/extension filter already
|
|
319
|
+
excluded — none of those are ever rescued.
|
|
320
|
+
|
|
321
|
+
- **Machine-wide test-suite lock (closes #1101)** — `npm test` /
|
|
322
|
+
`npm run test:unit` / `npm run test:integration` now route through the new
|
|
323
|
+
`scripts/with-test-lock.mjs` wrapper, which acquires a single machine-wide
|
|
324
|
+
`~/.pi-lens/test-suite.lock` before running vitest and releases it after.
|
|
325
|
+
Concurrent full-suite runs (several agents on parallel worktrees, plus an
|
|
326
|
+
interactive run) previously each spawned a fork pool sized for a dedicated
|
|
327
|
+
machine and fought over CPU/RAM, producing vitest worker-crash cascades and
|
|
328
|
+
timing-budget flakes that looked like real bugs but weren't; they now
|
|
329
|
+
serialize (queue) instead. A waiting run prints a heartbeat line at least
|
|
330
|
+
every 15s so it never looks hung, and — mirroring the installer's
|
|
331
|
+
`.install.lock` — a lock is stale only after its recorded PID is confirmed
|
|
332
|
+
dead, never on a timer alone. `PI_LENS_TEST_NO_LOCK=1` opts out (CI sets it:
|
|
333
|
+
runners are isolated, nothing to serialize against). Lock core lives in
|
|
334
|
+
`scripts/lib/suite-lock.mjs`, unit-tested for acquire/release, dead-PID
|
|
335
|
+
takeover, and real contention (two concurrent acquisitions serialize).
|
|
336
|
+
AGENTS.md documents the lock plus the companion layer-1 policy for agents
|
|
337
|
+
running tests concurrently.
|
|
338
|
+
- **Source-walk skip observability (refs #1107, phase 1 of 2)** — the source
|
|
339
|
+
walk silently dropped real files whose NAMES match generated-artifact
|
|
340
|
+
heuristics (a real `src/gen.ts` was invisible to
|
|
341
|
+
`collectSourceFilesWithBudgetAsync`, with nothing anywhere saying so), an
|
|
342
|
+
invisible coverage hole for review-graph/word-index/project-diagnostics/
|
|
343
|
+
call-graph. This phase adds observability only — it does NOT change which
|
|
344
|
+
files get skipped (a content-probe escape hatch for name-only matches is
|
|
345
|
+
phase 2, tracked on #1107). `SourceCollectionResult` gains two additive
|
|
346
|
+
optional counters, `generatedOrArtifactSkips` (files dropped by the
|
|
347
|
+
generated/artifact NAME heuristic, `isGeneratedOrArtifact`) and
|
|
348
|
+
`buildArtifactSkips` (files dropped by the sibling-source artifact probe,
|
|
349
|
+
`isBuildArtifact`) — separate from extension-filter and ignore-matcher
|
|
350
|
+
skips, which are policy/config driven rather than name/content heuristics.
|
|
351
|
+
Both `collectSourceFilesWithBudget` and `collectSourceFilesWithBudgetAsync`
|
|
352
|
+
(and therefore every consumer that funnels through them — the review-graph
|
|
353
|
+
builder, project-diagnostics scanner, word index, etc.) populate the
|
|
354
|
+
counters and log a one-line rollup through the existing `logLatency`
|
|
355
|
+
channel (`phase: "source_walk_skip_summary"`) whenever either is nonzero; a
|
|
356
|
+
clean walk with no name/artifact-probe skips emits no new log line.
|
|
357
|
+
`tests/clients/graph-cache.test.ts`'s two remaining `gen.ts` fixtures
|
|
358
|
+
(unrelated to the drift-path test, which already used `alpha.ts`) were
|
|
359
|
+
unknowingly exercising empty-walk graph builds — renamed to
|
|
360
|
+
`buildstamp.ts`/`identitycheck.ts` since their assertions only cover
|
|
361
|
+
buildGeneration/cache-mode plumbing, never node/symbol content, so the
|
|
362
|
+
rename is behavior-preserving.
|
|
363
|
+
|
|
364
|
+
- **LSP diagnostics content binding (refs #1095, first PR)** — diagnostics are now
|
|
365
|
+
bound to the document content they were computed against, so a consumer can ask
|
|
366
|
+
"were these diagnostics computed against what's on disk now?" instead of inferring
|
|
367
|
+
staleness purely from mtime/TTL proxies. When a `publishDiagnostics` notification
|
|
368
|
+
echoes a document `version`, the owning client binds the stored diagnostics to a
|
|
369
|
+
content fingerprint captured at didOpen/didChange SEND time (never a disk read on
|
|
370
|
+
the notification path). `getAllDiagnostics`/`touchFile` results and the
|
|
371
|
+
workspace-diagnostics cache lookup expose a `binding`
|
|
372
|
+
`{version?, contentHash?, boundToCurrentDisk: boolean | "unknown"}`, with
|
|
373
|
+
`boundToCurrentDisk` verified lazily against disk (memoized per file+mtime, reading
|
|
374
|
+
with the identical raw-UTF-8 transform the payload was built with, so CRLF/BOM files
|
|
375
|
+
round-trip correctly). Across multiple contributing clients the merged binding is
|
|
376
|
+
`false` if ANY contributor mismatches disk, `"unknown"` if all are unknown, else
|
|
377
|
+
`true`. `tools/lsp-diagnostics.ts` adopts it: a result whose binding demonstrably
|
|
378
|
+
mismatches disk (`boundToCurrentDisk === false`) is demoted to inconclusive — even a
|
|
379
|
+
non-empty result no longer re-cements the footer with a stale view (the #1092
|
|
380
|
+
re-cementing path). Servers that never report a version yield binding `"unknown"` and
|
|
381
|
+
behave exactly as before (zero regression). Binding state (`bound`/`mismatch`/
|
|
382
|
+
`unknown`) is logged in the `lsp_touch_file` latency metadata so an unbinding server
|
|
383
|
+
is diagnosable. Non-goals (stated for follow-ups): cascade adoption in
|
|
384
|
+
`clients/dispatch/integration.ts` (#1094 in flight), pull-diagnostics resultIds,
|
|
385
|
+
widget per-entry timestamps (#1093), aux retag (#1094).
|
|
386
|
+
- **Cascade adopts LSP diagnostics content binding (refs #1095, second PR)** — the
|
|
387
|
+
cross-file cascade (`clients/dispatch/integration.ts`) now consults the `binding`
|
|
388
|
+
from the first PR instead of trusting a snapshot purely on TTL. A passive neighbor
|
|
389
|
+
snapshot whose diagnostics `boundToCurrentDisk === false` (the server's view diverged
|
|
390
|
+
from disk — e.g. the PRE-fix content) is no longer reconciled into the footer widget;
|
|
391
|
+
it falls through to an active touch on the existing cold-snapshot budget. This kills
|
|
392
|
+
the window where the first cascade after a fix-edit replayed a neighbor's stale
|
|
393
|
+
pre-fix snapshot. `"unknown"` bindings (version-less servers) keep EXACTLY the prior
|
|
394
|
+
TTL-only behavior; `true` reconciles (TTL stays the outer bound). On the active-touch
|
|
395
|
+
path, a bound-false result is treated like `inconclusive` — composed with it into one
|
|
396
|
+
`isConfirmedTouch()` predicate so a future flag can't be missed at just one gate — so
|
|
397
|
+
it neither reconciles nor seeds the recently-clean neighbor cache. `observedAt` stays
|
|
398
|
+
the snapshot's publish time (`entry.ts`) for passive reconciles. Binding-rejected
|
|
399
|
+
snapshots/touches are logged through the cascade channel (`bindingState`). The lazy
|
|
400
|
+
binding getter is re-read from a fresh `getAllDiagnostics()` every cascade run (the
|
|
401
|
+
Map is never retained across turns), with a caveat comment guarding future retention.
|
|
402
|
+
- **Project-level rule policy via `.pi-lens.json` `rules.<id>.disable` / `rules.<id>.select`** — a project's own config can now narrow what diagnostics actually surface.
|
|
403
|
+
Filtering is output-only, so the baseline, widget state, and dedup cache stay
|
|
404
|
+
authoritative and a policy edit never corrupts or resets delta tracking.
|
|
405
|
+
Matching is project-wide: the `<id>` key is a grouping label, not a filter
|
|
406
|
+
scope. Every `disable` list across every key unions into one drop list, and
|
|
407
|
+
every `select` list unions into one allowlist, so
|
|
408
|
+
`"security": { "disable": ["no-eval"] }` works without `security` being a real
|
|
409
|
+
rule id. Disable wins over select project-wide, even across different keys.
|
|
410
|
+
A non-empty `select` anywhere silences every rule not listed, project-wide, so
|
|
411
|
+
it is a big hammer (documented as such). Findings that carry no rule id at all
|
|
412
|
+
are exempt from select, since `Diagnostic.id` is a dedup key rather than a rule
|
|
413
|
+
id and measuring it against an allowlist would have silently dropped blocking
|
|
414
|
+
findings like eslint parse errors. A user lists `no-eval` once and the filter
|
|
415
|
+
covers `no-eval`, `ast-grep:no-eval`, and `no-eval-js`, reusing the rule-id
|
|
416
|
+
normalization that `inline-suppressions.ts` and `tools/lens-diagnostics.ts`'s
|
|
417
|
+
dedup already applied, now consolidated in
|
|
418
|
+
`clients/dispatch/rule-id-normalize.ts` so the three surfaces cannot drift.
|
|
419
|
+
That normalization conflates a rule whose real name ends in `-js` with its stem
|
|
420
|
+
(`prefer-js` with `prefer`), which is now pinned by tests and called out in the
|
|
421
|
+
docs. The policy applies on the per-edit dispatch path
|
|
422
|
+
(`clients/dispatch/dispatcher.ts`, resolved from the project root so a
|
|
423
|
+
package-local `.pi-lens.json` in a monorepo cannot shadow the root's policy,
|
|
424
|
+
matching the root `lens_diagnostics` already used) and across the
|
|
425
|
+
`lens_diagnostics` tool's `mode=delta`, `mode=all`, and `mode=full` paths. Each
|
|
426
|
+
loads the policy map once per call, filters after inline suppression and
|
|
427
|
+
disposition, and re-summarizes so the blocking/error/warning counts reflect the
|
|
428
|
+
drop, including delta mode's carried-over tally and mode=full's structured
|
|
429
|
+
`details` counts. Silencing a rule is not the same as fixing it: the stored
|
|
430
|
+
delta baseline is deliberately unfiltered, so dispatch compares against a
|
|
431
|
+
policy-filtered view of it. Otherwise every disabled finding would sit in the
|
|
432
|
+
"fixed" set on every single dispatch and inflate the agent's resolved tally
|
|
433
|
+
(`trackAgentFixed`) for as long as the rule stayed off. In the project-config
|
|
434
|
+
loader (`clients/project-lens-config.ts`) the lists are tolerantly parsed: a
|
|
435
|
+
non-array, an empty array, or an all-whitespace array is logged once and
|
|
436
|
+
dropped, matching the existing threshold-validation warn-once contract.
|
|
437
|
+
Threshold-only entries are unaffected, and policy-only entries (no `threshold`)
|
|
438
|
+
coexist cleanly. A `rules.<key>` whose value is not an object, or whose keys are
|
|
439
|
+
all unrecognized, now warns instead of failing silent. That covers the two
|
|
440
|
+
shapes a user is most likely to reach for first, both of which parse as valid
|
|
441
|
+
JSON and do nothing: the lists written directly under `rules`, and `only`
|
|
442
|
+
instead of `select`. New `tests/clients/dispatch/rule-policy.test.ts` (32 tests:
|
|
443
|
+
matcher, normalization, project-wide grouping, disable-wins-select, the `-js`
|
|
444
|
+
conflation, the rule-less select exemption, and the hot-path fast return),
|
|
445
|
+
`tests/clients/dispatch/rule-policy-dispatcher.test.ts` (13 tests covering
|
|
446
|
+
dispatch integration including monorepo root-versus-nested config resolution,
|
|
447
|
+
baseline integrity, and the resolved-tally guard),
|
|
448
|
+
`tests/clients/dispatch/rule-id-normalize.test.ts` (7 tests), and
|
|
449
|
+
`tests/tools/lens-diagnostics-rule-policy.test.ts` (14 tests covering the three
|
|
450
|
+
mode paths, `select` on the cache-only paths, the delta carried-over tally, the
|
|
451
|
+
project-diagnostics delta report, and mode=full's structured details).
|
|
452
|
+
Docs: `docs/settings.md` and `docs/globalconfig.md` gained the project-wide
|
|
453
|
+
schema, precedence, the select warning, and the `-js` caveat. #533 hygiene is
|
|
454
|
+
preserved, so typo and unknown-key warnings still surface and foreign LSP
|
|
455
|
+
namespaces stay silent.
|
|
456
|
+
|
|
457
|
+
- **Document the standing maintenance-routines menu (invoke-on-request)** — catalog named maintenance sweeps and their safety rails in `AGENTS.md`.
|
|
458
|
+
|
|
459
|
+
- **Ast-grep dogfood rules for raw JSON store writes and win32 path qualification (refs [#1158](https://github.com/apmantza/pi-lens/issues/1158))** — flag direct JSON store writes and `win32.isAbsolute` qualification calls, with the atomic-write and `isFullyQualified` seams documented as the sanctioned alternatives.
|
|
460
|
+
|
|
461
|
+
- **Helm chart linting (refs #1283, slice A)** — YAML and `.tpl` edits within a chart now run one bounded, canonical-root-deduplicated `helm lint`; warnings remain advisory and chart/template errors block. Rendered-manifest validation remains deferred to slice B.
|
|
462
|
+
|
|
463
|
+
- **Session degradation telemetry in health and performance reports (refs #1292 items 4–5)** — Trust refusals, output-mode suppressions, TypeScript idle evictions, grammar blocks, untrusted formatter skips, and actionable spawn-failure buckets now remain visible as bounded per-session telemetry; LSP pull failures also appear beneath their server in `pilens_health`.
|
|
464
|
+
|
|
465
|
+
- **Remaining silent degradation gates now surface in `pilens_health` (refs #1292)** — LSP breaker trips and diagnostic-timeout tallies, formatter failures, and tree-sitter WASM aborts are recorded with bounded per-session dedupe.
|
|
466
|
+
|
|
467
|
+
- **Deterministic LSP acquisition-race coverage (refs #1292)** — concurrent initialization and an aborted waiter now assert single-client ownership, zero leaked leases, and teardown reaping through the shared interleaving kit.
|
|
468
|
+
|
|
469
|
+
- **Weekly stale open-issue detector (closes #1323)** — the scheduled GitHub workflow reports open issues referenced by closing-shaped `master` commits or issue-named regression tests for human triage; it never closes issues automatically.
|
|
470
|
+
|
|
471
|
+
- **Opt-in compact one-line tool result rendering (closes #1327)** — pi-lens tools can now render as a single theme-aware line (`<status glyph> <tool name> — <summary>`) instead of two rows, via `--lens-compact-tool-line` / `ui.compactToolLine=true` in `~/.pi-lens/config.json`. Default off — behavior is byte-identical when disabled. Expand-to-view-full-output is unchanged.
|
|
472
|
+
|
|
473
|
+
- **HostPorts capability boundary (refs #1358, S2)** -- a typed interface over every host capability the engine consumes (notify, trust, mode, log, emit, status, spawn policy, render, session, workspace, flags, tools), with headless-parity defaults and the existing getter seams as thin adapters; no behavior change, direct-ctx migration deferred to S4.
|
|
474
|
+
- **Global LSP configuration fallback (refs [#870](https://github.com/apmantza/pi-lens/issues/870))** — load shared LSP settings from `~/.pi-lens/lsp.json`, with project configuration taking precedence.
|
|
475
|
+
- **Per-rule configuration controls (closes [#444](https://github.com/apmantza/pi-lens/issues/444))** — enable or disable individual rules through `.pi-lens.json`.
|
|
476
|
+
- **Edit-batch read-guard observability (refs [#1053](https://github.com/apmantza/pi-lens/issues/1053))** — expose batch-level read-guard activity for clearer diagnostics.
|
|
477
|
+
- **Review-graph call evidence (closes [#1070](https://github.com/apmantza/pi-lens/issues/1070))** — include call/reference evidence in project-intelligence reports and codebase models.
|
|
478
|
+
### Changed
|
|
479
|
+
|
|
480
|
+
- **Safe dependency refresh for v4** — updated the pi host SDK and TUI to `0.84.2`, ast-grep NAPI to `0.45.1`, and Biome to `2.5.8`; refreshed the transitive dependency graph to remove the audited high-severity `nanoid` finding. Major or otherwise non-essential updates remain deferred for follow-up validation.
|
|
481
|
+
|
|
482
|
+
- **Installer probe-cache persistence now uses the shared durable-store protocol (closes #1212)** — awaited commits use the shared quarantine-recovering bounded PID lock, authoritative in-lock reread/delta merge, throwing atomic publication, and in-lock mirror refresh while retaining deferred/failed retry outcomes; TTL, existence, and mtime validation remain read-side policy. Turn-state folding remains a future decision, not part of this change.
|
|
483
|
+
|
|
484
|
+
- **Shared durable-store and atomic-stage protocols (refs #1212, closes #1209)** — diagnostic dispositions and actionable-warning suppression now use one locked in-lock-reread/merge/throwing-atomic-write commit seam, while one staging namespace module owns `.tmp-<pid>-<threadId>-<seq>` minting, classification, and bounded own-stage sweeping.
|
|
485
|
+
|
|
486
|
+
- **Standalone managed-tool clients share one typed availability/install seam (closes [#1290](https://github.com/apmantza/pi-lens/issues/1290), refs [#1214](https://github.com/apmantza/pi-lens/issues/1214))** — madge, Knip, Biome, and ast-grep now join Ruff/jscpd behind runner helpers. Single-command clients use the cached typed checker; ordered-candidate clients retain their local/global/npx/platform search through a typed custom-probe wrapper. The seam also owns managed child environments and read-only discovery, while a source coverage test rejects direct `ensureTool()` calls or bare managed-tool spawns outside sanctioned wrappers. Raw ENOENT consumers in `lsp/launch.ts` remain for #1214's safe-spawn-wide taxonomy.
|
|
487
|
+
|
|
488
|
+
- **Cooperative word-index startup work (closes #1224, closes #1225, closes #1226, refs #1215)** — centralizes monotonic event-loop work budgets, time-budgets shared source walking and word-index reads, yields within high-document-frequency posting filters while publishing each replacement atomically, and reuses the bounded refresh preflight file/stat set for full rebuilds instead of walking and stating it twice.
|
|
489
|
+
|
|
490
|
+
- Ruff and jscpd availability now use the shared dispatch checker, including typed missing-command installation, in-flight dedupe, install-failure suppression, and session reset (refs [#1290](https://github.com/apmantza/pi-lens/issues/1290), phase 1; not closing the issue). The seam adds configurable probe timeout and a managed-command fast path to preserve jscpd's 1500ms PATH probe and managed-tools lookup.
|
|
491
|
+
|
|
492
|
+
- **Review-graph persistence lifecycle telemetry (refs #1048)** — `review-graph.log` now correlates graph/build and persistence generations, sequence hints, graph timestamps/counts, exact total/persisted node/edge/file coverage, source-walk truncation, coalescing and supersession, worker outcomes, and fallback status with bounded metadata, while preventing stale-generation fallback writes; source contents and paths are never added to the new metadata.
|
|
493
|
+
|
|
494
|
+
- **Prompt-cache context attribution (refs #1018)** — add bounded `cache_context` latency records that correlate context injection sources, placement, message counts, sizes, and privacy-preserving hashes with provider `cache_usage` by session/turn, while keeping provider cache misses and local prefix changes distinct.
|
|
495
|
+
- **Friendlier AST search (refs #125)** — correct agent-facing examples, add exact grammar-kind and explicit recursive descendant searches, bound explicit path lists, keep pagination output aligned with `maxMatches`, and document lazy-tool activation timing.
|
|
496
|
+
|
|
497
|
+
- Document red-first regression-test and worktree-hygiene conventions
|
|
498
|
+
|
|
499
|
+
- **Adopt commit-message and doc/prose (Google + STE) writing conventions**
|
|
500
|
+
|
|
501
|
+
- **Refresh README/docs for v4 and update contributors** — reconciled the
|
|
502
|
+
README feature list and `docs/` reference against current code: documented
|
|
503
|
+
the four memory idle-eviction env vars (`PI_LENS_TS_IDLE_EVICT_MS`,
|
|
504
|
+
`PI_LENS_WORD_INDEX_IDLE_EVICT_MS`, `PI_LENS_PROJECT_SNAPSHOT_IDLE_EVICT_MS`,
|
|
505
|
+
`PI_LENS_REVIEW_GRAPH_IDLE_EVICT_MS`), the session degradation ledger
|
|
506
|
+
surfaced through `pilens_health`/`/lens-health`, deferred-format bounded
|
|
507
|
+
concurrency, and the `--lens-guard` commit/push blocker; linked
|
|
508
|
+
`docs/mcp.md` from the README and added an MCP-server bullet. Added six
|
|
509
|
+
external contributors (Nathan Cooke, Eli Stark, Marvin Aziz, Mark Faga,
|
|
510
|
+
aeturnal, floatGray) to the README contributors table, sourced from
|
|
511
|
+
`git shortlog` and merged-PR authorship.
|
|
512
|
+
|
|
513
|
+
- **Dogfood the shape-2 path-semantics rule (refs #1158)** — Flag host-default path operations inside branches committed to Windows path semantics.
|
|
514
|
+
|
|
515
|
+
- **Cooperative-budget acceptance coverage hardened (closes #1215)** — the shared time-budget helper's occupancy and abort-latency guarantees are now locked by non-vacuous regression tests at 800-item scale (assertions within a small multiple of the budget); no runtime behavior change.
|
|
516
|
+
|
|
517
|
+
- **Per-entry changelog files end merge-train conflicts (closes #1321)** — User-facing changes can now land as independent per-entry files and roll up in the version-bump PR before normal CI and tag-time verification.
|
|
518
|
+
|
|
519
|
+
- **TypeScript language services release idle program memory (refs #1332)** — After five inactive minutes (configurable with `PI_LENS_TS_IDLE_EVICT_MS`), TypeScript LSP clients shut down and rebuild transparently on the next request instead of retaining fully hydrated programs indefinitely.
|
|
520
|
+
|
|
521
|
+
- **Terminal behavior now follows the pi host's run mode (refs #1334)** — pi-lens reads `ctx.mode` (`"tui" | "rpc" | "json" | "print"`) instead of assuming it owns an interactive terminal. The diagnostics widget mounts only in `tui`, and proactive `ui.notify` chatter is logged rather than rendered in the one-shot `print`/`json` modes, so piped and machine-readable runs stay clean. `/lens-widget-toggle` now says which mode is blocking it instead of blaming the pi version. Hosts that expose no `mode` field behave exactly as before.
|
|
522
|
+
|
|
523
|
+
- **Enforce the host-SDK type-only import rule that keeps clean installs working (refs #1334)** — `@earendil-works/pi-coding-agent` is an optional peer that pi omits at install time, so a value import of it breaks pi-lens at user sites. That rule was prose only; a regression test now scans every shipped source file for static, dynamic and `require` value imports and fails the build on one. The edit-tool result payload also now uses the host's own `EditToolDetails` type instead of a locally re-declared shape.
|
|
524
|
+
|
|
525
|
+
- **A formatter that exits nonzero is now reported as a failure (closes #1337)** — formatter exit-code strictness was opt-in, so any formatter that had not opted in turned every nonzero exit — rejected flags, a crashed binary, an unparseable file — into `{ success: true, changed: false }`, indistinguishable from "already formatted". That default is what let `ruff format` silently no-op for a full release cycle (#1336). The seam is now strict by default, with an audited opt-out (`lenientExitCode`, carrying its evidence) for the four lint-autofix formatters whose exit status reports remaining offenses *after* a successful rewrite: `rubocop -a`, `standardrb --fix`, `ktlint -F`, and `sqlfluff fix`. **Behavior change:** the other 29 formatters now surface an error where they previously reported a silent no-op — most visibly, saving a file with a syntax error reports the formatter's parse error instead of quietly doing nothing. Three supporting fixes make that surfacing trustworthy: Biome now passes `--no-errors-on-unmatched` (it exits 1 on paths its own `biome.json` ignores, which would otherwise report a failure on every edit under an ignored directory); the reported message skips decorative banners and falls back to stdout, so tools that report there — Biome, ktlint, `mix format` — no longer surface a bare exit code; and the PowerShell formatter now runs with `$ErrorActionPreference = 'Stop'` plus a null guard, so a failing `Invoke-Formatter` reports the failure instead of exiting 0 and truncating the file it was asked to format.
|
|
526
|
+
|
|
527
|
+
- **Re-home probe-cache ageing and cooperative yields (closes #1354)** — authoritative probe-cache commits now prune expired entries while preserving durable-store quarantine recovery; the remaining corpus-scaled scan yields use the cooperative time budget.
|
|
528
|
+
|
|
529
|
+
- **Warm dispatch, LSP async consumers, and formatter catalog lazily at session start (refs #1394 — Phase 2).**
|
|
530
|
+
|
|
531
|
+
### Removed
|
|
532
|
+
|
|
533
|
+
- **Dead-code cleanup: 5 orphaned production modules + the `server-strategies.ts` deprecated shim (refs #1090)** — `clients/project-metadata.ts` (orphaned since the `/lens-booboo` removal, #397/#396), `clients/lsp/interactive-install.ts` + its test (superseded by `resolveAndLaunch`, #374/#375), `clients/ast-grep-parser.ts` (superseded by the napi runner, #197), `clients/types.ts` (initial-commit LSP types, superseded by `LSPDiagnostic` in `clients/lsp/client.ts`), and `clients/lsp/lsp-index.ts` (stillborn barrel, zero references ever) all had zero remaining importers, re-verified at current master (including dynamic `await import()` patterns). `clients/lsp/server-strategies.ts` (a `@deprecated` re-export shim over `wait-policy/strategies.ts`) is also deleted after repointing its last two consumers — `scripts/probe-clean-signal.mjs` and `tests/clients/lsp/server-strategies.test.ts` — directly at `clients/lsp/wait-policy/strategies.ts`, plus ~25 stale doc/comment references across `CONTRIBUTING.md`, `docs/lsp-capability-matrix.md`, and the LSP/probe source. Non-goals: `clients/module-report-lsp.ts` and `clients/source-groups.ts` (parked pending #236 / roadmap decisions) and the un-export candidates identified in #1090 remain untouched.
|
|
534
|
+
|
|
11
535
|
### Fixed
|
|
12
536
|
|
|
537
|
+
- **Git guard closes shell-expansion and command-wrapper bypasses (refs #1084, PR #1395)** — command-position normalization now handles all supported IFS and positional-parameter forms, and guarded verbs nested in known or unknown command-string launchers fail closed while literal text remains non-blocking.
|
|
538
|
+
|
|
539
|
+
- **LSP roots and per-session clients are conservatively bounded (closes #1325, refs #1126, refs #1129)** — manifest-bearing fixture conventions (`tests/fixtures`, `__fixtures__`, and `testdata`), gitignored directories, and atomic-write staging namespaces no longer become standalone LSP roots; their files attach to the nearest eligible ancestor project. Client reuse remains keyed by server and normalized resolved root, while a configurable `PI_LENS_LSP_CLIENT_CEILING` (24 by default) evicts the least-recently-used idle client through graceful shutdown before spawning another and never evicts a client with an active LSP request.
|
|
540
|
+
|
|
541
|
+
- **Fully qualified Windows paths use one shared classifier (closes [#1213](https://github.com/apmantza/pi-lens/issues/1213))** — drive-relative (`C:foo`) and rooted-relative (`\\foo`) paths are no longer mistaken for self-contained absolute paths; UNC and drive-absolute paths remain fully qualified across command validation, installer caching, and managed-tool retention.
|
|
542
|
+
|
|
543
|
+
- **Kotlin formatter selection follows Spotless configuration deterministically (closes #1306)** — a Spotless `kotlin { ktlint() }` block pins ktlint and `kotlin { ktfmt() }` pins ktfmt, so both selection branches cannot claim the same Kotlin file and unconfigured projects retain the style-preserving no-format policy.
|
|
544
|
+
|
|
545
|
+
- **Managed npm clients see and execute off-PATH installs (closes [#1289](https://github.com/apmantza/pi-lens/issues/1289))** — madge and Knip availability probes receive the managed environment, Knip threads the same environment into analysis, Biome checks the managed absolute candidate before its npx fallback, and every install result is retained as the command used by the client.
|
|
546
|
+
|
|
547
|
+
- **Turn-end madge telemetry stays compact and its batch metadata is runtime-tested (closes [#1250](https://github.com/apmantza/pi-lens/issues/1250), [#1251](https://github.com/apmantza/pi-lens/issues/1251))** — aggregate batch counters remain exact, while per-target timing breadcrumbs are retained only for spawns taking at least 100 ms and capped at 12 slow targets, preserving the smells-rollup lookback; the real turn-end path now verifies madge batch execution and metadata propagation.
|
|
548
|
+
|
|
549
|
+
- **Rule-policy normalization and source-filter matrices no longer drift (closes #1087)** — P3.2 derives CodeRabbit's language suffixes from the vendored rules tree so disable/select and inline suppression cover every shipped language; P3.3 aligns source-twin precedence ordering with sibling resolution and adds a behavioral agreement guard while preserving the broad `.jsx` build-artifact fallback.
|
|
550
|
+
|
|
551
|
+
- **Smart-default formatters preserve existing indentation when a repository has no style configuration (closes [#1144](https://github.com/apmantza/pi-lens/issues/1144))** — Biome and shfmt now infer and pin a file's indentation for unconfigured formatting, skipping when their style cannot be inferred; Prettier and Ruff receive the same fallback indentation flags. Explicit formatter configuration and `.editorconfig` remain authoritative.
|
|
552
|
+
|
|
553
|
+
- **Bus emitters no longer retain a stale session context after replacement (closes #1128)** — all pi-lens event publishers resolve the live `pi.events` emitter at delivery time through a shared getter seam; deferred lens events also resolve inside their `setImmediate` callback. The sweep found no other outliving session-context emitter closures: the quiet-window summary already uses its current-context holder and its stale-session guard; LSP repaint callbacks capture UI primitives intentionally rather than `ctx`; remaining lifecycle callbacks use the event-time context or runtime generation gates.
|
|
554
|
+
|
|
555
|
+
- **Durable disposition marks no longer lose sibling-process updates (closes [#1202](https://github.com/apmantza/pi-lens/issues/1202))** — the repo-wide shared-store audit classifies every durable writer and records accepted best-effort caches explicitly. The sole unguarded behavior-gating read/modify/write store, `diagnostic-dispositions.json`, now takes a bounded pid-owned cross-process lock, re-reads under that lock, and merges only its anchor delta, so an older writer cannot promote a stale whole-file snapshot over a concurrent mark.
|
|
556
|
+
|
|
557
|
+
- **Behavior-gating durable stores no longer lose sibling-process updates (closes [#1202](https://github.com/apmantza/pi-lens/issues/1202))** — the repo-wide shared-store audit classifies every durable writer and records accepted-loss telemetry explicitly. Diagnostic dispositions and actionable-warning suppression state now share a bounded pid-owned cross-process lock, re-read under that lock, and merge only the caller's delta before atomic replacement. Disposition telemetry fires only after persistence succeeds, and turn-state clear/cycle/deferred-format paths now enforce the same live-owner boundary as edit registration.
|
|
558
|
+
|
|
559
|
+
- **Tool discovery caches now recover from session changes and deleted binaries (closes [#1203](https://github.com/apmantza/pi-lens/issues/1203))** — cwd-scoped runner probes and the shared ast-grep memo follow the existing dispatch session generation, while installer in-memory positives revalidate absolute paths and evict the matching persisted entry before re-probing when the binary disappears.
|
|
560
|
+
|
|
561
|
+
- **Managed ruff, jscpd, and madge installs are now actually used after auto-install (refs [#1289](https://github.com/apmantza/pi-lens/issues/1289))** — retain the absolute path returned by `ensureTool` so off-PATH managed shims are spawned directly instead of falling back to the bare command name.
|
|
562
|
+
|
|
563
|
+
- **The shared async walk engine no longer reads directories synchronously (refs [#1137](https://github.com/apmantza/pi-lens/issues/1137), [#1170](https://github.com/apmantza/pi-lens/issues/1170))** — `walkTreeStackAsync` (`clients/source-walker.ts`) already `setImmediate`-yielded every N entries, but every per-directory read still went through `readDirEntriesSafe` → `fs.readdirSync`. Chunked yielding only covers the CPU axis: on a cloud/network-backed tree (OneDrive) one stalled directory read held the Node event loop — and pi's TUI — for the entire stall, no matter how often the walk yielded around it. This is the exact shape #1170 fixed in `pipeline.ts`'s autofix snapshot walk while explicitly deferring the shared engine; fixing it here means every async walker (`collectSourceFilesAsync`, `countSourceFilesWithinLimitAsync`, and so the session-start source scans and word-index refresh) inherits the fix at once. The traversal itself is untouched: the one shared generator now yields a directory-read *request* and each driver satisfies it — `readdirSync` for the sync driver, `fs.promises.readdir` for the async one — so the sync/async pair still cannot drift, and visit order, `maxFiles` cap behaviour and results are identical. `tools/lsp-diagnostics.ts`'s directory-mode `collectFiles` walk is converted the same way (it was fully synchronous with no yielding at all, bounded only by files *kept*, and ran once per language in the `LANG_EXTENSIONS` loop); it keeps its immediate-descent recursion rather than adopting the stack-based shared driver, because the `maxFiles` cap makes traversal order observable. A `measureMaxSyncBlockMs` fail-then-pass occupancy screen injects the same per-directory stall into both APIs and pins that the async walk no longer holds the loop (verified to fail against pre-fix code: a 420ms block against a 30ms budget).
|
|
564
|
+
|
|
565
|
+
- **Direct-write cache stores now publish JSON atomically (refs [#1239](https://github.com/apmantza/pi-lens/issues/1239) [#1205](https://github.com/apmantza/pi-lens/issues/1205))** — turn state, rule, diagnostics, call-graph, codebase-model, actionable-warning, and metrics-history stores stage JSON through the shared tmp-plus-rename writer, so a process killed during a write leaves the previous complete cache value rather than a truncated file.
|
|
566
|
+
|
|
567
|
+
- **Dispatch's install-failure suppression never cleared, and a completed install could keep serving a pre-install madge resolution (closes #1266, refs #1276)** — `handleSessionStart`'s install-attempt suppression window (`installFailureReasons`, #1222/#1223) had no reset hook, so a tool that legitimately failed once stayed suppressed for the rest of the process even across sessions; it is now reset at session start for both sequential and MCP session starts (the existing concurrent-session guard already prevents a subagent session start from over-resetting a live parent). Separately, `DependencyChecker`'s madge managed-path memo (#1276) is keyed only by `projectRoot` but reads PATH/discovery/managed-install state that a completed install changes, and `finishInstallAttempt` dropped it via a fire-and-forget dynamic import — a caller that started the next madge resolution the instant `installTool`/`ensureTool` resolved could still observe the stale pre-install memo, and a rejected import became a silent unhandled rejection with no invalidation at all. The reset is now `await`ed before `installTool`/`ensureTool` return, wrapped in try/catch that logs (rather than swallows) an import failure, so a resolution issued right after a successful install deterministically sees the reset memo. The checker's own instance registry (needed so the reset reaches every live checker, not just whichever one ran the install) now holds instances via `WeakRef` instead of a strong `Set`, so short-lived checkers (tests, reinit) can be collected instead of retained for the process's lifetime; and the bare-command spawnability revalidation `resolvedCommandIsStale` added under #1276 is now cached per checker (invalidated by that same reset) instead of re-running a synchronous PATH scan on every cached resolution.
|
|
568
|
+
|
|
569
|
+
- **Orphan staging-file reaping no longer deletes legitimately named files (refs [#1267](https://github.com/apmantza/pi-lens/issues/1267), [#1260](https://github.com/apmantza/pi-lens/issues/1260))** — the session-start atomic-stage sweep now requires the full writer-emitted `.tmp-<pid>-<threadId>-<seq>` shape with strict, unpadded decimal components, so names such as `backup.tmp-000123-02-03` are left alone while genuine current-shape dead-pid orphans are still reaped.
|
|
570
|
+
|
|
571
|
+
- **`no-bare-except` and `duplicate-function-arg` grammar coverage, including a binding-aware fix for the broadened traversal (closes [#1269](https://github.com/apmantza/pi-lens/issues/1269), closes [#1270](https://github.com/apmantza/pi-lens/issues/1270))** — `no-bare-except`'s `not: any: [...]` exception-spec enumeration (identifier/attribute/tuple/parenthesized/as_pattern/subscript) was a denylist that any new node shape could slip past; it now matches the grammar's bare `except:` syntax directly (`pattern: "except:"`), so a future exception-spec kind cannot become a false negative by omission. `duplicate-function-arg`'s query previously bound `(required_parameter (identifier) @NAME)` with no depth restriction, so a rest parameter (`...a`) and other non-`required_parameter` shapes went unmatched; the query and its `same_param_name` post-filter's `bindingNames` helper were broadened to walk the full parameter pattern (covering rest params, arrow functions, and destructured/renamed collisions). That broadening over-corrected: `bindingNames` treated every descendant `identifier` as a bound name, so a reference inside a destructuring **default** (`{a = b}`, `[a = b]`, `{x: a = b}`, `{a = (() => b)}` — the `b` is a default-value expression, not a binding) or inside a **computed property key** (`{[key]: a}` — `key` is a reference) was wrongly counted, producing false-positive duplicate reports against an unrelated same-named parameter elsewhere in the list. `bindingNames` is now binding-aware: it descends into an `assignment_pattern`/`object_assignment_pattern`'s `left` field only (never `right`, the default expression) and skips `computed_property_name` entirely, while still walking nested destructuring binding positions so `{a}`/`[a]`/renamed-binding collisions keep flagging. (`{a = a}, a` legitimately still flags — the left side of `{a = a}` binds `a`, colliding with the second parameter's own `a`; Node itself rejects that snippet with "Duplicate parameter name not allowed in this context".) Both false-positive fixtures were confirmed to reproduce against the pre-fix traversal before the change and pass after.
|
|
572
|
+
|
|
573
|
+
- **Stop-hook turn-end residuals: stdin hang, invisible skips, unguarded client cwd, delivery-loss on the ack error path, and transcript rendering (refs [#1271](https://github.com/apmantza/pi-lens/issues/1271), [#1272](https://github.com/apmantza/pi-lens/issues/1272), [#1273](https://github.com/apmantza/pi-lens/issues/1273), [#1274](https://github.com/apmantza/pi-lens/issues/1274), [#1275](https://github.com/apmantza/pi-lens/issues/1275))** — five defects found in post-merge review of #1211, all on the turn-end path.
|
|
574
|
+
- `pi-lens-analyze`'s `main()` awaited the stdin read BEFORE examining `--file`/`--turn-end`, with no timeout and only an `isTTY` guard, so any spawner that left a piped stdin open hung the bin forever — a regression on the pre-existing `--file` path, and as a Stop hook a full 60 s Claude Code timeout every single turn. argv now wins: stdin is read only when neither flag answers the question, and the read is raced against a 2 s timer so a stuck pipe degrades to "no payload" instead of hanging. Every existing test called `child.stdin.end()`, which is exactly why the suite could not catch this; the two new guards deliberately do not.
|
|
575
|
+
- Every turn-end skip wrote one line to **stderr** and exited 0, and Claude Code does not surface stderr from a hook that exits 0 — so a permanently dead integration was byte-for-byte indistinguishable from a clean turn, indefinitely. The skip now also goes to stdout (the hook's only transcript-visible channel, one line), the message names the actual wire reason instead of always claiming "no warm pi-lens MCP server" (absent server, slow pass, schema skew and stale build have different remedies), and the hook records `{ran, skipped, lastSkipReason, lastRunAt}` per workspace so `pilens_health` reports it — the #544 precedent applied to the hook process, which is separate and short-lived and therefore left no other trace. That per-workspace status file is a shared-tmpdir read-modify-write two concurrent Stop hooks for the same workspace can race; the write now routes through `writeFileAtomic` (`clients/atomic-write.ts`) so a race can no longer torn-read the file (parse failure resetting both counters) or hit a Windows sharing violation — the read-modify-write itself can still drop one increment under a true race, which is accepted for this bounded best-effort telemetry.
|
|
576
|
+
- The warm turn-end IPC route trusted the client-supplied `cwd`, called `ensureReady` on it and ran **that directory's configured test runner** there. Turn-end and its ack now reject any cwd that is not the server's own `DEFAULT_CWD` or inside it, and log the mismatch; every legitimate client already derived the socket path from that same cwd, so the guard costs nothing legitimate. A subdirectory of the workspace stays accepted. The guard's first cut wrongly rejected the exact workspace root on win32 whenever the cwd it was handed differed from `DEFAULT_CWD` only in case (drive letter or a path segment): `path.relative` is case-insensitive there and returns `""` for that pair, but the guard's `rel !== ""` term read that as "outside the workspace" — a real gap, since Claude Code's Stop-hook cwd can legitimately differ in case from how the server derived its own `DEFAULT_CWD` (`ipcPathForCwd`/`workspaceHash` in `clients/mcp/ipc.ts` already `.toLowerCase()` for exactly this reason), silently no-opping the feature at the root on every turn. Fixed by dropping the redundant `target === DEFAULT_CWD` fast path and the `rel !== ""` term together — `rel === ""` only ever means "this is the workspace root" and must be accepted, not rejected.
|
|
577
|
+
- `acknowledgeTurnEnd` deleted the pending delivery BEFORE calling `commit()`, so a throwing consume destroyed the only handle to the findings — the exact loss the two-phase protocol exists to prevent, reintroduced on its error path. It now commits first and drops the capability only on success. `pendingTurnEndDeliveries` also had no expiry and no size cap (every timed-out client and every distinct raw-cwd alias left an entry for the life of the process); it now expires at 10 min and caps at 8, both dropping entries **without** committing so the findings re-arm for a later Stop rather than vanishing. The route no longer starts a heavy pass for a client whose socket is already destroyed, and byte-identical in-flight Stop requests (no files, no sessionId) coalesce onto the one pass instead of a third being rejected outright by the single-waiter queue.
|
|
578
|
+
- Transcript rendering: the 2000-character cap sliced UTF-16 code units and split surrogate pairs, putting U+FFFD where an emoji marker belonged; the cut now backs off a high surrogate. The `tests` section is the raw vitest failure dump, so ANSI escape sequences and C0 control characters (newline/tab excepted) are stripped before capping, which also removes the half-sliced-escape leak. The injection-framing strip was `startsWith`-only and now removes every occurrence, so a token carried into the middle of a joined section cannot survive.
|
|
579
|
+
|
|
580
|
+
- **Marksman silent-clean confirmation now requires a liveness check, so a wedged server is no longer rendered clean (refs [#1277](https://github.com/apmantza/pi-lens/issues/1277))** — #1253 converted a diagnostics timeout on a `tier3-silent` server (e.g. Marksman) into a confirmed-clean verdict from the static capability classification alone. A server that accepted the notify write and then became unresponsive was therefore reported clean rather than inconclusive — the unresponsive-server-as-clean silent-failure shape. The gate now additionally requires `clientPingLiveness`, an active `workspace/symbol` round-trip bounded at 300ms and independent of the notify-write: a wedged server never answers, the timer wins, the ping returns false, and the result stays inconclusive. A genuinely silent-clean server (alive, tier3-silent, no diagnostics) still confirms clean, and the enumerable `confirmation` wrapper contract is unchanged. Guarded by a notify-accepted-then-unresponsive test confirmed to fail against the pre-fix body.
|
|
581
|
+
|
|
582
|
+
- **Stop-hook turn-end findings are no longer lost when the client deadline wins (refs [#1218](https://github.com/apmantza/pi-lens/issues/1218))** — the warm Stop route now separates execution from durable delivery. It returns a delivery capability over one one-shot connection, and consumes the turn-end/test finding bridges only after a receipt acknowledgement on a separate one-shot connection. A timeout, late reply, or connection close leaves findings available for a later authorized Stop; repeated acknowledgements are idempotent. Turn-state writers now carry explicit pi/MCP ownership, so `sessionId:null` cannot erase a shared worklist or let a live foreign session consume it. Spawn tests use isolated temp workspaces/data/home roots and refuse the real workspace socket.
|
|
583
|
+
|
|
584
|
+
- **Markdown `lsp_diagnostics` now preserves Marksman's authoritative silent-clean confirmation (closes [#1253](https://github.com/apmantza/pi-lens/issues/1253))** — `LSPService.touchFile` already treated a successful Marksman notify followed by its expected no-publish clean behavior as confirmed, but the tool boundary retained only the empty diagnostics array and `inconclusive` flag. The tool then reclassified the same empty tier-3 result, attempted the TypeScript-only fallback path, and rendered clean Markdown as inconclusive. `TouchFileResult` now carries explicit confirmation metadata for completed collections, and every local diagnostics request routes through `touchFile` when available — including default all-scope batches with no explicit wait. Primary-scope and non-TypeScript all-scope consumers accept that confirmation; all-scope TypeScript still runs its synchronous tsserver fallback before any clean verdict, so unpublished TypeScript findings remain visible. Absent metadata, failed writes, timeouts, binding mismatches, and ambiguous capability state remain unconfirmed; confirmed clean results remain cacheable and widget-reconcilable, and real findings are unchanged. Two carriage holes that only became reachable once the confirmation was surfaced to the tool are closed with it. (1) `touchFile` recorded its recent-touches debounce entry even when a server's notify write TIMED OUT or rejected, so the next touch inside the 1500ms window skipped the notify entirely and reported `notifyWriteTimedOut: false` — the exact precondition both silent-clean gates (#799 single-server, #814 aggregate) read as "the server saw this content". A `silentOnClean` server that never received the file was therefore confirmed clean one debounce window later; in a batch this fired routinely, because `ensureWarmForSweep`'s warm-up touch is the first push and the per-file touch is the second. The debounce is now keyed **per server** (`file:clientScope:serverId`) and each entry is recorded at the moment that server's own write lands, so a failed write re-pushes instead of laundering into a confirmation. Per-server rather than the file-level "record only when every spawned server's write landed" this fix first shipped as: `recentTouches` carried no server component, so withholding the entry withheld it from every co-touched server, and one wedged auxiliary disabled the debounce for its HEALTHY siblings — re-pushing them on the next touch and clearing diagnostics they had already computed, which is the same per-file collapse #743 removed from the notify-write deadline (and which its own in-file doctrine comment forbids). Keyed per server both hold at once: the stalled server has no entry and is re-pushed, while every sibling whose write landed keeps its own debounce and its original timestamp, so the window still expires naturally instead of being extended by reuse. The version-baseline diagnostics wait is likewise per-server now, so a server that WAS pushed still gets its `minVersion` wait when a sibling was debounced away. Guarded by a two-server test (one stalled, one healthy, same content twice) confirmed to fail against the pre-fix body — the healthy server's `notify.open` was called twice where it must be called once. (2) The warm-attach collection branch returns before the local touch and had no provenance to carry, so a warm-attached session reported every clean Markdown file unconfirmed regardless of the tool-side fix; `WarmDiagnosticsResponse` now carries the incumbent touch's `confirmation` as an explicit enumerable DTO field (same doctrine as `inconclusive`), consumed as an AGGREGATE confirmation because the incumbent always touches `with-auxiliary` — an incumbent that omits the field stays unconfirmed. A confirmed touch whose findings are all filtered away by the requested severity floor now renders **clean at that severity** rather than consulting the empty-result classifier (which answered "unconfirmed" for a tier-3 silent server): the unconfirmed doctrine exists for a server that said NOTHING, not for one that demonstrably answered and had its findings filtered by the caller's own floor. The filtered diagnostics are still cached and widget-reconciled, and the tier-3 sync escape hatch is still reserved for a genuinely empty result. Covered end-to-end: a new suite drives the real `LSPService` through the real tool with only the server registry and client transport faked, so the confirmation has to actually flow rather than being asserted separately on each side of a mock; the incumbent-side DTO composition is pinned separately (a confirmed touch emits the field, an unconfirmed or inconclusive one OMITS the key rather than serializing `undefined`, which is what makes the absent-field degradation byte-identical to a pre-#1253 incumbent and the no-version-bump safe).
|
|
585
|
+
- **Turn-end madge resolved its command once per FILE, and a slow older pass could overwrite newer circular-dep state (refs [#766](https://github.com/apmantza/pi-lens/issues/766))** — `resolveMadge` ran inside `checkFilesBatch`'s per-file mapper, and its `findNodeToolBinary` miss path spawns uncached `npm config get prefix` / `pnpm bin -g` / `yarn global bin` probes, so a turn touching N import-changed files paid that whole chain N times before falling back to `npx madge` — never consulting the managed tools tree pi-lens installs madge into in the first place. Resolution now runs once per project root (memoized on the instance) and falls through local/global bin → `ensureTool("madge", { allowInstall: false })` → `npx madge`; the discovery step can never download, because installation stays owned by `ensureAvailable()`. That memo is deliberately not one-way, since re-probing per spawn is also what used to make resolution self-healing: a memoized absolute path is re-validated with one `existsSync` per resolution, so a madge uninstalled mid-session re-resolves instead of failing every remaining spawn, and the `npx` result is never pinned — it is the slow path being removed, and one transient installer failure would otherwise disable managed resolution for the whole session. The resolved command's `kind` is classified from the returned STRING (`local`/`managed`/`global`/`path`/`npx`) rather than from the step that produced it: `findNodeToolBinary` probes the npm/pnpm/yarn/bun GLOBAL bins as well as the project's `node_modules/.bin`, and the installer's discovery legitimately hands back a bare PATH name or an npm-global path as well as the managed one. `checkFilesBatch` now returns `{ results, stats }`, and turn-end attaches `MadgeBatchStats` — requested/missing/cacheHits/spawned/failed, `commandKind`, `resolveMs`, and per-target durations capped at 12 with `targetsTruncated` so the cap is never silent — as metadata on the existing `phase: "madge"` latency entry, which as one whole-phase duration could not distinguish many files from one slow file from command resolution. Separately, `lastCircular`/`circularFiles` are shared instance state that `checkFile`, `checkFilesBatch` and `scanProject` all overwrite wholesale while genuinely overlapping (turn-end against the background scans in `runtime-session.ts` and `project-diagnostics/fresh-fetch.ts`): each writer now takes a generation when it CLASSIFIES and publishes only while that generation is still the newest, so an operation that started against older file content can no longer resurrect that view over a newer one, whatever order the spawns finish in. The batch folds into a batch-local copy and publishes once at the end, so its returned map keeps the array-order sequential equivalence either way, and both guard tests were confirmed to fail against the unguarded publish rather than merely to pass after it. No result cache was added (#533, "when in doubt, re-run") — changed content still re-runs madge, and the guard only skips writes that a newest-wins sequential interleaving would also have discarded. A content-fingerprint single-flight map was considered and dropped as unreachable: `importsChanged` writes its `{timestamp, size}` entry synchronously during classification and neither caller has an interleaving await there, so a second overlapping operation on the SAME content always classifies as a cache hit and never reaches a spawn. Scope honesty: `isInCircular`/`getCircularForFile` have no production callers and turn-end only dbg-logs `hasCircular`, so the guard's payoff today is the correctness of future cache-hit results rather than user-visible output — which is input to the still-open step 4 of #766 (whether the turn-end madge pass should exist at all).
|
|
586
|
+
- **`duplicate-function-arg` no longer fires a false duplicate when a parameter's default-value expression references an earlier parameter name (closes [#1256](https://github.com/apmantza/pi-lens/issues/1256))** — the query bound `(required_parameter (identifier) @NAME)` with no depth restriction, so an identifier at **any depth** inside the second parameter's subtree — the default value (`root = node`) or a type annotation — captured as `@NAME`, and `same_param_name` compared it to the first parameter's name and fired. Dogfooded live: `containsYieldInFunctionBody(node, root: TreeSitterNode = node)` in `clients/tree-sitter-client.ts` reported a phantom duplicate on every dispatch. Both captures now bind through `pattern: (identifier)`, the field the grammar actually exposes (`name:` does not exist on `required_parameter` — it would silently kill the rule; `pattern:` is already used by the sibling `variable-shadowing.yml`), so only the parameter NAME can match. Regression tests: a default-value reference must NOT match, and genuine duplicates with a default-valued earlier parameter must still match.
|
|
587
|
+
|
|
588
|
+
- **Autofix surfaces now apply the same config as the lint surfaces that reported the diagnostics (closes [#1247](https://github.com/apmantza/pi-lens/issues/1247))** — `markdownlint-cli2 --fix`, `ruff check --fix` and `biome lint --write` ran with NO `--config`, so on projects without their own config the tool's default ruleset rewrote files per rules the lint surface never endorsed: markdownlint's default all-rules-on config produced whole-file rewrites of CHANGELOG.md/AGENTS.md and `#946` → `# 946` heading-spacing corruption (MD018), while ruff/biome applied default rule sets instead of the package-owned `core.toml`/`core.jsonc`. The lint runners and the autofix paths now consume ONE shared config-args seam (`markdownlintConfigArgs`/`ruffConfigArgs`/`biomeConfigArgs` in `clients/tool-policy.ts`) — package-owned fallback when the project has no config, the user's config when present — so the two surfaces cannot drift again. `biomeClient.fixFileAsync`/`ruffClient.fixFileAsync` gained an optional dispatch-cwd for discovery. Guarded by builder-level tests (fallback + user-config states for all three tools) and a pipeline-level test pinning the `--fix` invocation args.
|
|
589
|
+
- **Three hand-rolled `.tmp-<pid>` staging writers bypassed `atomic-write.ts` and never inherited the #1205 fix, and the fixed staging name still collided across worker threads (closes [#1205](https://github.com/apmantza/pi-lens/issues/1205), closes [#1217](https://github.com/apmantza/pi-lens/issues/1217), refs [#1202](https://github.com/apmantza/pi-lens/issues/1202))** — #1205 made `writeFileAtomic`'s staging path unique per call, but only for writers that actually route through the module. `clients/instance-reaper.ts`'s `pruneDeadInstances` (machine-global `~/.pi-lens/instances.json`), `clients/gzip-stage-write.ts` (both persist workers' streamed-gzip stage write) and `clients/grammar-source.ts`'s `downloadGrammar` each interpolated their own per-PROCESS staging name, so all three still published torn files exactly as pre-#1205 code did. The reaper was the reachable one: `instances.json` is written fire-and-forget from `instance-registry.ts`'s `prunePids` as well, with nothing serializing the two, and `readInstanceRegistry` degrades a parse failure to empty — so a tear dropped **every** registered instance rather than the one entry being pruned. Reproduced against the pre-fix body at **22/40 unparseable on Linux**, the same scale as #1205's own 21/40. All three now source their staging name from `atomic-write.ts` (`writeFileAtomicAsync` for the reaper and `writeFileAtomic` for the grammar fetch, which also gains the failure-path staging cleanup neither hand-rolled copy had; `stagePathFor` alone for `gzip-stage-write.ts`, which needs its own streaming pipeline). `stagePathFor` itself gained `threadId`: its counter is per-THREAD (a worker gets its own module instance, so its own `_stageSeq` starting at 0) while `process.pid` is shared across every thread, so two threads each minted the identical `${target}.tmp-<pid>-0` — #1205's shared-inode tear reintroduced one axis over, and no longer hypothetical now that `gzip-stage-write.ts` is a real worker-side caller. The staging name is therefore `${target}.tmp-<pid>-<threadId>-<seq>`, covering the three ways two writes can overlap — pid across processes, thread id across threads, counter within one thread — and `STAGE_TMP_PATTERN` grew a third optional group so sweepers still collect leftovers in all three shapes on disk (`.tmp-<pid>`, `.tmp-<pid>-<seq>`, `.tmp-<pid>-<threadId>-<seq>`). `gzip-stage-write.ts`'s docstring claimed "the `stagePath` itself is only ever created by the atomic rename, so a crash mid-write can never leave a torn stage file behind" — true for a crash, false for concurrency, the same false-confidence pattern #1205 found in `atomic-write.ts`'s own docstring — and now scopes its guarantee to crash-safety and states that it provides no ordering and no isolation beyond tear-freedom. The repo-wide sweep #1217 asked for ran over every `rename` call site rather than a `.tmp-` grep, which is what surfaced `grammar-source.ts` (missed by the incidental discovery that filed #1217); the remaining renames are log rotation, installer moves, and LSP workspace edits, none of which stage. Every new guard was confirmed to fail against the pre-fix body on Linux, not merely to pass after (recurring defect shape 2/7): the reaper tear at 22/40, the gzip writers' renames at `ENOENT`, and two worker threads minting one byte-identical staging path.
|
|
590
|
+
|
|
591
|
+
- **The review graph's stale-stage sweep deleted OTHER modules' in-flight atomic-write staging files out of the shared project cache dir (refs [#1206](https://github.com/apmantza/pi-lens/issues/1206), [#1202](https://github.com/apmantza/pi-lens/issues/1202))** — `sweepStaleStageFiles` (`clients/review-graph/builder.ts`) matched `entry.includes(".stage-") || /\.tmp-\d+$/` and `fs.rm`'d the result out of `getProjectDataDir(cwd)/cache`. The `.tmp-<pid>` alternative is the generic staging shape `clients/atomic-write.ts` produces for **every** durable store — diagnostic dispositions, cache-manager scanner data, project snapshot, call graph, codebase model, workspace/project diagnostics caches — not the review graph's property. A review-graph persist that readdir'd the cache dir while another process sat between its `writeFileSync` and its `renameSync` deleted that staging file, so the rename threw `ENOENT` (propagating out of `markDisposition` into the agent's mark tool for `bestEffort: false` stores) or silently dropped the update for `bestEffort: true` ones — one subsystem's garbage collection reaching into unrelated subsystems' transactions, and the inverse of the atomic-write contract. The predicate is now scoped to artifacts the review graph itself produces (basename starts with `review-graph.` **and** contains `.stage-`, covering `review-graph.json.gz.stage-<pid>-<gen>`, the checkpoint stage, and either's persist-worker `<stage>.tmp-<pid>`), and an entry whose embedded stage pid is still alive is skipped via the reaper's conservative `realIsPidAlive` (ESRCH-only-means-dead) rather than a second hand-rolled liveness probe — so a concurrent healthy owner's in-flight stage survives, and a recycled pid leaks one stage file rather than destroying a live one. Because the bare `.tmp-<pid>` shape is no longer matched at all, this is independent of the per-call-unique staging rename in #1205; the now-unowned orphaned-atomic-write-temp GC gap is tracked separately by #1228.
|
|
592
|
+
|
|
593
|
+
- **`writeFileAtomic` published torn files for concurrent same-process writes — the staging name was per-PROCESS, not per-CALL (refs [#1205](https://github.com/apmantza/pi-lens/issues/1205), [#1202](https://github.com/apmantza/pi-lens/issues/1202))** — `clients/atomic-write.ts` staged every write at `${target}.tmp-${process.pid}`, so two in-flight writes from one process to one target shared a staging inode: both `open(O_TRUNC)`ed it, the first `rename` published that inode to the target, and the second writer's still-open fd kept writing into the now-published file. The result was a torn hybrid at the target path — measured 35/40 in the issue report and reproduced by the new regression test against the pre-fix body on **both** OSes — 13/40 on a Windows dev box and **21/40 on ubuntu-latest CI** (verified by pushing the pre-fix body to a throwaway draft PR so the test would actually run on the CI OS, rather than assuming a Windows-only reproduction generalizes — recurring defect shape 2/7). The test carries deterministic structural assertions too (two concurrent `bestEffort: false` writers must report two different `err.path` staging files; `stagePathFor` must never repeat), so the guard does not depend on winning a scheduler race on any platform. The precondition is violated by design: `recordLspChild` (`lsp/client.ts`), `updateHeartbeat` (`runtime-turn.ts`), `prunePids` (`instance-registry.ts`) and `appendRecentTouches` (`bus-publish.ts`) are all deliberately fire-and-forget, and every reader in that family swallows parse errors to `{}`/`[]`, so a tear degraded to silent TOTAL loss of the store rather than partial loss. Fixed by routing both the sync and async paths through a new `stagePathFor()` that appends a monotonic per-process counter (`${target}.tmp-<pid>-<seq>`), making the staging path unique per call; the failure-path cleanup unlinks that same per-call path, so it still removes exactly the file the call created. A counter rather than `randomBytes` because it is allocation-free on these hot per-turn/per-touch paths and makes collision impossible by construction rather than merely improbable, with `process.pid` still supplying cross-process distinctness. The module docstring previously claimed "a concurrent reader always sees either the fully-old or fully-new file" — true cross-process, false same-process — and has been rewritten to state the real guarantee (crash-safe replacement + tear-free publication) alongside what the primitive explicitly does NOT provide: no read-modify-write isolation, no ordering, no mutual exclusion, no fsync durability, no orphan reaping (tracked in #1228), and a Windows-specific `bestEffort: true` loss mode when a reader holds the destination open without `FILE_SHARE_DELETE`. The naming scheme is now exported as `stagePathFor`/`STAGE_TMP_PATTERN` so orphan-staging-file sweepers consume it instead of re-deriving `.tmp-<pid>` (the pattern keeps the `<seq>` group optional so pre-#1205 leftovers on disk are still swept); `sweepStaleStageFiles` in `review-graph/builder.ts` is migrated to it separately under #1206, which must add its own pid-based exclusion rather than rely on that sweeper's existing `.stage-<pid>-` own-marker, which does not cover the `.tmp-` branch.
|
|
594
|
+
|
|
595
|
+
- **Dense word-index startup refresh no longer turns quadratic or monopolizes the TUI event loop (refs [#1197](https://github.com/apmantza/pi-lens/issues/1197))** — replacing one stale document is cheap, but the incremental path repeated posting-array filters even when almost the entire persisted index was stale: pi-lens refreshed 2,061 documents with zero reuse in 216.8s and recorded a genuine 22.5s event-loop block, while a linear full build of the same corpus took 7.5s. Refresh preflight now selects a separately-built full replacement before mutating the old index whenever the stale set's estimated WORK exceeds one full rebuild — density is not a bound, because per-document cost grows with the corpus: 800 documents with 239 stale (29.875%, under any 30% ratio) measured 90,577ms with a 39,553ms synchronous block against 1,029ms / 10.6ms for a full build, and at the 6,000-file cap up to 1,799 stale documents stayed on that path. The work model is calibrated against this repository's own 2,062-document corpus and its predicted crossover (23 stale documents) matches the measured one (~21). The >30% ratio, >30% file-set-churn and legacy-metadata transitions remain as-is. Every bulk path — the cooperative full builder and both refresh loops — now yields on an ~8ms monotonic budget OR'd with its item checkpoint rather than count-only, including within large documents and after any line of 4,096+ characters (a minified bundle previously got zero in-document yields). A build superseded mid-warmup returns gracefully instead of throwing out of the quick-mode warmup pass and skipping the #947 LSP pre-warm. Results remain byte-equivalent to the synchronous reference builder, superseded sessions never publish partial state, and sparse refreshes retain their unreadable-file retry semantics.
|
|
596
|
+
|
|
597
|
+
- **Windows bare-command resolution ignored the caller's child `PATH`/`PATHEXT` and could reuse another environment's cached answer (closes #1199)** — `safeSpawnAsync` and deprecated `safeSpawn` now derive case-insensitive Windows command resolution from the exact normalized environment passed to the child, so managed command shims such as Knip's `.cmd` are found even when only `options.env.Path`/`PATH` contains their directory. Cache identity includes the effective path and extension list; explicit/relative Windows paths use `path.win32`, and the same canonical absolute cwd used by drive-relative resolution is passed to the child. Positive cache hits revalidate the executable, negative entries expire after one second, and successful managed installs invalidate the cache immediately, while existing shell, UTF-8, timeout, abort, and tree-kill behavior remains unchanged.
|
|
598
|
+
|
|
599
|
+
- **`project_report`'s `toDisplayPath` hand-rolled relativization without the shape-aware `isWindowsPath` branch, mis-rendering a Windows-shaped path as the full absolute path on Linux CI (closes #1194, refs #1163 #1152 #1024)** — `clients/project-report.ts`'s `toDisplayPath` used host-default `path.isAbsolute`/`path.relative` directly, the exact defect `toProjectRelativePath` (`clients/path-utils.ts`) was fixed for in #1163 — its sibling `module-report.ts`'s `toDisplayPath` already delegates to that shape-aware helper, but this file reimplemented the same convention natively instead, so `path.isAbsolute("C:\\repo\\src\\x.ts")` returned `false` on Linux and short-circuited to the whole absolute path instead of `src/x.ts`. The #1163 sweep missed it because it grepped the `isWindowsPath` hot zone and this file never branched on it at all. Fixed by delegating to `toProjectRelativePath`, matching `module-report.ts`. Second-axis sweep of every other hand-rolled `path.relative`/`path.isAbsolute` relativizer in `clients/`/`tools/` found no further offenders: `call-graph.ts`'s `formatImpact` and `lens-map.ts`'s `toDisplayPath` already delegate to `toProjectRelativePath`; every other `path.relative`/`path.isAbsolute` hit resolves a live, same-process, same-OS path (dispatch context, LSP results, subprocess/runner output, filesystem walks) rather than a persisted/cross-OS review-graph path, so none share this class. Fail-then-pass verified directly against the pre-fix body under explicit `path.posix` semantics (not host-default, since a Windows dev box's bare `path` is already win32 and would pass vacuously) — it returned `"C:/repo/src/x.ts"` instead of `"src/x.ts"`, which the delegated implementation now corrects on any OS.
|
|
600
|
+
- **`trivy-config.test.ts` hardcoded a POSIX path literal (`/tmp/main.tf`), so it failed on a Windows dev machine while staying green on Linux CI (closes #1190, refs #1024)** — the terraform pass-through tests built `ctx.filePath`/`ctx.cwd` from the string literals `"/tmp/main.tf"`/`"/tmp"` and asserted the mocked `safeSpawnAsync` call against those same literals. The runner itself (`clients/dispatch/runners/trivy-config.ts`) is OS-agnostic — it resolves the scan path with `path.resolve(cwd, ctx.filePath)` — but on Windows that resolves `/tmp/main.tf` to a drive-relative `C:\tmp\main.tf`, so the hardcoded POSIX-form expectation no longer matched: the inverse of the #1024 class (a test only holding on one OS, this time POSIX-only rather than Windows-only). Test-only fix: the two cases now derive `tfCwd`/`tfFile` via `path.join(os.tmpdir(), ...)` and assert against those same derived values, so the expectation is computed the same way the runner computes its resolved path — real on both POSIX and Windows, with no hardcoded separator on either side.
|
|
601
|
+
|
|
602
|
+
- **Structural shape-5 hardening: LSP `touchFile` now returns a `{ diags, inconclusive, binding }` wrapper whose flags survive any copy by construction, plus a fail-closed graph-build-info guard (closes #1179, refs #1108 #1094 #1096)** —
|
|
603
|
+
the #1108 audit found all five side-channel flags safe as-is (every consumer
|
|
604
|
+
reads off the original), but the three LSP diagnostics flags stayed
|
|
605
|
+
fragile-by-construction: a non-enumerable property hung on the returned
|
|
606
|
+
diagnostics array, silently dropped by any `[...]`/`.map`/`.filter`/
|
|
607
|
+
`structuredClone`/`JSON` copy between producer and consumer (the class that bit
|
|
608
|
+
as #1094 `inconclusive` and #1096 `binding`). `touchFile` now resolves an
|
|
609
|
+
explicit `TouchFileResult` wrapper (`clients/lsp/diagnostic-binding.ts`) with
|
|
610
|
+
`inconclusive` and `binding` as ENUMERABLE fields alongside `.diags`, so a copy
|
|
611
|
+
operates on `.diags` and can no longer drop them — the copy-loss is impossible
|
|
612
|
+
by construction. Every flag-reading consumer was migrated in lockstep (the
|
|
613
|
+
cascade `readInconclusive`/`readBoundToCurrentDisk`/`isConfirmedTouch` and its
|
|
614
|
+
`.filter()` site in `clients/dispatch/integration.ts`, the dispatch LSP runner,
|
|
615
|
+
the `lsp_diagnostics` tool, the workspace-sweep `lens_diagnostics mode=full`
|
|
616
|
+
path, and the warm-attach IPC producer — which continues to re-surface
|
|
617
|
+
`inconclusive` as an enumerable DTO field over the socket). Behavior is
|
|
618
|
+
IDENTICAL — same flags, same values, same decisions; only the carriage
|
|
619
|
+
changed. `getAllDiagnostics`'s per-entry `binding` is DELIBERATELY left as a
|
|
620
|
+
lazy non-enumerable getter on the documented read-off-original contract (making
|
|
621
|
+
it enumerable would fire the per-file disk stat+hash on any incidental spread —
|
|
622
|
+
the stat storm its laziness exists to prevent). Separately closes a latent P3
|
|
623
|
+
from the #1108 review: `getGraphBuildInfoForGraph`'s global-slot fallback could
|
|
624
|
+
serve a SIBLING graph's build-info on a `_graphBuildInfoByGraph` identity miss,
|
|
625
|
+
feeding the `graph_degraded` marker gate a (possibly healthy) sibling verdict —
|
|
626
|
+
a new `graphBuildInfoIsTrustworthy` guard fails CLOSED so an unstamped/rehydrated
|
|
627
|
+
graph surfaces an honest degraded/unknown advisory instead of a #533 false
|
|
628
|
+
clean. Inert on the live path (every build stamps its returned graph before the
|
|
629
|
+
cascade reads it). An ast-grep rule remains not viable (AGENTS.md shape 5 is
|
|
630
|
+
semantic; cross-ref #1158).
|
|
631
|
+
- **Per-entry widget observation timestamps: a cross-file cascade merge no longer over-clears a whole footer record, dropping only the genuinely-stale entries (closes #1186, refs #1093 #1092 #1020)** —
|
|
632
|
+
`reconcileCascadeNeighborLspErrors` → `commitDiagnostics` used to stamp the
|
|
633
|
+
ENTIRE merged record's single `touchedAt` with the incoming `observedAt`
|
|
634
|
+
(e.g. a passive snapshot's `entry.ts`, up to ~240s old), including PRESERVED
|
|
635
|
+
entries observed more recently. If the neighbor's mtime later fell between
|
|
636
|
+
that stale stamp and a preserved entry's true observation time,
|
|
637
|
+
`reconcileStaleWidgetFiles` dropped the WHOLE record — losing the newer
|
|
638
|
+
preserved findings (the residual documented at `clients/dispatch/integration.ts`).
|
|
639
|
+
`WidgetDiagnostic` now carries a per-ENTRY `observedAt`: `normalizeDiagnostics`
|
|
640
|
+
stamps each incoming entry at its observation time, the cascade merge keeps
|
|
641
|
+
each preserved entry's own prior stamp (never re-aging a fresh finding to the
|
|
642
|
+
incoming stamp), and `reconcileStaleWidgetFiles` gates per ENTRY — dropping
|
|
643
|
+
only entries observed before the file's current mtime, keeping the record when
|
|
644
|
+
any survive, and dropping the record only when empty (a clean, finding-less
|
|
645
|
+
record still gates on `touchedAt`). `PersistedWidgetState` bumps v1→v2:
|
|
646
|
+
`importWidgetState` accepts a v1 snapshot and migrates each stampless entry to
|
|
647
|
+
inherit the record's `touchedAt` (a safe, over-conservative default; a
|
|
648
|
+
missing/non-numeric version or a future version this build can't understand is
|
|
649
|
+
still rejected — the pre-existing strictness is preserved). The sibling gates
|
|
650
|
+
`dropStaleFiles`
|
|
651
|
+
(session-state-store) and `reconcileProjectDiagnosticsSnapshot` are unchanged
|
|
652
|
+
— both compare against a single save/scan time ≥ every entry's observation, so
|
|
653
|
+
a whole-record drop there already equals dropping every entry. The
|
|
654
|
+
`markDependentsUnverified`/seq-stamp half of #1093's sketch is a separate,
|
|
655
|
+
non-contained follow-up (left open under #1093).
|
|
656
|
+
- **The `runtimeExitWindow` breaker map never dropped a stale/aged-out key, and its cooldown comment described the wrong call order (closes #1183, refs #1142 #1181)** — two P3 hygiene follow-ups from the #1181 review of the #1142 windowed-rate breaker in `clients/lsp/index.ts`. (1) `runtimeExitWindow` was only ever `delete`d on the currently-unreachable optional-trip path, so a key that crashed once then recovered kept a stale timestamp array forever (bounded by servers×roots, not a leak, but untidy). The "survived past the threshold" branch now also drops the key once every entry in its window has aged out, mirroring where `runtimeExitCounts` is already `delete`d there — anchored on the death's own `exitedAt` (not `Date.now()`) so it only ever fires for deaths `recordRuntimeExitWindow` declined to record (over the sleep-gap ceiling, or a missing `exitedAt`), never for a death that was just added to the window. (2) The `Math.max` cooldown comment claimed it "never shortens a longer cooldown the fast path may have just set," which has the ordering backwards — `state.broken` is `delete`d at the top of the method, so the window's `.get(key) ?? 0` always reads 0, and the fast path runs AFTER this block. Reworded to describe the actual ordering; harmless either way since the non-optional branch latches `permanentlyBroken` and the optional branch is currently unreachable. Comment-only for (2); pure map hygiene for (1) — trip logic, thresholds, and the fast path are unchanged.
|
|
657
|
+
- **`allowScripts` entries had drifted from what the declared dependency ranges actually resolve to, so npm v12's exact-version script-approval check saw stale pins (closes #1176)** —
|
|
658
|
+
`package.json`'s `@ast-grep/cli` dependency range is `"^0.45.0"`, but the
|
|
659
|
+
`allowScripts` map still keyed its approval on `"@ast-grep/cli@0.44.1"` — a
|
|
660
|
+
version the range can no longer even resolve to. pi-lens has no
|
|
661
|
+
`allowScripts`-enforcing tool of its own (no `@lavamoat/allow-scripts`, no
|
|
662
|
+
`.npmrc` script policy), so the field does nothing for pi-lens's own
|
|
663
|
+
`npm ci`; its only audience is an external consumer or vetting tool reading
|
|
664
|
+
the *published* package. The published package ships **no lockfile**
|
|
665
|
+
(`package-lock.json` is not in the `files` array and there's no
|
|
666
|
+
`npm-shrinkwrap.json`), so that consumer fresh-resolves `"^0.45.0"` against
|
|
667
|
+
the registry — which lands on `0.45.1` (registry `latest`; only `0.45.0`
|
|
668
|
+
and `0.45.1` exist in the `0.45.x` line), exactly what the original report
|
|
669
|
+
observed. The correct target is therefore the **freshest in-range /
|
|
670
|
+
fresh-resolve version**, not the locally-committed lockfile's resolution —
|
|
671
|
+
so the entry is `"@ast-grep/cli@0.45.1": true`, and the committed
|
|
672
|
+
`package-lock.json` was also bumped (`npm install @ast-grep/cli@0.45.1`,
|
|
673
|
+
an in-range patch bump — the `^0.45.0` dependency range is unchanged) so
|
|
674
|
+
pi-lens's own dev tree and a fresh consumer resolve both land on the same
|
|
675
|
+
version. `0.45.1` ships the same `postinstall` script `0.44.1` needed
|
|
676
|
+
approval for. Swept every other `allowScripts` member for the same drift
|
|
677
|
+
class (a pinned exact version no longer matching the freshest version its
|
|
678
|
+
governing range resolves to): `@google/genai@1.52.0` (transitive via the
|
|
679
|
+
`@earendil-works/pi-coding-agent` devDependency) already matched, unchanged;
|
|
680
|
+
`protobufjs@7.6.4` (also transitive via `@earendil-works/pi-coding-agent`)
|
|
681
|
+
had drifted the same way and moved to `"protobufjs@7.6.5": true` — the
|
|
682
|
+
freshest in-range version, which here happens to match both the lockfile
|
|
683
|
+
and a fresh resolve. `@ast-grep/napi` (also range `^0.45.0`) was re-checked
|
|
684
|
+
for an install-time script at both `0.45.0` and `0.45.1` (`npm view
|
|
685
|
+
@ast-grep/napi scripts`) and has none in either, so it correctly has no
|
|
686
|
+
`allowScripts` entry. `fsevents@2.3.3` also has an install script
|
|
687
|
+
(`hasInstallScript: true`) but is intentionally NOT listed: it's
|
|
688
|
+
`dev:true, optional:true, os:["darwin"]`, so it's never part of a
|
|
689
|
+
consumer's production install and out of scope for this allowlist — noting
|
|
690
|
+
it here rather than leaving the omission silent. No other version-pinned
|
|
691
|
+
metadata (`overrides`/`resolutions`/`pnpm.overrides`/`packageManager`)
|
|
692
|
+
exists in `package.json` to drift, and a repo-wide grep for hardcoded
|
|
693
|
+
`0.44.`/`0.45.` ast-grep version literals outside `package.json`/the
|
|
694
|
+
lockfile turned up only historical comments in test files and one rule
|
|
695
|
+
doc-comment describing ast-grep's behavior as of a past version — not pins
|
|
696
|
+
that need to track the current range.
|
|
697
|
+
- **`switch-case-termination` false positives for returning try/catch and exhaustive conditionals (closes #1079)** — the `no_terminating_statement`
|
|
698
|
+
post-filter only checked whether a case's last statement was a literal
|
|
699
|
+
terminator, so a case ending in `try { return … } catch { return … }` (or an
|
|
700
|
+
exhaustive `if/else`) was flagged even though every path returns. It now
|
|
701
|
+
runs a bounded, fail-safe control-flow analysis: trailing blocks,
|
|
702
|
+
`try/catch/finally` (a returning try completes after finally; a try body
|
|
703
|
+
that completes normally only terminates via a finally), and exhaustive
|
|
704
|
+
`if/else` (no else still falls through). It also honors an intentional
|
|
705
|
+
`// fallthrough` marker comment (ESLint `no-fallthrough` convention),
|
|
706
|
+
checked against comment nodes only so a `case "fallthrough":` value can't
|
|
707
|
+
suppress a finding. Genuine fall-throughs are still reported. TS + JS
|
|
708
|
+
regression fixtures cover grouped labels, try/finally, and intentional
|
|
709
|
+
fallthrough.
|
|
710
|
+
- **Non-enumerable side-channel flags — the shape-5 copy-loss sweep (refs [#1108](https://github.com/apmantza/pi-lens/issues/1108))** — Audited every non-enumerable / identity-scoped side-channel flag hung on a returned diagnostics or graph value for the copy-loss class that bit as #1094 (`inconclusive`) and #1096 (`binding`): the `inconclusive` and `binding` flags on `touchFile` results, the lazy `binding` getter on `getAllDiagnostics` entries, and the `_graphBuildInfoByGraph` / `_graphImportChanges` WeakMaps keyed by `ReviewGraph` identity. All five are safe — every consumer reads the flag off the ORIGINAL producer object before any `.filter()`/`.map()`/spread, the one genuine JSON boundary (the warm-attach IPC socket) already re-surfaces `inconclusive` as an explicit enumerable response field, and both WeakMaps fail safe on a copy/rehydrate miss (a global-slot fallback for build-info; a full-rebuild + generation-gate for the import delta). Hardened the contract durably where it is not owned by an in-flight PR: documented the "read off the original, never a derived copy; re-surface as an enumerable field to cross a serialization boundary" carriage contract on the `DiagnosticBinding` type, and added an IPC-boundary guard test asserting the consumer still honors an inconclusive answer carried as an enumerable field across the JSON round-trip. The producer-side type annotations and the structural `{ diags, inconclusive, binding }` wrapper migration live in the LSP index (owned by the #1142 breaker work) and are deferred to a tracked follow-up ([#1179](https://github.com/apmantza/pi-lens/issues/1179)); an ast-grep rule is not viable (AGENTS.md shape 5 is semantic — good and bad uses are syntactically identical).
|
|
711
|
+
- **LSP runtime-exit breaker missed persistent low-frequency crash loops (closes #1142, refs #1127 #1139)** —
|
|
712
|
+
#1139's `runtimeExitCounts` counts a client death toward the circuit breaker
|
|
713
|
+
only when its lifetime (`exitedAt - spawnedAt`) is under
|
|
714
|
+
`RUNTIME_EXIT_UPTIME_THRESHOLD_MS` (60s) — the hot crash loop (opengrep dies in
|
|
715
|
+
seconds). A death OVER the threshold falls into the "survived → reset the
|
|
716
|
+
streak" branch, so a server that reliably dies at ~65-90s after every spawn
|
|
717
|
+
NEVER tripped: every death reset the streak and it churned forever, a slow loop
|
|
718
|
+
the hard-cutoff design structurally can't detect. Added a SECOND, independent
|
|
719
|
+
windowed-rate trip that COMPOSES with (does not replace) the fast path:
|
|
720
|
+
`RUNTIME_EXIT_WINDOW_TRIP_COUNT` (5, = `BROKEN_PERMANENT_AFTER`) non-intentional
|
|
721
|
+
deaths within a rolling `RUNTIME_EXIT_WINDOW_MS` (15 min) window trip the
|
|
722
|
+
breaker regardless of each death's individual lifetime. N/M err against
|
|
723
|
+
false-tripping: five clustered non-intentional deaths is definitionally a loop,
|
|
724
|
+
and sparse benign crashes age out of the rolling window before five coincide.
|
|
725
|
+
The fast path is byte-for-byte unchanged — a hot loop still trips at exactly 5
|
|
726
|
+
consecutive early exits, never delayed by the window. Benign over-threshold
|
|
727
|
+
"deaths" are excluded: intentional teardowns (user restart, config/workspace
|
|
728
|
+
reload, session change, #743 eviction, generation handoffs) are gated out by the
|
|
729
|
+
existing `!wasShutdownIntentional()` guard the windowed trip shares, and a
|
|
730
|
+
death whose lifetime exceeds a 10-min ceiling — a genuinely long healthy run, or
|
|
731
|
+
an `exitedAt - spawnedAt` inflated across a machine-sleep/Modern-Standby suspend
|
|
732
|
+
(the #1122/#1139 death-timestamp lesson) — is not recorded, backed structurally
|
|
733
|
+
by per-server-key windowing (one suspend kills at most one live client per key).
|
|
734
|
+
The death-timestamp window is bounded on BOTH axes (prune aged-out entries +
|
|
735
|
+
hard-cap the array at the trip count, drop-oldest) and needs no timer (ages by
|
|
736
|
+
prune-on-check). Fail-then-pass regression tests cover all three cases: the slow
|
|
737
|
+
~65-90s loop now converges (verified failing on pre-fix — the whole bug), the
|
|
738
|
+
hot loop still trips fast (no regression), and a sleep-gap/long-run death,
|
|
739
|
+
sparse aged-out crashes, and deliberate restarts do NOT trip.
|
|
740
|
+
- **Closed out the three #1134 P3 tails left after #1138's Svelte oxfmt support (closes #1134)** —
|
|
741
|
+
1. **TOML line-match heuristic limits.** `hasOxfmtSvelteConfig`'s
|
|
742
|
+
`OXFMT_SVELTE_TOML_TRUE` regex required `svelte = true` to be
|
|
743
|
+
immediately followed by end-of-line/end-of-file, so a trailing inline
|
|
744
|
+
comment (`svelte = true # enable`) false-negatived. Relaxed the
|
|
745
|
+
trailing match to tolerate an optional `#`-comment, and added a
|
|
746
|
+
docblock sentence documenting the heuristic's remaining known limits
|
|
747
|
+
(it is a line match, not a TOML parser — a `[table]`-sectioned or
|
|
748
|
+
multi-line-string `svelte = true` occurrence could still false-positive;
|
|
749
|
+
accepted since that only causes oxfmt to be offered, never a silent
|
|
750
|
+
formatter failure). Added a fail-then-pass regression test for the
|
|
751
|
+
`svelte = true # comment` case (`tests/clients/tool-policy.test.ts`).
|
|
752
|
+
2. **Monorepo asymmetry.** `hasOxfmtSvelteConfig`'s svelte-dependency check
|
|
753
|
+
stops at the nearest `package.json` (`hasNearestPackageJsonDependency`)
|
|
754
|
+
while its config walk goes all the way to the repo root
|
|
755
|
+
(`walkUpDirs`) — a root-level `svelte` dependency with a sub-package
|
|
756
|
+
`cwd` under-offers oxfmt for `.svelte` there. Documented via a
|
|
757
|
+
docblock sentence (no behavior change: this cell fails safe —
|
|
758
|
+
under-offering, never mis-offering a formatter that then errors at
|
|
759
|
+
runtime — and stays untested by design, matching the async
|
|
760
|
+
`.tflint.hcl` nearest-vs-root-walk note already in this file).
|
|
761
|
+
3. **docs/language-coverage.md's Vue row** showed formatter `—` though
|
|
762
|
+
`.vue` has been wired to `prettier` (default) + `oxfmt` (appended via
|
|
763
|
+
`OXFMT_SUPPORTED_EXTENSIONS`) since #1138 landed. Corrected the row to
|
|
764
|
+
`prettier, oxfmt`.
|
|
765
|
+
- **Rule-cache disk cache could replay a stale compiled rule set on a same-mtime+same-size rule-file edit (closes #1118, refs #1105 #878)** —
|
|
766
|
+
`clients/cache/rule-cache.ts`'s `computeRuleHash` fingerprinted every
|
|
767
|
+
effective rule file over METADATA only (`mtimeMs:size`) — the review-graph
|
|
768
|
+
first-filter without its content-hash CONFIRM step. A rule-file edit
|
|
769
|
+
preserving both mtime and byte size (git-checkout timestamp restoration, a
|
|
770
|
+
same-length tweak, a formatter that preserves mtime) replayed a stale
|
|
771
|
+
compiled rule set from `<language>-rules-v6.json`, and because this is a
|
|
772
|
+
PERSISTED disk cache, the stale set was re-persisted under the
|
|
773
|
+
fresh-looking fingerprint, poisoning every future process (the #878
|
|
774
|
+
`force`-reload failure mode, one axis over). Fixed by splitting the
|
|
775
|
+
fingerprint on rule-file origin, mirroring the project/bundled split
|
|
776
|
+
`yaml-rule-parser.ts`/`ast-grep-napi.ts` already use for ast-grep rules
|
|
777
|
+
(#1105): the small, mutable PROJECT-LOCAL subset (under
|
|
778
|
+
`<project>/rules/tree-sitter-queries/`) now gets a content-hash CONFIRM on
|
|
779
|
+
top of the metadata fingerprint; the ~705 BUNDLED files (immutable within a
|
|
780
|
+
process) stay metadata-only, so `RuleCache.get`'s per-edit tree-sitter
|
|
781
|
+
runner hot-path cost is unchanged when a project has no rule overrides.
|
|
782
|
+
`CACHE_VERSION` bumped v6→v7 so a v6 entry persisted under the old
|
|
783
|
+
metadata-only formula (a potentially poisoned one) misses once on upgrade
|
|
784
|
+
instead of being trusted. Fail-then-pass regression tests pin a rule
|
|
785
|
+
file's mtime via `utimesSync`, edit its content while preserving byte size,
|
|
786
|
+
and assert the cache invalidates for project-local files but still hits
|
|
787
|
+
for bundled-only rule sets; a third test confirms a persisted
|
|
788
|
+
metadata-only-fingerprinted entry is rejected post-fix.
|
|
789
|
+
- **Formatter definitions and `FORMATTER_POLICY_BY_EXTENSION` were unbound hand-maintained inverse lists; added a bidirectional drift guard and fixed two latent mismatches (closes #1135; refs #1086 #1134 #883)** —
|
|
790
|
+
`clients/formatters.ts` (formatter → `extensions[]`) and `clients/tool-policy.ts`'s
|
|
791
|
+
`FORMATTER_POLICY_BY_EXTENSION` (extension → `formatterNames[]`) are hand-maintained
|
|
792
|
+
INVERSE mappings of the same relation with no test binding the two directions — the
|
|
793
|
+
#883/#209 single-source-of-truth class, and the general shape under #1134's oxfmt/.svelte
|
|
794
|
+
gap. Added `tests/clients/formatter-policy-consistency.test.ts`, which imports the real
|
|
795
|
+
`ALL_FORMATTERS` definitions and the policy maps and asserts both directions: every
|
|
796
|
+
definition extension is policy-included / a documented deliberate exclusion / a documented
|
|
797
|
+
no-policy fallback (so a definition gaining a policy-gated extension can't be silently
|
|
798
|
+
never-offered — #1134's exact symptom); and every policy `formatterName`/`defaultFormatter`
|
|
799
|
+
is a real formatter whose definition claims that extension or filename (so a broken option
|
|
800
|
+
can't be offered — including the terragrunt-hcl filename-keyed variant). Fixed two real
|
|
801
|
+
latent drifts surfaced by the guard, both behavior-identical (the mismatched entries were
|
|
802
|
+
already inert): the `.sass` policy listed `oxfmt` though oxfmt does not support `.sass`
|
|
803
|
+
(absent from `OXFMT_SUPPORTED_EXTENSIONS`), and the `.fish` formatter policy named
|
|
804
|
+
`fish-indent`, which has no `FormatterInfo` (it is a lint runner) — a dead, unsatisfiable
|
|
805
|
+
entry, now removed. A structural derive (tool-policy importing the definitions) was rejected
|
|
806
|
+
because `formatters.ts` already imports `tool-policy.ts`, so the reverse edge would create a
|
|
807
|
+
module import cycle; the test-based guard binds both directions while keeping the dependency
|
|
808
|
+
one-way. Also bound `AUTO_INSTALLABLE_DEFAULT_FORMATTERS` keys to real formatter definitions
|
|
809
|
+
(same formatter-name-reference class). The guard reads four newly-exported read-only symbols
|
|
810
|
+
(`ALL_FORMATTERS` plus the three policy maps `FORMATTER_POLICY_BY_EXTENSION`,
|
|
811
|
+
`FORMATTER_POLICY_BY_FILENAME`, `AUTO_INSTALLABLE_DEFAULT_FORMATTERS`), and its own two
|
|
812
|
+
allowlists (deliberate exclusions, no-policy fallbacks) each carry a minimality check so
|
|
813
|
+
they cannot rot into blanket escape hatches.
|
|
814
|
+
- **Pull-diagnostics entries never bound to document content, so they never demoted (closes #1104, refs #1095 #1096 #1100)** —
|
|
815
|
+
#1096 bound LSP diagnostics to a content fingerprint on the PUSH path
|
|
816
|
+
(`publishDiagnostics` version echo + send-time hash); the PULL path
|
|
817
|
+
(`textDocument/diagnostic`, `workspace/diagnostic`) recorded no fingerprint
|
|
818
|
+
at all, so pull-served cache entries read binding `"unknown"` forever and
|
|
819
|
+
the #1096 P2-1 service-sweep binding gate could never protect them. Fixed by
|
|
820
|
+
threading the server's `resultId` and a request-time content hash through
|
|
821
|
+
both pull requests: a `"full"` report is fingerprinted (the single-file path
|
|
822
|
+
reuses the exact sent-content hash `recordSentContent` already captures on
|
|
823
|
+
every didOpen/didChange — no extra read; the project-wide
|
|
824
|
+
`workspace/diagnostic` pull hashes disk bytes at request time since files
|
|
825
|
+
may not yet be open when it fires), and an `"unchanged"` report (now
|
|
826
|
+
requested via `previousResultId`/`previousResultIds`) inherits the prior
|
|
827
|
+
pull's diagnostics AND binding instead of being misread as a confirmed-clean
|
|
828
|
+
`[]` (the #570/#571 false-clean shape). The workspace pull-sweep record site
|
|
829
|
+
(`clients/lsp/index.ts`) now threads this `contentHash` into
|
|
830
|
+
`workspaceDiagnosticsCacheCtx.record()`, closing the gap the site's own
|
|
831
|
+
`#1095` doc comment used to document as intentional. Also closed an
|
|
832
|
+
AGENTS.md shape-5 gap in the sibling per-file touch path: `processFile` read
|
|
833
|
+
the `#1095` binding off the RAW `touchFile` diagnostics array before
|
|
834
|
+
`applyAuxiliarySuppressions`' `.filter()` rebuilds it (a `.filter()` copy
|
|
835
|
+
does not carry a source array's non-enumerable `.binding`), and
|
|
836
|
+
`LSPService.mergeBinding` no longer gates a contributor's `contentHash` on
|
|
837
|
+
that SAME contributor also carrying a `version` — pull bindings
|
|
838
|
+
legitimately carry a hash with no version, and the old gate silently
|
|
839
|
+
dropped it. Folded in two #1100-review P3s on the same surface: the
|
|
840
|
+
cascade's degraded/fallback display paths and the `lsp_binding_rejected`
|
|
841
|
+
indeterminate-advisory preamble (`clients/runtime-turn.ts`), which
|
|
842
|
+
previously reused the graph-unavailable wording even when the review graph
|
|
843
|
+
was fine and only the LSP display was withheld, now use a
|
|
844
|
+
binding-rejection-specific frame. Fail-then-pass regression tests cover:
|
|
845
|
+
the pull-sweep record site actually receiving a `contentHash`; a
|
|
846
|
+
pull-recorded entry whose content changed under a matching mtime being
|
|
847
|
+
demoted (not replayed) on the next sweep; the per-file touch path's binding
|
|
848
|
+
surviving the suppression filter; and the `"unchanged"`-report inheritance
|
|
849
|
+
on both the single-file and workspace pull protocols.
|
|
850
|
+
- **Config caches gated freshness on mtime alone — the mtime-only cache-freshness class sweep (closes #1105)** —
|
|
851
|
+
Completed the shape-6 (freshness stamp that doesn't cover the data's real
|
|
852
|
+
dependency) sweep the #1092→#1119 diagnostics/word-index arc deferred. Audited
|
|
853
|
+
every persisted/derived cache with an mtime/TTL key: word-index (#1119),
|
|
854
|
+
`rule-cache.ts` (`mtime:size`), `sgconfig.ts` (content-hashed), the
|
|
855
|
+
`yaml-rule-parser` project/bundled split (project rules content-hashed via
|
|
856
|
+
`loadYamlRulesFresh`, bundled dir-mtime is safe by process-lifetime
|
|
857
|
+
immutability), `reverse-deps` (snapshot-seq/generation-coupled), installer
|
|
858
|
+
`probe-cache` (existence re-validated every read + ast-grep version-family
|
|
859
|
+
verify — mtime is a refresh hint, not the correctness gate), `TreeCache`
|
|
860
|
+
(content-hash authoritative, #890), and the project-snapshot per-file
|
|
861
|
+
`mtime:size` entries — all already-hardened or safe. The one gap: the
|
|
862
|
+
`.pi-lens.json` config caches gated reuse on the config file's **mtime alone**,
|
|
863
|
+
so an in-place edit that preserved mtime (git checkout timestamp restoration, a
|
|
864
|
+
same-second rewrite) but changed the file's byte length replayed a stale parsed
|
|
865
|
+
config / ignore matcher — a config that drives mutation, ignore, and rule
|
|
866
|
+
policy. This spanned every freshness gate in the two files: the root parsed-
|
|
867
|
+
config caches (`loadPiLensProjectConfig`/`loadPiLensConfigInDir` in
|
|
868
|
+
`clients/project-lens-config.ts`), the root ignore-matcher cache, and — the
|
|
869
|
+
member found in adversarial review — the **nested `.pi-lens.json`/`.gitignore`
|
|
870
|
+
layering cache** (`patternsForDir`, #783) in `clients/file-utils.ts`, which
|
|
871
|
+
short-circuits AHEAD of the root caches so a preserved-mtime, length-changing
|
|
872
|
+
edit to a NESTED config replayed stale patterns for that subtree. Fixed by
|
|
873
|
+
adding **`size` as the free second axis** of the review-graph `size:mtimeMs`
|
|
874
|
+
signature to every gate — the root `.pi-lens.json` (threaded through
|
|
875
|
+
`PiLensConfigMarker` in `clients/workspace-topology.ts`), the root `.gitignore`
|
|
876
|
+
and global `~/.pi-lens/config.json` (via a shared `fileFreshnessSignature`
|
|
877
|
+
helper), and both the nested `.pi-lens.json` and nested `.gitignore` axes. The
|
|
878
|
+
same stat that yields mtime already reads size, so the cheap hit path stays
|
|
879
|
+
cheap (no content hashing on the hot path). The residual (identical mtime AND
|
|
880
|
+
identical size, changed content) matches the review-graph/word-index accepted
|
|
881
|
+
residual by design. Fail-then-pass regression tests cover the root and nested
|
|
882
|
+
gates (each proven to replay stale on the pre-fix mtime-only code); FS-agnostic
|
|
883
|
+
(mtime pinned via `utimesSync`, size varied by content length) so they exercise
|
|
884
|
+
the gates identically on Linux CI.
|
|
885
|
+
|
|
886
|
+
- **Windows drive-root & autofix-snapshot `readdirSync` blocked the event loop on slow cloud-backed dirs (refs #1137)** —
|
|
887
|
+
the genuine synchronous event-loop-block tier from #1122 (the 1.6–10.6 s
|
|
888
|
+
single-process blocks in `latency.log`, distinct from the machine-level
|
|
889
|
+
Modern-Standby/commit-exhaustion artifacts dispositioned in #1122 and tagged
|
|
890
|
+
`suspectSystemStall` by #1125). On a OneDrive/network-backed path a stalled
|
|
891
|
+
synchronous directory read blocks the Node loop — and pi's TUI — for the whole
|
|
892
|
+
stall. Converted the two clearly-live-path, low-risk offenders: (a) the Windows
|
|
893
|
+
Ruby drive-root enumeration (`readdirSync(driveRoot)` scanning `C:\` for
|
|
894
|
+
`ruby<N>` installer dirs), previously re-run **synchronously on every LSP spawn**
|
|
895
|
+
in `buildAugmentedPath` (PATH build) and on every Ruby candidate build in
|
|
896
|
+
`rubyBinCandidates` — now a shared `clients/lsp/ruby-drive-dirs.ts` module that
|
|
897
|
+
**memoizes** the result once per process (O(1) amortized) and reads the drive
|
|
898
|
+
root **off the loop** via `fs.promises.readdir` on the hot spawn path; and
|
|
899
|
+
(b) the `tool_result` autofix side-effect snapshot walk (`snapshotDirInto`,
|
|
900
|
+
`clients/pipeline.ts`), whose per-directory `readdirSync`/`statSync` are now
|
|
901
|
+
`fs.promises.readdir`/`stat` (the walk was already async + chunk-yielding, but
|
|
902
|
+
each synchronous per-dir read still blocked on a cloud stall). Same outputs,
|
|
903
|
+
just non-blocking. `measureMaxSyncBlockMs` occupancy guards assert the converted
|
|
904
|
+
drive-root path no longer holds the loop and fail against the pre-conversion
|
|
905
|
+
sync shape. Remaining suspect sites (the shared `walkTreeStackAsync` per-dir
|
|
906
|
+
read in `clients/source-walker.ts`; `hasProjectMarker`/`expandWorkspacePattern`
|
|
907
|
+
deep in sync memoized/bounded chains; the `safe-spawn.ts` Windows `spawnSync`
|
|
908
|
+
teardown/cached paths) are deferred as scoped follow-ups on #1137 — each ripples
|
|
909
|
+
into a widely-called sync API or subprocess-teardown correctness.
|
|
910
|
+
- **`session_start_sequence_read` was an unbounded synchronous blocking read on the session_start hot path (closes #1162)** —
|
|
911
|
+
`readLatestProjectSequence` called `fs.readFileSync` on the project
|
|
912
|
+
change-log before `session_start_total` returned; normally ~2ms, but under
|
|
913
|
+
host I/O pressure it had no escape hatch and was observed to balloon to
|
|
914
|
+
2125ms in production latency.log. A `setTimeout`/`Promise.race` timeout
|
|
915
|
+
cannot preempt a synchronous read (the thread only returns to the event
|
|
916
|
+
loop once the OS call returns), so the fix adds an async twin
|
|
917
|
+
(`readLatestProjectSequenceAsync`, `fs.promises.readFile`) and races it
|
|
918
|
+
against a 250ms budget (`PI_LENS_SEQUENCE_READ_BUDGET_MS`-overridable) in
|
|
919
|
+
both the quick-mode and full-mode session_start paths. On a healthy read
|
|
920
|
+
(the common case) this adds ~zero overhead; on a stalled read, session_start
|
|
921
|
+
proceeds immediately with the safe cold-start sequence (only gates
|
|
922
|
+
snapshot freshness, never correctness) while the real read finishes in the
|
|
923
|
+
background and re-seeds the runtime — skipped for a one-shot `pi --print`
|
|
924
|
+
process via `isPrintMode()`, screening the #1154/#1153 one-shot
|
|
925
|
+
referenced-handle retention class. The fallback is never silent: the
|
|
926
|
+
`session_start_sequence_read` latency line now carries a `timedOut` flag,
|
|
927
|
+
and a background reseed logs its own
|
|
928
|
+
`session_start_sequence_read_deferred_reseed` phase. Fail-then-pass
|
|
929
|
+
regression tests inject a controllable slow read and assert session_start
|
|
930
|
+
returns within budget, falls back to cold-start with the flag set, and
|
|
931
|
+
still seeds normally on the healthy path. Adversarial review (#1168) caught
|
|
932
|
+
two P3s in this exact stall regime, both fixed in the same PR: (1) the
|
|
933
|
+
background reseed's `isCurrentSession` guard caught a cross-session move-on
|
|
934
|
+
but not a SAME-session advancement — an edit landing in the stall window
|
|
935
|
+
could have its `bumpFileSeq` result clobbered by a late reseed of the
|
|
936
|
+
pre-edit state; fixed with a `runtime.projectSeq > 0` guard (the cold seed
|
|
937
|
+
always sets it to exactly 0, so `> 0` at reseed time can only mean an
|
|
938
|
+
in-window bump happened). (2) The cold sentinel's `projectSeq: 0` was
|
|
939
|
+
indistinguishable from a project's legitimate first-ever snapshot (also
|
|
940
|
+
persisted at `seq === 0`), so a timed-out read could hydrate a stale seq-0
|
|
941
|
+
snapshot as fresh; fixed with a dedicated `UNKNOWN_PROJECT_SEQ` (`-1`)
|
|
942
|
+
sentinel fed only to the freshness check (never to `runtime.projectSeq`
|
|
943
|
+
itself, keeping fix (1)'s guard valid). Both have their own fail-then-pass
|
|
944
|
+
regression tests.
|
|
945
|
+
- **Resource-sampler Windows CIM spawns were not `.unref()`'d, the same one-shot-retention shape as the orphan reaper (refs #1155)** —
|
|
946
|
+
`clients/resource-sampler.ts`'s two Windows-only `Get-CimInstance Win32_Process`
|
|
947
|
+
spawns (`findDescendantPidsWindows`'s descendant-tree lookup and
|
|
948
|
+
`sampleProcessesWindows`'s CPU/RSS query) used `stdio:["ignore","pipe","ignore"]`
|
|
949
|
+
with a piped, `data`-listener-attached stdout and neither the child nor its
|
|
950
|
+
stdout was ever `.unref()`'d — the same shape #1153/#1160 fixed for the orphan
|
|
951
|
+
reaper (shape 4 of AGENTS.md's recurring-defect catalog: a referenced handle
|
|
952
|
+
that outlives a one-shot settle). The sampler was empirically absent under a
|
|
953
|
+
trivial `pi --print` prompt (its own `setInterval` was already unref'd, and it
|
|
954
|
+
only runs bracketed to an awaited analyzer spawn), but was not safe by
|
|
955
|
+
construction for a file-editing repro that does exercise it. Fixed by
|
|
956
|
+
extracting the reaper's `unrefReaperChild` AND its `spawnCollectStdout`
|
|
957
|
+
spawn→pipe-stdout→resolve-on-close plumbing into a shared, dependency-free
|
|
958
|
+
`clients/child-unref.ts` (`unrefChildAndPipes`, `spawnCollectStdout`) and
|
|
959
|
+
calling `spawnCollectStdout` at both sampler spawn sites — a single source
|
|
960
|
+
of truth for both modules instead of a second hand-rolled copy (the
|
|
961
|
+
promotion also resolved a SonarCloud new-code-duplication gate failure:
|
|
962
|
+
adding an identical `unrefChildAndPipes(child)` line to both near-identical
|
|
963
|
+
spawn blocks had pushed duplicated-line density over the 3% threshold;
|
|
964
|
+
collapsing both blocks to parse-only call sites around the shared helper
|
|
965
|
+
removed the duplication instead of adding to it). Unref only detaches this
|
|
966
|
+
child ALONE from keeping a settled one-shot alive; in an interactive/
|
|
967
|
+
long-lived session (or one bracketed to real analyzer work) the loop stays
|
|
968
|
+
referenced for other reasons, so sampling is unaffected — the parse logic
|
|
969
|
+
at both call sites is otherwise unchanged, so a spawn/error failure still
|
|
970
|
+
resolves to the same empty/partial result as before. Fail-then-pass
|
|
971
|
+
regression tests assert both spawn sites unref the child and its stdout, in
|
|
972
|
+
both the sampler and (unchanged) the reaper.
|
|
973
|
+
- **`toProjectRelativePath` never relativized a Windows-shaped path off native Windows (closes #1163, refs #1150/#1152/#1161/#1024)** —
|
|
974
|
+
shape-2 bug-class sweep of the `path.*`-on-cross-shaped-input hot zone.
|
|
975
|
+
`clients/path-utils.ts:toProjectRelativePath` used the host-default
|
|
976
|
+
`path.isAbsolute`/`path.relative` even when the input was Windows-shaped
|
|
977
|
+
(drive letter or UNC). On Linux CI, `path.isAbsolute("C:\\repo\\src\\x.ts")`
|
|
978
|
+
is `false` (no POSIX leading slash), so the function short-circuited and
|
|
979
|
+
returned the whole absolute path instead of the project-relative `src/x.ts`
|
|
980
|
+
it produces on Windows — a persisted call-graph symbol-key path or graph
|
|
981
|
+
display path (via `module-report`/`lens-map`'s `toDisplayPath` and
|
|
982
|
+
`call-graph`'s `formatImpact`) rendered as a full absolute path on Linux
|
|
983
|
+
(green-locally / wrong-on-CI, the #1024 divergence class). Fixed
|
|
984
|
+
shape-conditionally (`isWindowsPath(p) ? win32 : path`, the #1152 idiom):
|
|
985
|
+
a Windows-shaped path is parsed with `win32.*` on ANY OS; native same-OS
|
|
986
|
+
paths are unchanged. Fail-then-pass regression tests feed `C:\...`/UNC
|
|
987
|
+
literals as INPUT and assert the relative result on any OS (meaningful on
|
|
988
|
+
Linux CI). The rest of the swept hot zone
|
|
989
|
+
(`widget-state`/`file-utils`/`call-graph`/`installer`/`elixir-check`, plus
|
|
990
|
+
`file-role` #1152 and `resolveNonExisting` #1150 already fixed) was audited
|
|
991
|
+
and cleared as native-by-design — inputs are real on-disk paths the running
|
|
992
|
+
OS produced (cwd/project-roots/scanned files, `path.resolve`'d first) or
|
|
993
|
+
already fold through `normalizeEphemeralMapKey`/`PathKeyedMap`/the
|
|
994
|
+
regex-based `parseSymbolKey`.
|
|
995
|
+
- **`generated-artifacts.ts` used module-default `path.basename` on Windows-shaped paths, under-detecting lockfiles/declarations off native Windows (closes #1161, sibling of #1150/#1152)** —
|
|
996
|
+
`hasStrongGeneratedArtifactPath` (lockfile match), `hasWeakGeneratedFileNamePattern`
|
|
997
|
+
(name-pattern match), and `isDeclarationFile` (`.d.ts`/`.d.mts`/`.d.cts` match)
|
|
998
|
+
all took the module-default `path.basename(filePath)` on shape-committed
|
|
999
|
+
input. On Linux CI, `path.basename("C:\\proj\\package-lock.json")` finds no
|
|
1000
|
+
POSIX separator and returns the whole string unchanged, so
|
|
1001
|
+
`LOCKFILE_NAMES.has(...)` misses — a Windows-shaped lockfile or declaration
|
|
1002
|
+
path was silently treated as ordinary source. `generated-artifacts.ts` is
|
|
1003
|
+
imported by `file-role.ts`'s `"generated"` branch, so this residual sat
|
|
1004
|
+
within `detectFileRole`'s own call tree even after #1152 fixed the
|
|
1005
|
+
dir-segment/basename split there. Fixed with a shared `basenameForShape`
|
|
1006
|
+
helper that routes through `path.win32.basename` when `isWindowsPath`
|
|
1007
|
+
(exported by #1152) is true, mirroring `file-role.ts`'s fix exactly —
|
|
1008
|
+
shape-conditional, not shape-committed, so native-OS classification is
|
|
1009
|
+
unchanged. The strong directory-segment match (`pathSegments`, which splits
|
|
1010
|
+
on `[\\/]+`) was already shape-safe and untouched. Fail-then-pass regression
|
|
1011
|
+
tests cover a `C:\...`-shaped lockfile and `.d.ts` literal.
|
|
1012
|
+
- **Quick-mode background warmup kept a one-shot `pi -p`/`--print` process alive (closes #1154)** —
|
|
1013
|
+
`handleSessionStart` forces **quick mode** for both a real `pi -p`/`--print`
|
|
1014
|
+
one-shot AND an interactive process's first session (to protect keystroke
|
|
1015
|
+
latency, then warm caches for the next `/new`). Quick mode scheduled a +2s
|
|
1016
|
+
background warmup that (1) armed a `setTimeout` that was **not** `.unref()`'d —
|
|
1017
|
+
violating the repo-wide convention that every background timer is unref'd
|
|
1018
|
+
(`clients/runtime-session.ts` previously had zero `.unref()` calls) — and
|
|
1019
|
+
(2) launched LSP-prewarm children + a language-profile source walk that
|
|
1020
|
+
outlive settle with **no `session_shutdown` abort**. In a one-shot (which
|
|
1021
|
+
exits right after the turn and has no future session to warm) both are pure
|
|
1022
|
+
waste AND referenced-handle keep-alives that hold the settled process open —
|
|
1023
|
+
the located **#1122 hypothesis-A** concern and a member of the
|
|
1024
|
+
one-shot-retained-by-a-referenced-handle class of #1097/#1110/#1148/#1149.
|
|
1025
|
+
Fixed by (a) skipping the warmup entirely in print mode via a new shared
|
|
1026
|
+
`isPrintMode()` helper (`clients/print-mode.ts`) — an interactive first
|
|
1027
|
+
session (quick but not print) still warms — and (b) `.unref()`'ing the warmup
|
|
1028
|
+
timer as defense-in-depth + convention conformity. Also unref'd the adjacent
|
|
1029
|
+
full-mode `scheduleStartupScans` deferred timers (same latent violation, not a
|
|
1030
|
+
one-shot member today). This resolves the #1122 hypothesis-A tail (cross-ref
|
|
1031
|
+
#1122). Fail-then-pass regression tests assert the warmup does not run in print
|
|
1032
|
+
mode and the scheduled warmup timer is unref'd.
|
|
1033
|
+
- **`detectFileRole` misclassified a Windows-shaped path off native Windows (closes #1152, latent sibling of #1150/#1151)** —
|
|
1034
|
+
`clients/file-role.ts:detectFileRole` used the module-default `basename`/
|
|
1035
|
+
`dirname` (POSIX on Linux) even when the input path was Windows-shaped
|
|
1036
|
+
(drive letter or UNC prefix). A backslash-only `C:\...` path has no `/`
|
|
1037
|
+
for POSIX `dirname` to find, so it collapsed to `"."` and the dir-based
|
|
1038
|
+
role branches (`/tests/`, `/spec/`, generated-marker dirs) silently
|
|
1039
|
+
misclassified — the byte-identical forward-slash form already worked,
|
|
1040
|
+
since POSIX `dirname` handles `/` regardless of the leading drive
|
|
1041
|
+
letter. `detectFileRole` is platform-native by design, so the fix is
|
|
1042
|
+
shape-conditional rather than shape-committed (unlike #1151's
|
|
1043
|
+
`resolveNonExisting`, which is already win32-committed): a Windows-shaped
|
|
1044
|
+
path (per the now-exported `isWindowsPath`, `clients/path-utils.ts`) is
|
|
1045
|
+
parsed with `path.win32.basename`/`dirname` regardless of the running
|
|
1046
|
+
OS; a same-OS-native path is unaffected (native win32 already equals the
|
|
1047
|
+
module default there; native POSIX was never win32-shaped). Added
|
|
1048
|
+
regression coverage in `tests/clients/file-role.test.ts` asserting
|
|
1049
|
+
coherent `"test"`/`"init"` classification for `C:\...`, `C:/...`, and
|
|
1050
|
+
UNC-shaped inputs regardless of the running OS.
|
|
1051
|
+
`clients/lsp/server.ts:639`'s `normalizeSlashKey(path.dirname(path.resolve(file)))`
|
|
1052
|
+
(flagged in #1151's review as the same shape) was audited and verified
|
|
1053
|
+
**safe, not fixed**: `path.resolve(file)` runs before `dirname`, so by
|
|
1054
|
+
the time `dirname` sees it the value is already coerced to the running
|
|
1055
|
+
OS's native absolute-path shape — `file` here is always a real on-disk
|
|
1056
|
+
path produced by this process's own directory walking (extension-root
|
|
1057
|
+
resolution backed by real `existsSync`/`stat` probes, confirmed via
|
|
1058
|
+
`tests/clients/lsp/typescript-extension-root.test.ts`), never a
|
|
1059
|
+
persisted or cross-OS-supplied literal. Same "platform-native by
|
|
1060
|
+
design" exemption #1151 already applied to `path-utils.ts`'s
|
|
1061
|
+
`walkUpDirs`/`findNearestContaining` family.
|
|
1062
|
+
- **Orphan-reaper fire-and-forget PowerShell/`ps` spawns kept a completed `pi --print` alive past settle (closes #1153)** —
|
|
1063
|
+
the orphan reaper (`clients/instance-reaper.ts`) is fired fire-and-forget from
|
|
1064
|
+
`session_start` (`index.ts` `sweepOrphans`/`sweepUntrackedOrphans`), not
|
|
1065
|
+
awaited and not gated out of one-shot/`--print` mode. Its OS-process-table
|
|
1066
|
+
enumeration spawns (`enumerateManagedProcesses`, `queryCommandLines`,
|
|
1067
|
+
`findPidsByMarkerWindows` — PowerShell on Windows, `ps` on POSIX) used
|
|
1068
|
+
`stdio:["ignore","pipe","ignore"]` with a `data` listener and **no `.unref()`
|
|
1069
|
+
anywhere in the file**. A piped, listener-attached stdout stream keeps the
|
|
1070
|
+
event loop REFERENCED until the child `close`s, and `sweepUntrackedOrphans`
|
|
1071
|
+
guarantees ≥1 such PowerShell on every Windows `session_start` — so a settled
|
|
1072
|
+
one-shot process could not exit until that PowerShell finished cold-starting
|
|
1073
|
+
(routinely 300 ms–2 s). This is the child-process member of the referenced-
|
|
1074
|
+
handle class (#1097/#1110 timers, #1148/#1149 worker ports). Fixed by
|
|
1075
|
+
`unref()`-ing every reaper child AND its stdio pipes (a `child.unref()` alone
|
|
1076
|
+
does not release a piped stdout that re-refs the loop) via a shared
|
|
1077
|
+
`unrefReaperChild` helper applied at all six spawn sites (the five enumeration
|
|
1078
|
+
spawns plus `killPidTree`'s `taskkill`). Unref, not a print-mode skip: the
|
|
1079
|
+
reaper is a machine-wide orphan backstop, not a next-session-only concern, so
|
|
1080
|
+
gating it out of `--print` would blind orphan cleanup on print-only machines
|
|
1081
|
+
(CI/automation/subagents — exactly where one-shots dominate and orphans
|
|
1082
|
+
accumulate); unref preserves the sweep in interactive sessions (the loop stays
|
|
1083
|
+
referenced for other reasons, so every child's `close` still fires and the
|
|
1084
|
+
sweep completes) while letting a genuinely-settled one-shot exit without
|
|
1085
|
+
waiting. Regression test spawns a fake child per spawn site and asserts the
|
|
1086
|
+
child + its stdout are unref'd (fails pre-fix, passes post-fix).
|
|
1087
|
+
- **Persistence workers could keep completed one-shot processes alive ([#1148](https://github.com/apmantza/pi-lens/issues/1148))** — project-snapshot and review-graph workers called `unref()` before registering their `"message"` listeners, and Node re-referenced the public `MessagePort` when each listener was added. Both workers now install all lifecycle listeners before `unref()`, so persistence remains asynchronous without retaining an otherwise-finished `pi --print` or subprocess workflow. Real child-process regression tests require both persistence paths to finish writing and exit naturally.
|
|
1088
|
+
- **`normalizeFilePath` mangled a Windows-shaped path on non-Windows OS (closes #1150)** —
|
|
1089
|
+
`normalizeFilePath` commits to its win32 branch by path *shape*
|
|
1090
|
+
(`isWindowsPath`), so a `C:/…`- or `C:\…`-shaped path enters that branch on
|
|
1091
|
+
ANY OS — but `resolveNonExisting`'s upward walk used the module-default
|
|
1092
|
+
`dirname` (POSIX on Linux) while the rest of the branch already used
|
|
1093
|
+
`win32.resolve`/`win32.normalize`. On Linux the POSIX `dirname` found no
|
|
1094
|
+
separator in the win32-resolved path, collapsed to `.`, stopped the walk at
|
|
1095
|
+
`process.cwd()`, and produced `<cwd>/file.ts` instead of the literal key — so
|
|
1096
|
+
a Windows-shaped path normalized to a DIFFERENT key on Linux than the
|
|
1097
|
+
byte-identical literal it is on Windows (the #1024 OS-divergence class,
|
|
1098
|
+
surfaced by #1139's green-on-Windows/red-on-Linux-CI). Fixed by using
|
|
1099
|
+
`win32.dirname` in the win32-committed branch, making the function internally
|
|
1100
|
+
coherent regardless of the running OS. Guarded by a both-OS-meaningful
|
|
1101
|
+
regression test (native win32 path on Windows; shape-committed win32 branch on
|
|
1102
|
+
Linux) and an AGENTS.md convention: tests must derive `normalizeMapKey`-keyed
|
|
1103
|
+
structure keys via `normalizeMapKey`, never hardcode a drive-letter literal.
|
|
1104
|
+
- **Workspace-edit preflight: five contained P3 deferrals from #1085 (refs #1085)** —
|
|
1105
|
+
`clients/lsp/edits.ts`:
|
|
1106
|
+
- **P3-2** `mergeWorkspaceTextEditsByPriority`'s exact-duplicate dedup
|
|
1107
|
+
collapsed identical zero-width inserts duplicated within ONE server's
|
|
1108
|
+
own edit on the `renameFile` merge path, contradicting the documented
|
|
1109
|
+
multiplicity invariant (several identical zero-width inserts at one
|
|
1110
|
+
point are meaningful, same as `validateTextEdits` on the normal apply
|
|
1111
|
+
path). The dedup now only applies to non-empty ranges; a genuine
|
|
1112
|
+
cross-server exact duplicate (two servers proposing the identical
|
|
1113
|
+
non-empty replace) still collapses to one.
|
|
1114
|
+
- **P3-3** an ordered edit `rename(b→c); create(b); textEdit(b)` was
|
|
1115
|
+
falsely rejected in preflight ("text edit target does not exist"): a
|
|
1116
|
+
`create` at a path vacated by an earlier rename in the same edit had
|
|
1117
|
+
`resolveVirtualPath` return `undefined` (correct — the path is
|
|
1118
|
+
virtually gone), so the created state was silently discarded instead
|
|
1119
|
+
of recorded. Added a `virtualOverrides` overlay, keyed on the raw
|
|
1120
|
+
query path, consulted only when `resolveVirtualPath` cannot resolve a
|
|
1121
|
+
physical address, so a later op at that same path (e.g. the trailing
|
|
1122
|
+
text edit) sees the re-established file.
|
|
1123
|
+
- **P3-4** `version: null` (LSP 3.17: "don't check") combined with a
|
|
1124
|
+
numeric version for the SAME uri across two edit containers incorrectly
|
|
1125
|
+
threw "conflicting text document versions". A numeric version is now
|
|
1126
|
+
authoritative and adopted over a `null`/`undefined` counterpart; two
|
|
1127
|
+
genuinely different numeric versions still conflict, and a numeric
|
|
1128
|
+
version adopted from a `null` pairing is still checked against the live
|
|
1129
|
+
document version (a stale adopted numeric is still rejected).
|
|
1130
|
+
- **P3-6** `fileDetails[].importsChanged` reported "the file contains any
|
|
1131
|
+
import statement" (`/^import\s/m.test(updated)`) rather than "this edit
|
|
1132
|
+
changed an import/re-export line", over-invalidating downstream
|
|
1133
|
+
dependency-graph re-checks (`cache-manager.ts`'s `importsChanged`
|
|
1134
|
+
filter, `lsp-mutation.ts`'s `addModifiedRange`) on every edit to a file
|
|
1135
|
+
that merely already had imports. Now compares an import/re-export-from
|
|
1136
|
+
line signature of the pre- and post-edit content; unchanged unless the
|
|
1137
|
+
edit actually touched those lines. `create`/`rename`/`delete` keep
|
|
1138
|
+
their existing conservative flags (a create writes an empty file;
|
|
1139
|
+
rename/delete are structural, not diffable against pre-edit text).
|
|
1140
|
+
- **P3-8** an ordered edit that CREATEs `foo.txt` then case-renames it to
|
|
1141
|
+
`Foo.txt` within the SAME edit fail-closed with "destination already
|
|
1142
|
+
exists" on a case-insensitive FS: the alias check (`isSameFsEntry`)
|
|
1143
|
+
lstats disk, but the just-created file exists only in the preflight's
|
|
1144
|
+
virtual overlay, never on disk yet. The rename branch now also treats
|
|
1145
|
+
a rename as an alias when the destination's cached `VirtualFile`
|
|
1146
|
+
object is referentially identical to the source's (both `stateFor`
|
|
1147
|
+
calls resolve through the same case-folded map key), before falling
|
|
1148
|
+
back to the physical `isSameFsEntry` probe — which is unchanged for
|
|
1149
|
+
genuinely-physical paths, so the #1024/#1120 ino-guard and
|
|
1150
|
+
case-sensitivity invariants are untouched.
|
|
1151
|
+
|
|
1152
|
+
**Deferred at the time (now fixed, see below):** P3-5 and P3-7 were
|
|
1153
|
+
rehomed to #1147; see the #1085 closing comment for that history.
|
|
1154
|
+
- **Workspace-edit CRLF boundary class + rename close-failure plaintext reopen (closes #1147, refs #1085)** —
|
|
1155
|
+
the two P3 deferrals left open after #1146:
|
|
1156
|
+
- **P3-5** (`clients/lsp/edits.ts`) — #1120 fixed only the past-EOL clamp
|
|
1157
|
+
member of the CRLF-boundary class. Two general members remained: (a) a
|
|
1158
|
+
`newText` containing a bare `\n` was spliced verbatim into a CRLF
|
|
1159
|
+
file's content, producing mixed line endings, because the LSP
|
|
1160
|
+
workspace-edit apply path (unlike the host-edit path) never
|
|
1161
|
+
EOL-normalized `newText`; (b) `utf16Position`'s past-EOL clamp only
|
|
1162
|
+
triggered on `character > wireLength`, so an in-bounds,
|
|
1163
|
+
caller-supplied `character === wireLength` on a line whose `\r` is
|
|
1164
|
+
folded into `lineTextAt`'s with-`\r` length landed the position
|
|
1165
|
+
squarely between `\r` and `\n`, splitting the pair on write. Fixed by
|
|
1166
|
+
(a) normalizing every `newText` through the exact
|
|
1167
|
+
`detectLineEnding`/`normalizeToLF`/`restoreLineEndings` contract
|
|
1168
|
+
`clients/host-edit-normalize.ts` already uses for the host-edit path
|
|
1169
|
+
(LF files are unaffected — `restoreLineEndings` is the identity for
|
|
1170
|
+
`"\n"`), applied once in `normalizeTextEditsForContent` so it covers
|
|
1171
|
+
both the preflight virtual-content chain and the final on-disk write;
|
|
1172
|
+
(b) clamping on `character > clampedWireLength` (the `\r`-stripped
|
|
1173
|
+
length) instead of the with-`\r` length, which subsumes and simplifies
|
|
1174
|
+
#1120's original past-EOL clamp into the same branch. All of #1120's
|
|
1175
|
+
existing invariants (single application-ordering sort, clamp, tie-break)
|
|
1176
|
+
are unchanged and covered by the full `edits.test.ts` suite.
|
|
1177
|
+
- **P3-7** (`clients/lsp/index.ts`) — the rename close-failure recovery
|
|
1178
|
+
path reopened the old document as a hardcoded `"plaintext"` languageId,
|
|
1179
|
+
degrading that server's diagnostics until the next genuine open. Now
|
|
1180
|
+
reopens with `getLanguageId(oldFilePath) ?? "plaintext"` — the same
|
|
1181
|
+
resolver every genuine `notify.open` call in this file already uses —
|
|
1182
|
+
so a recognized extension reopens with its real language ID and only a
|
|
1183
|
+
genuinely unrecognized extension still falls back to `"plaintext"`.
|
|
1184
|
+
- **Post-init runtime exits now count toward the LSP circuit breaker (closes #1127)**
|
|
1185
|
+
— `LSPService`'s (`clients/lsp/index.ts`) exponential-backoff breaker
|
|
1186
|
+
(`failureCounts` → cooldown → permanent-disable after
|
|
1187
|
+
`BROKEN_PERMANENT_AFTER`) only incremented on spawn/initialize failure.
|
|
1188
|
+
A server whose spawn SUCCEEDS but then exits shortly after (opengrep's
|
|
1189
|
+
post-init "Unhandled message" JSON-RPC crash, per #1122's Phase C
|
|
1190
|
+
corroborating-signal review — 37 respawns in one real session, never
|
|
1191
|
+
converging) hit the "dead client — needs respawn" path instead, which
|
|
1192
|
+
never touched the breaker: `failureCounts` was already cleared by the
|
|
1193
|
+
preceding successful spawn and the runtime exit itself was never counted.
|
|
1194
|
+
Fixed by adding a parallel `runtimeExitCounts` counter fed only by EARLY
|
|
1195
|
+
(lifetime < 60s) non-intentional exits, sharing the same cooldown formula
|
|
1196
|
+
and the same `state.broken`/`permanentlyBroken` maps as the existing
|
|
1197
|
+
breaker — tracked separately from `failureCounts` specifically because a
|
|
1198
|
+
successful respawn (which correctly resets the spawn/init failure streak)
|
|
1199
|
+
is not proof of health for a crash-loop server, so reusing that map would
|
|
1200
|
+
erase the streak on every respawn attempt (the #1127 bug). Deliberate
|
|
1201
|
+
teardowns (session reset, `#743` notify-backpressure eviction, generation
|
|
1202
|
+
handoffs) call `shutdown()` themselves before the process exits and set
|
|
1203
|
+
`shutdownRequested`; a new `wasShutdownIntentional()` accessor on
|
|
1204
|
+
`LSPClientInfo` (`clients/lsp/client.ts`) exposes that flag so the breaker
|
|
1205
|
+
distinguishes a genuine crash from a restart it initiated and never counts
|
|
1206
|
+
the latter. Adversarial review caught that lifetime was originally
|
|
1207
|
+
computed from the moment a dead client is lazily DETECTED (the next
|
|
1208
|
+
`getClientForFile` attach), not from when it actually died — #1127's
|
|
1209
|
+
documented pattern is attach-triggered respawns minutes to hours apart, so
|
|
1210
|
+
an early crash detected an hour later would misread as a long healthy run
|
|
1211
|
+
and never count. Fixed by stamping a real `exitedAt` on `LSPClientState`
|
|
1212
|
+
the moment the client's connection/process actually dies (first of
|
|
1213
|
+
`onError`/`onClose`/process `exit` to fire) and exposing it via a new
|
|
1214
|
+
`getExitedAt()` accessor; the respawn site computes lifetime as
|
|
1215
|
+
`exitedAt - spawnedAt`, falling back to the detection-time delta only when
|
|
1216
|
+
`exitedAt` is unexpectedly unset. New coverage in
|
|
1217
|
+
`tests/clients/lsp/service-runtime-exit-breaker.test.ts`: a crash-loop
|
|
1218
|
+
respawn sequence converges to permanent-disable instead of respawning
|
|
1219
|
+
forever (fails against pre-fix behavior), an early death with detection
|
|
1220
|
+
delayed by hours still counts (death time, not detection time, decides —
|
|
1221
|
+
fails against the pre-fix detection-time computation in both directions),
|
|
1222
|
+
a deliberate `shutdown()`-driven restart sequence never counts, a runtime
|
|
1223
|
+
exit past the lifetime threshold resets the streak instead of counting,
|
|
1224
|
+
and the REAL `#743` notify-write-backpressure eviction path (driven
|
|
1225
|
+
through `touchFile`, not a synthetic stand-in) never double-counts against
|
|
1226
|
+
this breaker. Full existing LSP suite (536 tests) stays green.
|
|
1227
|
+
|
|
1228
|
+
- **`terragrunt hcl fmt` reported success when it never ran (refs #1117)** —
|
|
1229
|
+
`formatFile` decided success from `result.error` alone, and a formatter that
|
|
1230
|
+
fails leaves the file byte-identical, which is indistinguishable from
|
|
1231
|
+
"already formatted". A terragrunt binary predating the `hcl` command group
|
|
1232
|
+
exits non-zero without touching the file, and pi-lens reported
|
|
1233
|
+
`success: true, changed: false`. `FormatterInfo` gained an opt-in
|
|
1234
|
+
`strictExitCode`, set on `terragrunt-hcl`, which folds the exit status into
|
|
1235
|
+
the check and surfaces the tool's own first stderr line as the error. It is
|
|
1236
|
+
opt-in rather than global because the lint-autofix formatters (`rubocop -a`,
|
|
1237
|
+
`ktlint -F`, `standardrb --fix`, `sqlfluff fix`) exit non-zero when offenses
|
|
1238
|
+
remain AFTER a successful rewrite; failing those would surface a formatter
|
|
1239
|
+
error on every file with an unfixable offense.
|
|
1240
|
+
- **Runners reported a clean file on a non-zero exit with no output (refs #1117)** —
|
|
1241
|
+
seven runners gated their "the tool never ran" skip on
|
|
1242
|
+
`result.error` alone. `safeSpawnAsync` sets `error` only for spawn, timeout,
|
|
1243
|
+
signal and abort failures, and resolves a NORMAL exit with no `error` at any
|
|
1244
|
+
status (see `SpawnResult.failure`: "nonzero exit statuses are not spawn
|
|
1245
|
+
failures"). So an unknown subcommand, a rejected flag, or a config that fails
|
|
1246
|
+
to load — non-zero exit, message on stderr, empty stdout — fell straight
|
|
1247
|
+
through the guard, parsed `""` into zero diagnostics, and reported the file as
|
|
1248
|
+
clean. Terragrunt was the visible case: its docblock claimed a binary
|
|
1249
|
+
predating the `hcl` command group was classified SKIPPED, and it was not. The
|
|
1250
|
+
test now lives in `spawnFailedWithNoOutput`
|
|
1251
|
+
(`clients/dispatch/runners/utils/spawn-outcome.ts`) and folds `status` into
|
|
1252
|
+
the check; terragrunt, tflint, hadolint, taplo, trivy-config, htmlhint and
|
|
1253
|
+
detekt all use it. Runners that exit non-zero BECAUSE they found something are
|
|
1254
|
+
unaffected, since their findings are on the stream the guard tests. ktlint,
|
|
1255
|
+
prisma-validate, gleam-check, zig-check, dart-analyze and elixir-check already
|
|
1256
|
+
covered the case through their own non-zero follow-up branches and are
|
|
1257
|
+
unchanged.
|
|
1258
|
+
- **Bare-binary GitHub release assets skip signature siblings** — the installer
|
|
1259
|
+
picked a release asset with `assets.find(a => a.name.includes(substring))`.
|
|
1260
|
+
Archive-based tools are unaffected (their substrings end in `.zip`/`.tar.gz`),
|
|
1261
|
+
but the three bare-binary entries (terragrunt, marksman, expert) resolve to
|
|
1262
|
+
the FULL asset name, which is a strict prefix of every `.asc`/`.sig`/`.sha256`
|
|
1263
|
+
sibling, so whichever the release listed first would be downloaded and marked
|
|
1264
|
+
executable as the binary. Selection now lives in an exported
|
|
1265
|
+
`pickReleaseAsset`: exact name first, then a substring match that excludes
|
|
1266
|
+
known sidecar suffixes.
|
|
1267
|
+
- **Cascade fallback-display paths re-displayed bound-false LSP snapshots (refs #1104)** —
|
|
1268
|
+
#1100 gated the cascade's RECONCILE path (the footer/widget) onto content
|
|
1269
|
+
binding (`boundToCurrentDisk`), but two DEGRADED-fallback DISPLAY paths in
|
|
1270
|
+
`clients/dispatch/integration.ts` still re-read TTL-fresh `getAllDiagnostics()`
|
|
1271
|
+
snapshots without consulting binding at all: the touch-error fallback (a
|
|
1272
|
+
failed active LSP touch falling back to the passive snapshot) and
|
|
1273
|
+
`appendFallbackNeighbors` (the CR-3/A2 degraded-fallback path when no
|
|
1274
|
+
neighbor produced trustworthy LSP data). A bound-false snapshot — diagnostics
|
|
1275
|
+
computed against a DIFFERENT disk state than what's currently on disk, e.g. a
|
|
1276
|
+
pre-fix-edit read — could still reach cascade OUTPUT even though the widget
|
|
1277
|
+
was protected. Both sites now apply the same false/`"unknown"`/true contract
|
|
1278
|
+
#1095/#1100 already established for reconcile: `false` → skip the stale
|
|
1279
|
+
display (logged via the cascade channel with `bindingState`), `"unknown"` →
|
|
1280
|
+
unchanged (the pre-existing fallback contract), `true` → display. HONESTY
|
|
1281
|
+
fix: filtering a display candidate could otherwise make a genuinely degraded
|
|
1282
|
+
cascade look clean, so when every fallback candidate a run considered was
|
|
1283
|
+
binding-rejected and nothing else produced output, the run now carries the
|
|
1284
|
+
same `indeterminate` marker #1023 built for a degraded graph compute (new
|
|
1285
|
+
`CascadeIndeterminateReason: "lsp_binding_rejected"`), so the turn-end
|
|
1286
|
+
advisory still surfaces an honest note instead of silence. The `resultId`
|
|
1287
|
+
pull-diagnostics plumbing that #1104 also tracks remains open — this covers
|
|
1288
|
+
only the cascade display-binding gap (#1100 review P3-1).
|
|
1289
|
+
- **`parseSymbolKey` mis-parsed LSP-fallback symbol kinds (refs #1088)** —
|
|
1290
|
+
the canonical-id parser whitelisted only the 7 kinds `buildSymbolId` mints
|
|
1291
|
+
directly, but `addLspFallbackSymbols` mints ids using the much larger
|
|
1292
|
+
`lspSymbolKindName` vocabulary (`enum`, `constant`, `struct`, `namespace`,
|
|
1293
|
+
`field`, `constructor`, `module`, the `lsp-symbol-<n>` catch-all, ...). Any
|
|
1294
|
+
of those ids failed the whitelist and fell through to the legacy
|
|
1295
|
+
last-colon split, shearing the id at the wrong boundary (e.g.
|
|
1296
|
+
`c:\p\a.kt:Color:enum:42` parsed as `filePath: "c:\p\a.kt:Color:enum"`,
|
|
1297
|
+
`symbolName: "42"`). `clients/review-graph/symbol-id.ts`'s
|
|
1298
|
+
`parseSymbolKey` now matches the trailing `:<kind-token>:<digits>` shape
|
|
1299
|
+
STRUCTURALLY (any lowercase, optionally hyphen-segmented token) instead of
|
|
1300
|
+
whitelisting specific kind strings, so it stays correct for the open-ended
|
|
1301
|
+
LSP-fallback vocabulary without needing a matching update per kind.
|
|
1302
|
+
- **Java try-with-resources suppression was dead (refs #1089)** —
|
|
1303
|
+
`clients/tree-sitter-client.ts`'s `not_closed_or_try_with_resources`
|
|
1304
|
+
post-filter built its resource-word regex as `` new RegExp(`\b${resource}\b`) ``
|
|
1305
|
+
inside a template literal, where `\b` is the BACKSPACE control character
|
|
1306
|
+
(U+0008), not a regex word-boundary escape. The regex could never match,
|
|
1307
|
+
so the Java 9 short-form `try (resource) { ... }` was never recognized as
|
|
1308
|
+
closing its resource — only an explicit `.close()` call suppressed the
|
|
1309
|
+
"resource should be closed" finding. Fixed to `\\b` plus a regex-escaped
|
|
1310
|
+
identifier. No shipped Java fixture used try-with-resources syntax, so no
|
|
1311
|
+
existing expected-findings fixture changes with this fix.
|
|
1312
|
+
- **Raw NUL byte in `review-graph/builder.ts` made the file look binary to grep (refs #1088)** — the checkpoint's `ignoredIds` join separator was a
|
|
1313
|
+
literal 0x00 byte embedded in source, which makes `ripgrep`/`grep` treat
|
|
1314
|
+
the whole 4,900+ line file as binary and stop scanning partway through —
|
|
1315
|
+
silently defeating the AGENTS.md-mandated repo-wide grep sweep for every
|
|
1316
|
+
commit that touched this file. Replaced the raw byte with the `\u0000`
|
|
1317
|
+
escape sequence, which is the byte-identical runtime string (same
|
|
1318
|
+
`hashIgnoredIds` function computes both the write-time and read-time
|
|
1319
|
+
hash, so no persisted checkpoint compatibility is affected).
|
|
1320
|
+
- **`buildCallGraph` same-file evidence accounting hardened + dead code removed (refs #1089)** — added regression coverage for the audited
|
|
1321
|
+
same-file evidence class (divergent path forms for the same file must
|
|
1322
|
+
both be recognized as same-file AND counted exactly once per reference,
|
|
1323
|
+
keeping the coverage sum invariant `validatePersistedCallGraph` enforces
|
|
1324
|
+
on every load); the normalized-compare fix for this had already landed
|
|
1325
|
+
upstream, so this closes out the finding with a fail-then-pass regression
|
|
1326
|
+
test rather than a behavior change. Also removed `clients/call-graph.ts`'s
|
|
1327
|
+
dead duplicate `saveCallGraph` overload signature (byte-identical to the
|
|
1328
|
+
implementation signature) and the unreachable `{}`-spread branch on the
|
|
1329
|
+
required `identity` parameter.
|
|
1330
|
+
- **JS destructured params dropped from `parameterCount` (refs #1089)** —
|
|
1331
|
+
`clients/dispatch/facts/function-facts.ts`'s `getParameters` recognized
|
|
1332
|
+
`required_parameter`/`optional_parameter` wrapper nodes (how TypeScript's
|
|
1333
|
+
grammar represents every parameter, destructured or not) but not the bare
|
|
1334
|
+
`object_pattern`/`array_pattern` nodes plain JavaScript's grammar uses
|
|
1335
|
+
for a top-level destructured parameter with no wrapper. `function
|
|
1336
|
+
f({a, b})` counted 0 parameters in a `.js` file while the TS-annotated
|
|
1337
|
+
equivalent counted correctly. Added JS-vs-TS parity tests.
|
|
1338
|
+
- **`call-graph.ts`'s exported `CACHE_VERSION` renamed to `CALL_GRAPH_CACHE_VERSION`** — collided in name (not value — separate
|
|
1339
|
+
modules) with `clients/cache/rule-cache.ts`'s own `CACHE_VERSION` export;
|
|
1340
|
+
renamed to match the `<SUBSYSTEM>_CACHE_VERSION` convention already used
|
|
1341
|
+
by `WORKSPACE_DIAGNOSTICS_CACHE_VERSION` and
|
|
1342
|
+
`PROJECT_DIAGNOSTICS_CACHE_VERSION`. Updated the two test-file imports.
|
|
1343
|
+
- **Dead SIGTERM→SIGKILL escalation guard on non-Windows kills (closes #1114)**
|
|
1344
|
+
— `clients/safe-spawn.ts`'s non-Windows `killTree` branch armed a 1s
|
|
1345
|
+
escalation timer gated on `if (!child.killed) child.kill("SIGKILL")`, but
|
|
1346
|
+
Node sets `ChildProcess#killed = true` the moment `kill()` successfully
|
|
1347
|
+
SENDS a signal — not when the child actually dies — so immediately after
|
|
1348
|
+
the `child.kill("SIGTERM")` one line above, the guard was always false
|
|
1349
|
+
and the SIGKILL escalation could never fire: a SIGTERM-ignoring child on
|
|
1350
|
+
Linux/macOS was never force-killed. Fixed by tracking OBSERVED death via a
|
|
1351
|
+
`closed` flag set synchronously (before any `await`) in the close/error
|
|
1352
|
+
handlers, gating the escalation on `!closed` instead — composes cleanly
|
|
1353
|
+
with the existing #1109/#1113 `escalationTimer` clear-on-close fix rather
|
|
1354
|
+
than switching to the LSP `killProcessTree` analog's unconditional-SIGKILL
|
|
1355
|
+
design, since `safeSpawnAsync` already has a real per-call close/error
|
|
1356
|
+
observation point to hang the flag off of. Proven with a new "child
|
|
1357
|
+
ignores SIGTERM → SIGKILL sent at the 1s mark" test
|
|
1358
|
+
(`tests/clients/safe-spawn-kill-escalation-timer.test.ts`) that fails
|
|
1359
|
+
against the pre-fix guard and passes post-fix; the existing #1109 timer-leak
|
|
1360
|
+
tests (escalation timer cleared when close/error DOES arrive) remain green.
|
|
1361
|
+
**Class sweep** of every `.killed` consumer under `clients/` and `scripts/`
|
|
1362
|
+
found two siblings of the same shape in `clients/lsp/client.ts`'s
|
|
1363
|
+
`killProcessTree`/`createLSPClient` and fixed both in this PR: (1) the
|
|
1364
|
+
`fast`-shutdown escalation timer checked `!proc.killed`, but the primary
|
|
1365
|
+
SIGTERM send there goes through the raw `process.kill(-pid, …)` process-group
|
|
1366
|
+
call (which never touches `proc.killed`), so the guard was either always-true
|
|
1367
|
+
(unconditional SIGKILL after the window on the common group-kill path) or
|
|
1368
|
+
dead (on the direct-child fallback path) — now tracked via a real `exit`
|
|
1369
|
+
listener set once up front, seeded from the same `exitCode`/`signalCode`
|
|
1370
|
+
pre-check the function's top-of-body early return already uses (a process
|
|
1371
|
+
that was already dead on entry — reachable when `options.processExiting`
|
|
1372
|
+
skips that early return — would otherwise miss its own "exit" event and
|
|
1373
|
+
still draw a redundant group SIGKILL at the escalation window); (2) the
|
|
1374
|
+
`initialize()`-timeout 2s SIGKILL backstop had the identical always-true
|
|
1375
|
+
`!lspProcess.process.killed` guard — switched to `lspProcess.process.exitCode
|
|
1376
|
+
=== null && lspProcess.process.signalCode === null` (both, not `exitCode`
|
|
1377
|
+
alone: a process killed BY a signal — the common case here, since
|
|
1378
|
+
`killProcessTree` above it signals rather than lets the process exit on its
|
|
1379
|
+
own — has `exitCode === null` forever and only `signalCode` set, so
|
|
1380
|
+
`exitCode` alone still re-armed the backstop's kill against an
|
|
1381
|
+
already-dead corpse; harmless in practice since `ChildProcess#kill()` on an
|
|
1382
|
+
exited handle is a swallowed no-op, but not an accurate "still alive"
|
|
1383
|
+
read). Other `.killed` reads audited and left as-is because they're
|
|
1384
|
+
liveness/status checks, not escalation-action gates: `isClientAlive`'s
|
|
1385
|
+
`!state.lspProcess.process.killed` (redundant with `isDestroyed`, already
|
|
1386
|
+
set from real exit/close handlers), `checkProcessAlive`'s informational
|
|
1387
|
+
"was killed" health-check string, `launch.ts`'s post-spawn
|
|
1388
|
+
immediate-failure check (`proc.killed` read before any kill was ever sent),
|
|
1389
|
+
`scripts/with-test-lock.mjs`'s `.once`-registered first-forward guard, and
|
|
1390
|
+
`scripts/smoke-tools.mjs`'s read of Node's own `execFileSync` timeout-kill
|
|
1391
|
+
flag on the caught error object.
|
|
1392
|
+
|
|
1393
|
+
**Adversarial-review follow-up round:** the reviewer ran
|
|
1394
|
+
`kill-process-tree.test.ts` against PRE-fix `client.ts` and it passed 7/7 —
|
|
1395
|
+
the sibling fixes above had ZERO effective test coverage, because the
|
|
1396
|
+
"non-fast shutdown escalates" mock (and the other pre-existing mocks in
|
|
1397
|
+
that file) lacked `once`/never set `killed`, so BOTH the old dead guard and
|
|
1398
|
+
the new fix's guard were vacuously permissive against them (the #1106
|
|
1399
|
+
vacuous-mock class, recurring in mock form: a test's fixture is too weak to
|
|
1400
|
+
distinguish correct from broken behavior, so it passes either way). Fixed
|
|
1401
|
+
by: upgrading that test's mock to be `.once`-capable so it actually
|
|
1402
|
+
exercises the `exited`-flag logic; adding two new `fast`-shutdown tests with
|
|
1403
|
+
a real `.once`-capturing mock proving BOTH directions (exit observed before
|
|
1404
|
+
the 1.5s window → no group SIGKILL; no exit observed → group SIGKILL at the
|
|
1405
|
+
window) — the "no premature SIGKILL" direction fails against the pre-fix
|
|
1406
|
+
`!proc.killed` guard (proven by temporarily reverting the guard and
|
|
1407
|
+
re-running); and adding a real-subprocess POSIX-only test
|
|
1408
|
+
(`tests/clients/lsp/initialize-timeout-backstop.test.ts`, skipped on win32
|
|
1409
|
+
with an explicit reason — killProcessTree's Windows path is
|
|
1410
|
+
`taskkill`-based, not signal-based, and is already covered by the
|
|
1411
|
+
kill-process-tree suite) for the previously fully-untested `initialize()`
|
|
1412
|
+
2s backstop.
|
|
1413
|
+
- **Micro-gap sweep: recorded coverage/observability/doc gaps (refs #1106, refs #1104)**
|
|
1414
|
+
- `session-state-store.ts`'s `loadSessionState` STATE_VERSION reject path
|
|
1415
|
+
(a wrong-version persisted snapshot is ignored, not rehydrated) had no
|
|
1416
|
+
test; `STATE_VERSION` is now exported so the new test can drive the
|
|
1417
|
+
mismatch off the real constant (`STATE_VERSION + 1`) rather than a
|
|
1418
|
+
hardcoded literal (#1116 pattern).
|
|
1419
|
+
- `tests/clients/cache/rule-cache.test.ts`'s deliberate `raw.version = "v2"`
|
|
1420
|
+
schema-mismatch override now pins `expect(CACHE_VERSION).not.toBe("v2")`
|
|
1421
|
+
alongside it (#1082/#1116 pattern), so the assertion can't vacuously pass
|
|
1422
|
+
if `CACHE_VERSION` ever became `"v2"`.
|
|
1423
|
+
- The cascade `neighbor_touch` log entry (`clients/dispatch/integration.ts`)
|
|
1424
|
+
now carries an `inconclusive` boolean in its metadata alongside
|
|
1425
|
+
`bindingState`, so the two independent unconfirmed-touch causes (notify/
|
|
1426
|
+
diagnostics wait lapsed vs. disk-diverged binding, #1093/#1095) are
|
|
1427
|
+
distinguishable from `cascade.log` alone, without cross-referencing
|
|
1428
|
+
`latency.log`.
|
|
1429
|
+
- `tools/lens-diagnostics.ts`'s `includeGenerated` param description now
|
|
1430
|
+
states it only takes effect with `mode=full refreshRunners=cheap/all`
|
|
1431
|
+
(it's silently a no-op under `cached`/`none`, since no project scan runs
|
|
1432
|
+
to apply it to) — a doc-only fix from PR #1115's review.
|
|
1433
|
+
- **mtime-only cache freshness sweep (refs #1105)** — the #1092→#1096 arc bound
|
|
1434
|
+
LSP-diagnostics freshness to real content; this sweep audited the OTHER
|
|
1435
|
+
persisted/derived caches for the same "mtime unchanged ≠ content unchanged"
|
|
1436
|
+
class (git checkout timestamp restoration, formatters preserving mtime,
|
|
1437
|
+
same-second writes) and hardened the two CONTAINED members whose fix is free:
|
|
1438
|
+
- **word index** (`clients/word-index.ts`): the incremental session-warmup
|
|
1439
|
+
refresh gated re-read on mtime ALONE (`fileMtimes.get(file) !== mtimeMs`), so
|
|
1440
|
+
a mtime-preserving content change left stale identifiers served to
|
|
1441
|
+
`symbol_search`/`pilens_symbol_search`. It now stores a per-file byte `size`
|
|
1442
|
+
(the SAME `stat` the walk already runs to enforce the byte cap — zero extra
|
|
1443
|
+
I/O) and re-reads when mtime OR size differs, matching the review graph's
|
|
1444
|
+
gold-standard `size:mtimeMs` first filter. `size` is a new optional parallel
|
|
1445
|
+
array in the serialized snapshot (`SerializedWordIndex.fileSizes`); a
|
|
1446
|
+
pre-#1105 snapshot lacking it forces one self-healing full re-read on the
|
|
1447
|
+
next refresh (the safe direction) rather than a version bump.
|
|
1448
|
+
- **dependency checker** (`clients/dependency-checker.ts`):
|
|
1449
|
+
`DependencyChecker.importsChanged`'s mtime fast path (`cached.timestamp >=
|
|
1450
|
+
stat.mtimeMs`) skipped the madge circular-dep re-check on a mtime-preserving
|
|
1451
|
+
import edit; it now also requires `cached.size === stat.size`.
|
|
1452
|
+
|
|
1453
|
+
The residual (same mtime AND same byte length, changed content) is left as the
|
|
1454
|
+
same accepted residual the review graph itself carries — closing it needs an
|
|
1455
|
+
unconditional per-read content hash, which the event-loop/hot-path discipline
|
|
1456
|
+
forbids. The sweep's full verdict table (word-index/dependency-checker fixed;
|
|
1457
|
+
project-snapshot seq, installer probe-cache, sgconfig, reverse-deps, TreeCache,
|
|
1458
|
+
yaml-rule-parser bundled cache all SAFE/already-hardened; rule-cache disk cache
|
|
1459
|
+
filed as a follow-up because a content-confirm there would read 700+ rule files
|
|
1460
|
+
on the per-edit hot path) is recorded in the PR.
|
|
1461
|
+
- **Test-role files no longer leak into collateral cascade/impact surfaces (closes #1080)**
|
|
1462
|
+
— the review graph is already tests-free, but several
|
|
1463
|
+
collateral surfaces re-derived neighbors from OTHER sources that never saw
|
|
1464
|
+
that filter, so an UNIGNORED `*.test.*` / `tests/` file could still surface
|
|
1465
|
+
as cascade collateral. The existing `getProjectIgnoreMatcher` filtering only
|
|
1466
|
+
closed the ignored-file half of the gap (#297). A new shared predicate
|
|
1467
|
+
`clients/collateral-test-role.ts:isTestRoleCollateral` composes the SINGLE
|
|
1468
|
+
existing structural classifier (`clients/file-role.ts:detectFileRole`) — no
|
|
1469
|
+
second matcher, no private test-filename list — and is applied at every
|
|
1470
|
+
collateral producer boundary: (1) `computeCascadeForFile` filters
|
|
1471
|
+
`impact.directImporters` / `directCallers` / `neighborFiles` AFTER graph
|
|
1472
|
+
neighbors, reverse-deps, LSP reference expansion, and transitive expansion
|
|
1473
|
+
are merged, so a test neighbor is excluded from the active-touch/passive-
|
|
1474
|
+
snapshot set, the returned `impact`, AND the formatted header
|
|
1475
|
+
(`formatImpactCascade` reads `impact` verbatim for `Direct importers` /
|
|
1476
|
+
`Check next` counts and names) — module-level downstream files (added inside
|
|
1477
|
+
`computeImpactCascade`) are caught here at the consumption boundary rather
|
|
1478
|
+
than inside the shared graph query; (2) `appendFallbackNeighbors` drops an
|
|
1479
|
+
unignored test file from the passive-snapshot fallback (ignore filtering
|
|
1480
|
+
stays separate and unchanged); (3) `runtime-turn.ts` filters call-graph
|
|
1481
|
+
`impact()` results so a test caller appears in neither the turn-end advisory
|
|
1482
|
+
text nor the persisted delta; (4) `callGraphImpactToProjectDiagnostics`
|
|
1483
|
+
re-applies the predicate at the persistence producer boundary. Honesty
|
|
1484
|
+
preserved: a KNOWN `"test"` role is filtered, but a role-classification
|
|
1485
|
+
failure RETAINS the candidate (never a false clean), the ignore matcher's
|
|
1486
|
+
fail-open behavior is untouched, and LSP-unavailable/inconclusive cascade
|
|
1487
|
+
semantics are unchanged. Per-runner and auxiliary-profile `skipTestFiles`
|
|
1488
|
+
semantics, primary LSP diagnostics, intentional test-runner findings, and
|
|
1489
|
+
the generic project-diagnostics snapshot/delta display are all left as-is.
|
|
1490
|
+
|
|
1491
|
+
- **`no-bare-except` (Python, ast-grep) still flags dotted/qualified exception types as bare (closes [#1031](https://github.com/apmantza/pi-lens/issues/1031))** — the ast-grep twin of the #1026 tree-sitter fix never received its `attribute` alternative: `except asyncio.TimeoutError:` has no direct `identifier` child (the identifiers live inside the `attribute` node), so the `not: any:` check passed and the rule reported a false positive on every dotted exception type — live for binary-installed users, where the ast-grep LSP/CLI runs this rule on every Python file. The rule now accepts the same five exception-spec shapes as the tree-sitter filter's `bare_except_only`: identifier, attribute (dotted name), tuple, `parenthesized_expression`, and `as_pattern` — the parenthesized-single-name case is the same latent class and was swept in with it (verified firing pre-fix via the real CLI). Fixtures: dotted + parenthesized names added to `no-bare-except-test.yml` as valid cases; the bare `except:` invalid cases unchanged. Verified against the real ast-grep CLI: pre-fix fired on `except asyncio.TimeoutError:` and `except (ValueError):`, post-fix fires only on genuine bare `except:` among the identifier/attribute/tuple/parenthesized/as_pattern spec shapes (`subscript` specs like `except dict[str, int]:` remain a tracked gap on both surfaces); the full 251-case `ast-grep test` corpus passes.
|
|
1492
|
+
- **The warm IPC side-channel re-dispatched the same request on stray bytes (closes [#1219](https://github.com/apmantza/pi-lens/issues/1219))** — the server's socket `data` handler (`mcp/server.ts` `startIpcServer`) buffered chunks but never consumed the request line, so any further `data` event on the connection — stray bytes without a newline still re-found the original line — re-ran the whole warm `analyzeFile` pass (three dispatches per connection in the pre-fix repro). The channel's clients write exactly one newline-delimited request and read one reply (`requestWarmAnalyze` ends the socket), so the handler is now strictly one-shot: `createWarmIpcLineReader` (`clients/mcp/ipc.ts`, re-exported through the lens-engine seam) accumulates chunks, dispatches the first complete line exactly once, and ignores everything after it. The stdio JSON-RPC loop already consumed its lines and is untouched. Regression tests drive the real reader: one request + stray bytes → exactly one dispatch (pre-fix: three), a second newline-terminated request is ignored, and a request split across chunks still assembles.
|
|
1493
|
+
|
|
1494
|
+
- **`npm run test:unit` runs again, and actually excludes the integration files** — the script passed `--ignore <file>`, which is not a vitest option, so every invocation died with `CACError: Unknown option --ignore` before a single test loaded (broken since the tier split was written; CI never caught it because CI runs `npm test`). The rename to the real flag, `--exclude`, fixes the crash but not the intent: `vitest.config.ts` defines per-project `exclude` lists, and a project's `exclude` REPLACES the root/CLI value rather than merging with it, so a command-line `--exclude` is silently dropped (verified with `vitest list`: both integration files still resolved). The exclusion now lives in the config, keyed off the script name npm exports (`npm_lifecycle_event`), which survives the `with-test-lock` wrapper identically on every OS — unlike an inline `FOO=1 …` prefix, which cmd.exe cannot parse. `npm run test:unit` is now 456 files, `npm run test:integration` the other 2, and `npm test` all 458.
|
|
1495
|
+
- **`loop_block` no longer reports a machine sleep or paging stall as a pi-lens synchronous block (refs #1122, refs #1123 item 1)** — the event-loop occupancy probe (`clients/event-loop-monitor.ts`) wraps Node's native `monitorEventLoopDelay`, whose libuv timer-lag is measured against the monotonic clock (`uv_hrtime`/`QueryPerformanceCounter`). When the whole process is frozen — Windows Modern Standby, or paging thrash under commit-charge exhaustion — the next timer fires late by the entire wall-clock gap, and that gap was logged as a "block". Two distinct machine artifacts were confirmed against the Windows System event log: a 290,179 ms block lined up exactly with a 14:33:05Z→14:37:55Z Modern Standby window (Kernel-Power 506/507), reported byte-identically by two independent pids because the histogram is HDR-bucketed and quantizes ~290 s into one bucket; and a later silent host exit with **zero** sleep events but twelve Resource-Exhaustion-Detector (2004) events at 97% commit charge. `latency.log` also held multi-*hour* "blocks" that can only be overnight sleep. Comparing a wall clock to a monotonic clock does NOT catch this — on Windows both advance across Modern Standby — so the probe now uses **CPU accounting**: a genuine synchronous block of D ms burns ≈ D ms of main-thread CPU, so its window must have consumed ≥ ~D ms of CPU; a frozen/thrashing process consumes ~0 CPU across the gap. The histogram is now windowed **per turn** (its long-intended reset, previously never wired, so each block is attributable to its turn and its CPU budget is bounded), and a worst block above a 20 s floor that the window's CPU cannot account for is tagged `suspectSystemStall` in the `loop_block` metadata (alongside `windowCpuMs`/`windowWallMs` and the `lastPhase` attribution hint) rather than raising the genuine-block high-water — so a machine freeze can no longer permanently suppress logging of later real blocks, and `/lens-health` reports the worst *genuine* block. Sub-floor blocks are never auto-tagged, but the logged CPU-vs-wall ratio still exposes a shorter paging stall for a human. Pure `isSuspendSuspectedBlock` and `getLastLoggedPhase` seams are unit-tested without a real machine sleep.
|
|
1496
|
+
- **`runtime-tool-result.ts`'s three tool-result-turn maps now key through `PathKeyedMap` (refs #1086, the #210/#1020/#1025 raw-path-key class)** — `inFlightPipelines`, `lastAnalyzedStateByFile`, and `debouncedPipelines` keyed directly on `filePath`, which is only `path.resolve`d from raw agent-supplied tool input (never case-folded, never realpath'd). A divergent Windows spelling of the same file (case, or an unresolved absolute path an agent supplies verbatim) bypassed the concurrent-state dedupe and debounce coalescing, producing duplicate pipeline runs, doubled LSP dispatch, and split telemetry participant counts; `flushDebouncedToolResults(filePath)` silently missed the pending entry it was meant to flush. All three now wrap `PathKeyedMap` with `normalizeEphemeralMapKey` — the cheap slash-fold + win32-lowercase normalizer (no `realpathSync`), matching the state's ephemeral, turn-scoped, single-process lifetime per AGENTS.md's normalizer-choice guidance (`normalizeMapKey`'s realpath cost is for long-lived cross-call-site state, not this). `inFlightPipelines`'s composite `${filePath}:${stateHash}` key is now a `PathKeyedMap<Map<string, InFlightPipeline>>` — normalized path outer, raw stateHash inner — rather than folding both halves through one string normalizer, so a genuine same-path content change still gets a distinct entry while divergent spellings of the same state collapse to one. Every read/write/delete/iteration site in the file was audited; `flushDebouncedToolResults`'s iteration and `scheduleDebounced`'s coalesce path both continue to see the map's preserved display path. No other raw path-keyed map exists in the file. New tests (`tests/clients/runtime-tool-result-debounce.test.ts`) prove, fail-then-pass, that (1) two concurrent `tool_result`s for the same file under a divergent (win32-normalizer-driven) spelling collapse into one pipeline run, and (2) `flushDebouncedToolResults` addressed by a divergent spelling still hits the entry scheduled under the other spelling (asserted by elapsed time, not just call count, so a silent miss falling back to the natural debounce timer can't vacuously pass); a third test proves the composite key's stateHash axis still keeps same-path-different-content calls distinct.
|
|
1497
|
+
- **`pi --print --no-session` no longer hangs after `agent_settled` — uncleared race-loser timers (refs #1097, recurrence of #22's symptom via a different handle)** — a completed one-shot print-mode process kept running in the event loop instead of exiting. Root cause: `LSPService.getClientForFile` raced the client-wait against an inline `setTimeout(effectiveMaxWaitMs)` whose handle was never stored, so when the client resolved first (the common case) the losing timer stayed a **ref'd** pending timer for the full remaining wait budget. In a long-lived interactive session that is invisible (it fires later, resolves an orphan promise, is GC'd); in a `--print`/subagent process it keeps the loop alive for up to `effectiveMaxWaitMs` (15s in the report, minutes with a server whose `clientWaitTimeoutMs` is large) after the run settled — the `session_shutdown` handler runs and `resetLSPService` completes, but nothing owned or cancelled this timer. Proven via `process`-handle async-hooks tracing: the sole pi-lens-owned handle surviving into the hang was this timeout. The defect **shape** — *an operation raced/bounded by an independently-armed timer whose loser is never cancelled on the operation's resolution* — was swept repo-wide; two same-shape siblings were fixed in the same pass: the quiet-window heartbeat sample-timeout (`clients/quiet-window.ts`, 2s ref'd, kept a one-shot alive ~2s per settle) and the formatter timeout (`clients/format-service.ts`, 30s ref'd). All three now store the timer and `clearTimeout` it in a `finally`, matching the `withDeadline` helper contract the rest of the codebase already uses. Regression test (`tests/clients/lsp/client-wait-timer-cleanup.test.ts`) asserts no pending timer survives a fast client win — it fails on the pre-fix code (`expected 1 to be 0`). One further same-shape site — the cascade references race in `clients/dispatch/integration.ts` (750ms) — is tracked in #1109 for a follow-up as it is under concurrent modification.
|
|
1498
|
+
- **Two more uncleared-race-timeout members closed out (closes #1109, sweep follow-up to #1097/#1110)** — the two class members #1110's adversarial review flagged rather than fixed. (1) `computeCascadeForFile`'s symbol-level blast-radius upgrade (`clients/dispatch/integration.ts`) races `lspService.references()` against an inline `setTimeout(() => reject(...), 750)` per changed symbol (up to 3 per file); when `references()` wins — the common case — the losing timer was never stored, so it stayed a REF'D pending timer for the remaining budget. Same `finally`-clear contract as the #1097 fix: the handle is now stored in a per-call `let` and cleared once the race settles. (2) `safeSpawnAsync`'s non-Windows `killTree` else-branch (`clients/safe-spawn.ts`) arms a REF'D 1s SIGTERM→SIGKILL escalation timer that was neither cleared on child exit nor unref'd (the main per-spawn `timeoutId` alongside it was already correctly cleared on both the `close` and `error` paths). Bounded to 1s and only on kill paths, so it can't reproduce #1097's hang by itself, but it is the same defect shape and now clears alongside `timeoutId` in both handlers. (Review-round finding: the escalation ACTION itself has always been dead code — `!child.killed` is false after a successful SIGTERM *send*, so SIGKILL never fires; this change fixes only the timer-handle leak and preserves the window; the dead guard is tracked in #1114.) Regression tests prove both fail on pre-fix code: `tests/clients/cascade-compute.test.ts` (`vi.getTimerCount()` after a fast `references()` win) and the new `tests/clients/safe-spawn-kill-escalation-timer.test.ts`, which mocks `process.platform` and `node:child_process`'s `spawn` (mirroring `tests/clients/lsp/kill-process-tree.test.ts`'s established technique) so the non-Windows branch is exercised deterministically on any host OS, including Windows dev machines — no vacuous platform-gated pass. A re-run of #1110's repo-wide `setTimeout`/`setInterval`/`Promise.race` grep over every file changed since that sweep (#1103, #1110, #1111) found no new members of the class.
|
|
1499
|
+
- **`sg scan` exit-1-with-matches is no longer misclassified as a CLI failure (refs #1087)** — ast-grep's linter-style contract (verified first-hand against the bundled CLI: a `severity: error` rule that MATCHES exits **1** with valid JSON matches on stdout and stderr "Scan succeeded and found error level diagnostics"). `SgRunner.interpretScanResult()` / `exec()` only exempted `status===1` with *empty* output, so every real match under an error-severity rule was dropped as `failure: "cli-failure"` with `matches: []`. Now `status===1` + stdout that parses as valid JSON is treated as SUCCESS (matches parsed); stderr-with-unparseable-stdout stays a failure, and the historical status-1/no-output no-match convention is preserved. Invariant: **an exit code that means "scan succeeded with findings" must never be classified as a CLI failure.** This restores `ast_grep_search`/`pilens_ast_grep_search` raw error-severity rules, the `pilens_ast_grep_replace` preview/preCheck path, and `validateRule` for a matching error-severity rule.
|
|
1500
|
+
- **"Silencing is not fixing" now covers the whole suppression class, not just the policy member (refs #1087)** — the delta baseline in `dispatcher.ts` was only re-filtered through the project rule policy before `filterDelta`, while the live set was additionally filtered by LSP/docker-overlap suppression, inline `pi-lens-ignore` comments, and agent/user dispositions. A finding persistently dropped by any of those three sibling filters stayed in the baseline's `after` view forever, so it counted in `resolvedCount` on EVERY dispatch and inflated `trackAgentFixed` unboundedly. The output filter pipeline (overlap → inline → disposition → policy) is now a single closure applied identically to the live set AND the delta baseline, so a persistently-suppressed finding is absent from both sides and never oscillates into `fixed`. The stored baseline remains the unfiltered deduped set (a policy/suppression edit never resets the user-authored delta baseline).
|
|
1501
|
+
- **Inline `pi-lens-ignore` comment tokens are now normalized like the diagnostic id (refs #1087)** — the diagnostic side matched raw OR normalized, but the comment side stored only the raw token, so `// pi-lens-ignore: no-eval-js` failed to suppress a finding surfaced under the normalized `no-eval` even though the identical `disable: ["no-eval-js"]` config key worked. The comment token now also contributes its `normalizeRuleId` form, making the two suppression surfaces symmetric.
|
|
1502
|
+
- **`"disable": []` / `"select": []` no longer warn spuriously (refs #1087)** — an explicitly empty rule-policy list is a well-formed no-op, not an error; it is now dropped silently. A NON-empty array whose entries are all blank/non-string is still a real authoring mistake and still warns ("must contain at least one non-empty string").
|
|
1503
|
+
- **Test-runner subcommands no longer lost during binary resolution (closes #1098)** — `TestRunnerClient`'s `resolveExec` unconditionally dropped `args()[0]` whenever local/global binary resolution succeeded, an npx-wrapper-convention assumption (arg 0 names the binary, e.g. `npx vitest run …`) that only holds for wrapper-style runners. For direct runners whose args lead with a real subcommand it silently ate that subcommand instead — `cargo test --no-fail-fast` resolved to the argv-invalid `cargo --no-fail-fast` (a clap usage error), likewise for go/dotnet/maven/mix, misreported at turn-end as a false "1/1 failed". The new `stripWrapperArgs` helper strips a leading arg ONLY when it actually names the resolved binary (`[binName, ...]`, or the pytest-style `["-m", binName, ...]` pair) and leaves every other argv untouched, applied at both the local-bin and global-bin resolution sites. Also fixes the same defect's inverse: `rspec`'s config declares `command: "bundle"` with `args: ["exec", "rspec", file]`, but binName resolution defaulted to the runner key `"rspec"` instead of the real binary `"bundle"`; it now declares `binName: "bundle"` so local/global resolution looks up the right binary and `exec rspec <file>` reaches `bundle` intact. A generic regression matrix (`tests/clients/test-runner-client.test.ts`) iterates the exported `RUNNERS` table (not a hand-copied runner list) across local-bin/global-bin/fallback resolution paths, so a future runner is automatically covered. A repo-wide sweep for the same defect shape (positional argv stripping tied to binary resolution, and command/binName-resolution-name mismatches) found no other live instance — every other `findGlobalBinary` caller (biome-client, formatters, ast-grep runners, LSP launch) builds its argv as fresh literals per resolution branch rather than slicing a shared generator, so none share this defect's precondition. Three additional command/binName-mismatch members exist but are unreachable in practice (their binaries never land in the npm-family bin dirs findGlobalBinary probes) and now resolve MORE correctly if they ever did: gradle, maven, and minitest (command "ruby" vs default binName "minitest"); pytest's resolved-path argv was also silently broken pre-fix (slice(1) ate "-m") and is now correct.
|
|
1504
|
+
- **Call-graph persisted-cache validator and canonical-freshness invariant now have real test coverage (refs #1089)** — two `call-graph.test.ts` "rejects…" tests hand-wrote `version: 4`, identity-less fixtures, so `loadCallGraph` returned `undefined` at the version/identity gate before the ~200-line semantic validator (`validatePersistedCallGraph`) ever ran; rewritten as `version: 5` fixtures with a valid canonical identity, each now isolating and proving out the ONE validator check it claims to cover (the inDegree/adjacency cross-check and the `complete && unsupportedEvidence > 0` honesty check). Also added the first tests anywhere that pass `expectedIdentity` to `loadCallGraph`, covering the freshness invariant edab5d5d introduced: a mismatched `reviewGraphVersion` or `reviewGraphSignature` rejects the cache (the #210/#1020 stale-replay class), while a matching identity loads normally.
|
|
1505
|
+
- **`module_report`'s `callGraph: true` surface gains coverage for the `file-cap` and `identity-missing` unavailability reasons and the `provenance.callGraph` field (refs #1089; the Java try-with-resources regex and grammar co-load items remain open there)** — `cached-call-graph` / `unavailable:file-cap` / `none` / omitted-when-not-requested are now all asserted, so the tool description's own honesty contract ("unavailable cache state is never reported as zero calls") is enforced by tests, not just documented.
|
|
1506
|
+
- **Review-graph cache-identity guard now detects a stale graph instance instead of a vacuous schema-tag compare (refs #1088; the `parseSymbolKey` LSP-fallback-kind item remains open there)** — `getReviewGraphCacheIdentity` compared `version`, the constant schema tag (e.g. `"v8"`, identical for every live graph), so it could never tell a caller's graph reference apart from a newer one that had since replaced it in the workspace cache — the concrete race is a session call-graph task computing a projection from an older `graph` instance while a concurrent cascade build races ahead and replaces `_workspaceGraphCache` before the identity lookup runs, letting a stale call-graph projection get persisted under a fresh-looking signature. Now compares the workspace-cache ENTRY's `buildGeneration` stamp (#459) — not the stored graph object's, which the reuse paths deliberately leave unstamped on their `cloneGraph` copies — so identity resolves for the graph a drift-reuse or disk-hit build just returned, while a stale or unstamped instance is still rejected.
|
|
1507
|
+
- **Nested-link diagnostics no longer flag ordinary JSX wrappers (closes #1076)** — report only the outermost anchor in a directly or deeply nested chain, while allowing single and sibling links inside non-anchor containers across both ast-grep and Tree-Sitter dispatch paths.
|
|
1508
|
+
- **LSP workspace edits apply safely in declared order (refs #1053)** — preserve text/resource operation ordering (including directory subtrees), stabilize same-position inserts, collapse identical replacement duplicates, prevalidate every text batch before writing, and close an open old URI before rename notifications.
|
|
1509
|
+
- **LSP workspace-edit apply paths: reversed same-position inserts, win32 casing destruction, versioned-edit rejection, out-of-range clamping (closes #1085; P3-1 folded in, P3-2/3/4/5/6/7 deferred on #1085)** — four empirically-reproduced defects in the `clients/lsp/edits.ts` write pipeline, each with a fail-then-pass regression test.
|
|
1510
|
+
1. **Same-position inserts applied REVERSED through `applyWorkspaceEdit` (the #1066 invariant).** The tie-break sort was correct only for exactly one pass, but the apply pipeline sorted an even number of times (and differed by server position-encoding), so `["A","B"]` inserted at one position produced `"aBAbc"` instead of `"aABbc"` for UTF-16 servers (typescript-language-server, pyright — the majority). The application-ordering sort is now performed **exactly once**, at the single string-write site; `normalizeTextEditsForContent` returns validated edits in original array order (never pre-sorted), so the same-position order can no longer flip on an even number of passes. Invariant: same-position inserts apply in array order through every entry path (direct `applyTextEditsToString`, `applyWorkspaceEdit` `changes`, `documentChanges`) under all three position encodings — a full encoding×entry-path matrix now asserts it.
|
|
1511
|
+
2. **Windows case-preservation destroyed on create/rename; case-only renames rejected (invisible to Linux CI).** Disk mutations routed the URI through `uriToPath`→`normalizeFilePath`, which lowercases a nonexistent path tail on win32 — so `create NewFile.txt` wrote `newfile.txt`, `rename → MixedCase.txt` wrote `mixedcase.txt`, and a legitimate case-only rename (`foo.txt → Foo.txt`) was rejected as "source and destination must differ". Confinement/validation still run on normalized paths, but the actual `fs.writeFile`/`fs.rename`/`fs.readFile`/`fs.rm` calls now target the URI's decoded path via the new `uriToDiskPath`. The preflight "destination exists" check decides a case-only (or otherwise-aliased) rename by on-disk **FS identity** (matching `dev` and matching **nonzero** `ino` via `lstat({ bigint: true })`), not a platform-keyed path fold — so it works on ANY case-insensitive FS (win32 **and** macOS APFS/HFS+, per the #1024 "probe the FS, don't branch on platform" lesson) and closes the inverse edge where a case-sensitive FS holding both spellings would otherwise silently clobber. The nonzero-`ino` requirement (and BigInt comparison) is fail-closed: on ino-less filesystems that report `ino: 0` (FAT32/exFAT, some SMB redirectors) the identity check never matches, so the destination-exists check runs and a rename can only error, never silently clobber — a genuine case-only rename there needs an explicit `overwrite`. Tests are FS-probe-guarded (assert casing preservation on a case-insensitive FS, success on a case-sensitive FS) plus a direct unit test of the `ino: 0` guard — never a vacuous pass.
|
|
1512
|
+
3. **Versioned `TextDocumentEdit`s always rejected on the tool apply paths.** Rename `apply:true` (`tools/lsp-navigation.ts`) and code-action autofix (`clients/actionable-warnings.ts`) never pass `documentVersions`, while `client.rename`/`codeAction` validated versions against the live map and then PRESERVED them — so a numeric `textDocument.version` reached an apply site with no map and failed 100% for version-stamping servers (gopls). `normalizeClientWorkspaceEdit` now validates the version against the live map (unchanged) and then STRIPS it (spec `null` = don't check), so the tool paths succeed. The server-initiated `workspace/applyEdit` handler does NOT route through here (it applies `params.edit` with the live `documentVersions`), so its real version check is fully intact. The existing rejection test's key-mismatch vacuity (raw-path key vs `normalizeMapKey` lookup — the #1106 class) is fixed and a positive success-path test added.
|
|
1513
|
+
4. **Out-of-range positions threw instead of clamping (LSP 3.17 regression).** A line past EOF now clamps to end-of-document and a character past line end clamps to line length, so the `(0,0)-(9999,0)` whole-document-replace sentinel idiom works again; malformed shapes (negative/non-integer, `start > end`, mid-codepoint boundary) still throw. The character clamp is **CRLF-safe**: because `lineTextAt` keeps a trailing `\r`, the clamp targets the length BEFORE that `\r`, so a char-past-EOL insert lands at the CRLF boundary and the whole-line sentinel replace preserves the `\r\n` (it does not eat the `\r` or leave a stray one mid-line).
|
|
1514
|
+
P3-1 (an insert listed after a replace starting at the same position was spuriously rejected as overlapping — LSP-legal, VSCode applies it) is fixed as a byproduct of the sort rework via an end-descending tie-break, making the result independent of listing order. The general CRLF-boundary hazard (P3-5) and P3-2/3/4/6/7 remain deferred on #1085.
|
|
1515
|
+
|
|
1516
|
+
- **Read-guard did-you-mean no longer quotes NFKC-normalized text as "current lines"** (#1050) — the `Did you mean one of these current lines?` block (#505) rendered its rows from the normalized match space instead of the file's real characters, so for CJK markdown it folded `:`→`:`, `,`→`,`, `;`→`;`, `()`→`()` and `—`→`-` and presented the result as ground truth about file content. That inverted #505's own stated invariant ("only for COMPARISON, never for the written content"). The damage compounded past a bad suggestion: an agent that copied the half-width row into `oldText` got past the guard, then the host's `fuzzyFindText` missed exact match, fell into fuzzy space, and `applyReplacementsPreservingUnchangedLines` rewrote the *touched* line from the normalized base — silently replacing full-width punctuation with ASCII while reporting success (untouched lines are safe per earendil-works/pi#5899; touched-line normalization is wontfix per earendil-works/pi#3554, so the suggestion is the only place this can be fixed). The failure mode is self-reinforcing: the agent re-reads the file next turn, sees changed punctuation, and concludes a markdown formatter is corrupting the file — a wrong root cause it then acts on. `findSimilarLines` now returns only `{ line, score }` and `formatSimilarLines` renders from the raw file, making it structurally impossible to leak normalized text into an error. The raw view is `normalizeToLF(stripBom(raw).text)` — the same STRUCTURAL normalization the match space applies, so line numbers cross-index 1:1 even on lone-CR or BOM files, but none of the CHARACTER folding, so full-width punctuation, smart quotes, and NBSP survive. A naive `\\r\\n`-only fold desyncs on lone-CR files and is covered by a regression test.
|
|
1517
|
+
- **Experimental git guard correctness (refs #1063)** — `--lens-guard` now detects real commit/push invocations through the shared shell tokenizer, gates only structured blocking findings (including blocking test failures), and rejects stale, malformed, cross-session, or sequence-mismatched state conservatively. Aggregate per-file blocker state, clean-turn invalidation, and bounded decision telemetry prevent advisory findings or a later clean file from silently clearing an unresolved blocker.
|
|
1518
|
+
- **Stale cross-file footer diagnostics now clear (refs #1093, refs #1092)** — a widget finding in file A that was *caused* by file B (a missing declaration, a changed export) is fixed by editing B, but every widget staleness gate compares A's OWN mtime to the record timestamp — and A's mtime never advances, so the resolved finding rendered forever (footer, `lens_diagnostics` replay, resume, fork adoption). Two minimal fixes close the reported shape. (1) The per-edit cascade already re-checks every edited file's dependents (`clients/dispatch/integration.ts`) but threw the result away as "display-only"; each **confirmed** neighbor result (a valid passive snapshot or a completed, non-inconclusive active touch — #571 semantics) now reconciles into widget state, **including the confirmed-clean `[]` case** that clears A's stale entry, keyed by the primary edit's `writeSeq` so a genuinely newer per-edit write still wins the `WriteOrderingGuard`. A touch that resolves `[]` while flagged `inconclusive` (its wait lapsed) is never treated as confirmed-clean — it neither reconciles nor seeds the recently-clean cache, so it cannot wipe a live finding (#533). Because the cascade only re-checks the *language server* and only for *errors*, the reconcile **merges** rather than replaces: it swaps out just the neighbor's existing LSP-error entries and preserves live biome/ruff/ast-grep findings and LSP *warnings* it never re-examined. Auxiliary-LSP findings (opengrep/ast-grep/zizmor/typos, identified by `findAuxiliaryProfileForSource`) are excluded from what the cascade writes — unlike the per-edit/scan writers it does not run `retagAuxiliaryDiagnostics`, so writing them would double-count the neighbor's own aux entry and escalate an advisory finding into a blocking `tool:"lsp"` error, bypassing aux policy. Tier-3-silent/recently-clean/within-turn-cache/rejected-touch results are never written. (2) `reconcileScanDiagnostics`/`recordDiagnostics` accept an observation timestamp; the `lsp_diagnostics` workspace-cache-hit branch (plus `lens_diagnostics mode=full`'s cache-served results) stamp `touchedAt` with when the diagnostics were *observed* (the cache entry's `scannedAt`), and the cascade's passive-snapshot reconcile stamps the snapshot's own publish time (`entry.ts`, up to `CASCADE_TTL_MS` old) — not `Date.now()` — so replaying an aging cached/snapshot view no longer keeps re-arming the mtime gate. The structural seq-stamping redesign (marking a changed file's reverse-dep set unverified up front) remains a follow-up under #1093.
|
|
1519
|
+
- **Class sweep: test fixtures hand-writing persisted-format version literals no longer go vacuous on version bumps (closes #1106; class generalized from #1089/#1082, fresh-risk flagged from #1096)** — a fixture that hand-writes a persisted-format `version:`/schema-tag literal a runtime gate checks BEFORE the code-under-test runs silently orphans on a version bump: the gate now rejects the (unrelated-stale) fixture and the test stays green for the wrong reason. Repo-wide sweep of `tests/` for hardcoded `version:` numerics and `"v<digit>"` schema tags across every known persisted-cache surface. Converted to import the runtime constant (exporting it where it was previously a private literal): `clients/call-graph.ts`'s `CACHE_VERSION` (was unexported — `tests/clients/call-graph.test.ts`'s `validPersistedCallGraph()` and `tests/clients/module-report.test.ts`'s call-graph-cache fixture both now import it instead of hand-writing `version: 5`), `clients/read-guard.ts`'s `READ_GUARD_STATE_VERSION`, `clients/widget-state.ts`'s `WIDGET_STATE_VERSION`, `clients/review-graph/builder.ts`'s `REVIEW_GRAPH_VERSION`, and a newly-extracted `clients/word-index.ts` `WORD_INDEX_FORMAT_VERSION` (previously an inline `2` duplicated at both the write and gate call sites). `tests/clients/project-diagnostics/scanner-wasm-abort.test.ts`'s `priorSnapshot()` fixture now imports the already-exported `PROJECT_DIAGNOSTICS_CACHE_VERSION` instead of hardcoding `2`. Every test that deliberately persists an OLD version to exercise the rejection path itself (call-graph v4, project-diagnostics v0 ×2, word-index v1, review-graph v3/v4) is pinned per the #1082 pattern: an adjacent `expect(CURRENT_CONSTANT).not.toBe(oldLiteral)` so a future bump onto that exact old value fails loudly instead of silently un-exercising the rejection path. Spot-proved non-vacuous for the three highest-traffic surfaces (call-graph, project-diagnostics, and — via the existing #1089 validator-disable proof already recorded in `call-graph.test.ts` — the semantic validator itself) by locally bumping the runtime constant, reverting the fixture to its old hardcoded literal, and confirming a loud failure (not a silent pass) before restoring both. Surfaces confirmed already immune (import the constant / derive-then-override a live value, so version drift cannot silently orphan them): `workspace-diagnostics-cache` (`WORKSPACE_DIAGNOSTICS_CACHE_VERSION` + self-relative `+1` mismatch), `warm-attach`/`mcp/ipc` (`WARM_DIAGNOSTICS_SCHEMA_VERSION`), `rule-cache` (`CACHE_VERSION`), `project-snapshot` (`PROJECT_SNAPSHOT_VERSION` for valid fixtures; deliberate-bad-version sentinels use `999`, permanently distant from the real 1-3 range). Confirmed NOT members of this class (noted for the next sweep so they aren't re-litigated): LSP wire-protocol `textDocument.version` fields (staleness/ordering, unrelated to a persisted-cache gate), assertions on production-EMITTED event/latency-log payloads (`lens-events`, `index-integration`, `cache-observability` — a bump fails these loudly, not vacuously, since they assert output rather than feed a hand-written fixture through a gate), in-memory-only `ReviewGraph` object literals in `transitive-impact.test.ts`/`lens-map.test.ts` (never round-trip through the persisted-JSON version gate), and `tests/tools/lens-diagnostics*.test.ts` (the entire `project-diagnostics/cache.js` module is `vi.mock`'d, so the real gate never runs). The installer's `probe-cache.json` has no version field/schema gate at all (not a member). `session-state-store.ts`'s own `STATE_VERSION` gate has no dedicated fixture test at all — a coverage gap, not a vacuous-fixture bug, left as a follow-up rather than expanded into new-test-authoring scope. SHAPE-gated fixtures (a loader that rejects on missing/malformed fields before the code-under-test) were also noted where encountered — `project-snapshot`'s "missing seq" meta test, `read-guard`'s malformed-`reads`-shape tests (now additionally non-vacuous on the version axis via the `READ_GUARD_STATE_VERSION` conversion above), and `word-index`'s malformed-input test — all correctly exercise their shape gate today.
|
|
1520
|
+
- **`no-bare-except` no longer flags subscripted exception specs (closes [#1244](https://github.com/apmantza/pi-lens/issues/1244))** — both detection surfaces (the ast-grep `no-bare-except` rule and the tree-sitter `bare-except` rule's `bare_except_only` post-filter) false-fired on valid subscript exception specs like `except dict[str, int]:` and PEP 654's canonical `except BaseExceptionGroup[TypeError, ValueError]:`. The #1031 sweep fixed the dotted-attribute sibling and stopped one member short: subscript was never in either surface's spec set, so neither surface suppressed it. The ast-grep `not: any:` and the tree-sitter filter's child-kind set both now accept `subscript`, keeping the two surfaces' shape spaces in lockstep (the same parity the #1026→#1031→#1243 arc enforced). Valid-fixture regression cases for both subscript forms fail against the pre-fix rule on both engines (2/2 false fires on the ast-grep CLI; 2/2 on the tree-sitter filter, confirmed by reverting the filter and re-running).
|
|
1521
|
+
|
|
1522
|
+
- **Spawn failures now distinguish missing tools from invalid working directories (closes #1214)** — Auto-install and reinstall paths run only when the executable itself is missing, preventing futile reinstall loops for cwd and permission failures.
|
|
1523
|
+
|
|
1524
|
+
- **Per-issue close-keyword CI validation** — Rejects comma-separated GitHub close lists and verifies merged PR close targets.
|
|
1525
|
+
|
|
1526
|
+
- **Recover stale git-guard blocker records (closes #1084)** — Clear persisted blocker content only when complete, well-typed blocker-file provenance proves the last represented blocker was revalidated clean; incomplete or forged provenance remains blocked.
|
|
1527
|
+
|
|
1528
|
+
- **Cascade module source-file walk is memoized on the existing freshness seams (refs [#1137](https://github.com/apmantza/pi-lens/issues/1137))** — `getModuleSourceFiles` (the #1318 slice-2 remnant) ran a recursive `readdirSync` walk per downstream module on every per-edit cascade in monorepos; it now memoizes per module root, revalidating on the visited directories' `mtimeMs` stamps plus ignore-matcher object identity (so `.gitignore`/`.pi-lens.json` edits re-walk), and clears with the module-graph cache.
|
|
1529
|
+
|
|
1530
|
+
- **Review-graph supersession cleanup is deterministic (refs #1318)** -- a superseded generation's staged write is reaped synchronously (and fault-tolerantly) before completion becomes observable, so CI waiters can no longer observe a leftover stage file; the supersession lock test moved to the quiet timing-sensitive phase.
|
|
1531
|
+
|
|
1532
|
+
- **In-pi extension logging no longer corrupts the pi TUI layout (closes #1333)** — pi owns the terminal, so the 26 ungated and 20 verbose-gated `console.error`/`console.warn` sites under `clients/` were landing raw bytes mid-frame and desyncing pi's screen model. Every site now writes to a `createNdjsonLogger` sink (the subsystem's own `tree-sitter.log`/`review-graph.log`/`latency.log` where one exists, otherwise the new `~/.pi-lens/extension.log`), user-facing degradations (invalid config, offline grammar fetch, WASM abort) additionally surface through `ctx.ui.notify`, and `index.ts` installs a defensive console reroute so a transitively loaded dependency cannot write to the frame either.
|
|
1533
|
+
|
|
1534
|
+
- **npx formatter fallback honors style-preservation gating (closes #1345)** — Files that must be skipped by `SKIP_FORMATTING` no longer invoke the static Biome or Prettier `npx` fallback when a primary formatter command is unavailable.
|
|
1535
|
+
|
|
1536
|
+
- **Formatter failures now appear in the diagnostics widget (closes #1346)** — Failed formatter runs render with a distinct error indication, remain deduplicated per formatter and file, and disappear after a successful retry.
|
|
1537
|
+
|
|
1538
|
+
- **Instance-registry liveness tests no longer depend on arbitrary runner PIDs (refs #1352)** — synthetic dead PIDs are classified through deterministic test seams, preserving coverage of footprint exclusion, registry pruning, and vanished-instance wiring without relying on the CI process table.
|
|
1539
|
+
|
|
1540
|
+
- **Trust-gated observability (refs #1334, #1358)** — LSP install refusals now log with warn-once deduplication, tree-sitter trust-blocked grammar toasts are deduplicated per grammar, trust transitions are recorded, and `~/.pi-lens/extension.log` is documented.
|
|
1541
|
+
|
|
1542
|
+
- **BLIND WRITE no longer fires on self-authored files (closes #1364)** — A `write`/`edit` of the same normalized path earlier in the blind-write window now counts as file knowledge, so the common author-then-iterate loop (create a file with `write`, refine it with successive `edit` calls) stays quiet. A genuinely stale edit of a file not touched in the window still warns, and the two-write threshold, 5-call window, and thrashing detection are unchanged. Thanks to @snrogers for the field report and the fix.
|
|
1543
|
+
|
|
1544
|
+
- **Edit counts now match normalized file paths (closes #1369)** — `AgentBehaviorClient.getEditCount` now uses the same path normalization as edit recording, so mixed-separator and case-variant lookups return the recorded count.
|
|
1545
|
+
|
|
1546
|
+
- **Bound warm word-index memory (closes #1370, refs #1332)** — idle/LRU per-root eviction now releases inactive indexes safely, while persisted snapshot postings no longer remain duplicated in long-lived in-memory caches.
|
|
1547
|
+
|
|
1548
|
+
- **Latency-log hygiene (closes #1371)** — Ast-grep unsupported-language telemetry now dedupes per language for each session and emits only a bounded rule-ID sample; the log analyzer excludes synthetic temp/scratchpad/heap-corpus rows by default, supports repeatable `--exclude` globs, and reports excluded-row counts.
|
|
1549
|
+
|
|
1550
|
+
- **Review-graph size gates stop at the cap sentinel (closes #1372)** — the shared cooperative source walk now stops at `maxFileCount + 1`, reports the partial count honestly as “more than N files,” and emits a distinct near-miss telemetry event within 5% of the cap.
|
|
1551
|
+
|
|
1552
|
+
- **Keep LSP roots inside the session project and coalesce nested marker clients (closes #1373, refs #1328)** — Clamp marker-selected roots at the declared cwd, and reuse same-server ancestor clients for config-only nested roots while preserving real manifest/lockfile sub-projects.
|
|
1553
|
+
|
|
1554
|
+
- **Telemetry phase pairing and attribution (closes #1374)** — Deferred formatting now emits counted terminal phases; LSP warm-up and unavailable events reconcile correctly, diagnostic timeouts identify their server, and startup timing preserves the console guard's import-time position.
|
|
1555
|
+
|
|
1556
|
+
- **Re-assert widget mounts after host UI replacement (closes #1381)** — The diagnostics widget remounts on the live UI at turn start when needed, while preserving mode and user visibility gates; unsupported widget hosts now emit a log-once diagnostic.
|
|
1557
|
+
|
|
1558
|
+
- **Widget and inter-extension bus updates recover after session replacement (closes #1383)** — every `session_start`, including the #473 guarded in-process subagent path, now reclaims activation-scoped bus, notifier, and widget-render wiring before returning. Stale bus failures are logged and ledgered once per failure occurrence, with a successful publish re-arming observability for a later channel death.
|
|
1559
|
+
|
|
1560
|
+
- **Deferred formatting uses bounded concurrency (closes #1387)** — the `agent_end` drain runs at most three formatter subprocesses in flight, applies results and synchronous bookkeeping in admission order with `setImmediate` yields between files, preserves per-file failure isolation, and requeues claimed records that were not started when the ambient turn aborts.
|
|
1561
|
+
|
|
1562
|
+
- **Hardened codebase-model cache identity (closes #1388)** — persisted models now carry a version and canonical review-graph identity, stale caches invalidate on mismatch, and model file selection uses shared role and artifact filtering.
|
|
1563
|
+
|
|
1564
|
+
- **Prevented consistency drift in language fixtures and extension handling (refs #1388)** — call-graph fixtures now cover every symbol-query language, JS/TS facts share the canonical extension policy, and bash file-access tracking derives source extensions from `KIND_EXTENSIONS`.
|
|
1565
|
+
|
|
1566
|
+
- **Canonicalized LSP edit merge buckets and call-graph path comparisons (refs #1388)** — equivalent URI spellings now share conflict/deduplication buckets while preserving the first display URI, and call-graph symbol/reference indexes normalize file keys consistently.
|
|
1567
|
+
|
|
1568
|
+
- **Bound and idle-evict review-graph workspaces and authoritative project snapshots (refs #1389).**
|
|
1569
|
+
|
|
1570
|
+
- Bound and evict rebuildable Tier 2 reverse-dependency, tree-sitter query, and workspace-topology caches while retaining unconsumed ReadGuard records until their edit or session end. ReadGuard now applies a high sanity cap with oldest-to-re-read eviction for read-only sessions (refs #1389; widget-state and Tier 3 remain deferred).
|
|
1571
|
+
|
|
1572
|
+
- **Bound Tier-3 process caches, prune expired negative LSP entries, decay widget/agent state, and close the cache audit (closes #1389).**
|
|
1573
|
+
|
|
1574
|
+
- **Scope session-start smell-tail diagnostics to the current session boundary (closes #1398)** — historical failures in the bounded bus-events and latency log tails are no longer reported as current; rows must have a parseable UTC timestamp at or after session start.
|
|
1575
|
+
|
|
1576
|
+
- **Unconfigured Python files are formatted again (refs #1144)** — `ruff format` rejects the `--indent-style`/`--indent-width` flags the style-preserving defaults were passing, exiting 2 without touching the file; because ruff was not treated as a strict-exit formatter, every unconfigured Python file silently reported "already formatted" and was never reformatted. Style is now pinned through ruff's inline TOML overrides, and a nonzero ruff exit is surfaced as a formatting failure instead of a clean no-op.
|
|
1577
|
+
- **Ast-grep console-rule exclusions (closes [#965](https://github.com/apmantza/pi-lens/issues/965))** — stop flagging intentional console usage in CLI scripts and logger implementations.
|
|
1578
|
+
- **Nullish equality exemptions (closes [#966](https://github.com/apmantza/pi-lens/issues/966))** — allow intentional `== null` / `!= null` checks while retaining other strict-equality findings.
|
|
1579
|
+
- **Security-pin dependency handling (closes [#968](https://github.com/apmantza/pi-lens/issues/968))** — stop Knip from flagging dependencies used only through override/resolution security pins.
|
|
1580
|
+
- **Unsafe-boundary fallback detection (closes [#969](https://github.com/apmantza/pi-lens/issues/969))** — recognize catch blocks that recover through fallback assignments.
|
|
1581
|
+
- **Promise-returning async callbacks (closes [#970](https://github.com/apmantza/pi-lens/issues/970))** — avoid async-noise findings when an explicit Promise return type is required.
|
|
1582
|
+
- **Native zizmor suppressions (closes [#971](https://github.com/apmantza/pi-lens/issues/971))** — honor inline `zizmor: ignore[...]` directives.
|
|
1583
|
+
- **Named URL constants (closes [#964](https://github.com/apmantza/pi-lens/issues/964))** — avoid hardcoded-URL findings for deliberate `SCREAMING_SNAKE_CASE` endpoint constants.
|
|
1584
|
+
- **Opengrep LSP compatibility (closes [#974](https://github.com/apmantza/pi-lens/issues/974))** — stop advertising unused work-done-progress support that could trigger client crash loops.
|
|
1585
|
+
- **Workspace configuration responses (closes [#983](https://github.com/apmantza/pi-lens/issues/983))** — return one resolved value for each requested configuration section.
|
|
1586
|
+
- **Same-file diagnostic ordering (closes [#1198](https://github.com/apmantza/pi-lens/issues/1198))** — prevent late diagnostic work from restoring stale pending results.
|
|
1587
|
+
- **Bounded installer retries (refs [#1222](https://github.com/apmantza/pi-lens/issues/1222), [#1223](https://github.com/apmantza/pi-lens/issues/1223))** — cap failed install retries and preserve probe-cache updates across concurrent writes.
|
|
1588
|
+
- **Stabilize LSP workspace-edit occupancy (closes [#1091](https://github.com/apmantza/pi-lens/issues/1091))** — keep workspace edits consistent under concurrent occupancy.
|
|
1589
|
+
### Security
|
|
1590
|
+
|
|
1591
|
+
- **Git guard command-injection hardening (refs #1084, PR #1395)** — canonicalizes shell IFS parameter-expansion separators once, quote-aware, before verb classification and treats any non-leading guarded `git` invocation as suspicious, including arbitrary unknown wrappers and run flags; literal text consumers remain non-blocking.
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
- **`toPosix()` / `splitPathSegments()` path primitives ([#1193](https://github.com/apmantza/pi-lens/issues/1193))** — the sanctioned backslash→forward-slash fold and shape-safe segment split, the single canonical form of the `p.replace(/\\/g, "/")` / `p.split(/[\\/]+/)` idioms otherwise hand-rolled ~138× across the codebase. Pure separator normalization — no resolve/case/realpath (use `normalizeFilePath`/`normalizeMapKey`/`normalizeEphemeralMapKey` when a canonical key is needed). First step of the OS-agnostic transformation-layer consolidation: funnelling the scattered transform makes a shape-2 lint/ast-grep rule possible for the first time (today a bare inline `.replace(/\\/g, "/")` is byte-identical to a sanctioned use; once everything routes through `toPosix`, an un-migrated inline `.replace` becomes detectable — #1158). Adds the primitives + tests only; migrating the existing call sites is a tracked follow-up under #1193.
|
|
1595
|
+
|
|
1596
|
+
- **On-demand heap snapshots for retainer attribution ([#1126](https://github.com/apmantza/pi-lens/issues/1126))** — `PI_LENS_DEBUG_HEAP=1` makes `/lens-health` also write a V8 `.heapsnapshot` to `~/.pi-lens/` (plus a breadcrumb line in `heap-snapshots.log`), so the "which objects retain the bytes" follow-up to #1123's `memory_sample` trajectory is answerable on a live >1 GB instance without a fresh ad-hoc expedition. The flag is read once at startup and the writer mirrors `clients/debug-handles.ts`: zero cost + no file when unset, and the (synchronous, multi-second) snapshot is only ever triggered from the operator-invoked diagnostics command — never a hot path or timer. Snapshot files are pruned to the newest `SNAPSHOT_RETENTION` (3) after each write, bounding the growing on-disk axis (AGENTS.md shape 9). Auto-capture on an RSS threshold is a deliberately-deferred follow-up (it would reintroduce the pause onto an automatic path).
|
|
1597
|
+
|
|
1598
|
+
- **Guard hardening (refs #1084)** - git commit/push detection now covers normalized wrapper launchers (including path- and PATHEXT-qualified names), shell-escaped verbs, keyword, combined-flag, and continuation forms; shell substitutions are treated as execution even inside text consumers, while literal text remains allowed; bash read and ownership grants use quote-aware tokenization and are recorded only after successful tool results. Stale-record recovery remains deferred.
|
|
1599
|
+
|
|
1600
|
+
- **Honor the pi host's project-trust decision before installing or spawning anything (refs #1334)** — pi-lens now reads `ctx.isProjectTrusted()` at every `session_start`. When the host says a project is NOT trusted, tool auto-install degrades to discovery-only (nothing is downloaded or executed) and LSP servers are not spawned; in-process analysis, caches and tree-sitter continue unchanged. Hosts that expose no trust surface behave exactly as before.
|
|
1601
|
+
|
|
1602
|
+
### Contributors
|
|
1603
|
+
|
|
1604
|
+
- Apostolos Mantzaris (@apmantza)
|
|
1605
|
+
- Nathan Cooke (@3choBoomer)
|
|
1606
|
+
- Eli Stark (@white-hat)
|
|
1607
|
+
- Mark Faga (@mjfaga)
|
|
1608
|
+
- Marvin Aziz (@marvtub)
|
|
1609
|
+
- Dominic (@aeturnal)
|
|
1610
|
+
- Kenneth McCormick
|
|
1611
|
+
- jiangmt
|
|
1612
|
+
|
|
13
1613
|
## [3.8.74] - 2026-08-01
|
|
14
1614
|
|
|
15
1615
|
### Added
|
|
@@ -17,1114 +1617,1093 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
17
1617
|
- **🙏 Huge thanks to @AngriestBird (Kenny McCormick), this round's standout community contributor** — five merged PRs landed this release: a single source of truth for CLI flags (#1011), secret redaction at the log-write boundary (#954), new slash commands (#906), tree-cache performance improvements (#877), and a test harness for rule-dispatch behavior (#871). Sustained, high-quality work spanning perf, security, config, and test infrastructure — thank you.
|
|
18
1618
|
|
|
19
1619
|
- **Prompt-cache observability (refs #1018, closes #1018)** — two provider-independent
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1620
|
+
signals now land in `~/.pi-lens/latency.log` as `type: "phase"` records.
|
|
1621
|
+
(1) Response-side: a defensively feature-detected `message_end` subscription
|
|
1622
|
+
(clients/agent-nudge.ts pattern — guarded, never throws on older hosts) logs one
|
|
1623
|
+
`cache_usage` record per assistant message that carries a `usage`, with
|
|
1624
|
+
`metadata: { provider, model, cacheRead, cacheWrite, input, output, cost }`
|
|
1625
|
+
(provider/model read straight off the assistant message; `cost` is the total).
|
|
1626
|
+
Messages with no usage (or non-assistant messages) are skipped rather than
|
|
1627
|
+
logged as zeros. (2) Request-side: the existing `context` handler now hashes
|
|
1628
|
+
`messages[0]` on every call and logs a `cache_prefix_break` record
|
|
1629
|
+
(`metadata: { turnIndex, previousHash, currentHash, sessionId, sessionRole }`,
|
|
1630
|
+
plus a baseline record on first observation) whenever that hash changes
|
|
1631
|
+
turn-over-turn — a regression guard that flags anything (pi-lens or otherwise)
|
|
1632
|
+
breaking the byte-stable prefix #1016 established. The baseline is keyed by the
|
|
1633
|
+
stable per-session id (`ctx.sessionManager.getSessionId()`) in a small bounded
|
|
1634
|
+
LRU, so a resume/reload keeps comparing against its own baseline while a
|
|
1635
|
+
new/fork or a concurrent in-process subagent (#473) gets an independent baseline
|
|
1636
|
+
instead of a spurious cross-session break; each record is tagged with the
|
|
1637
|
+
read-only #473 `sessionRole` classification, and a primary `session_shutdown`
|
|
1638
|
+
drops the ended session's entry. The hash is pure observation: it never changes
|
|
1639
|
+
the handler's injection behavior or return value, and runs even on non-injecting
|
|
1640
|
+
turns. New `clients/cache-observability.ts`; no new dependencies.
|
|
41
1641
|
|
|
42
1642
|
- **Word-index build/refresh/persist outcomes are now durably observable**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- **Closed four observability gaps in recently-changed typos-config and
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
longer silent.
|
|
1643
|
+
(refs #958, #926, #533) — every word-index signal previously rode solely on
|
|
1644
|
+
the optional `dbg` callback, a documented no-op in the MCP host where
|
|
1645
|
+
`symbol_search` reads the index. The structured logger (`~/.pi-lens/
|
|
1646
|
+
word-index.log`, shared `createNdjsonLogger` infra) now records, independent
|
|
1647
|
+
of `dbg`: the full-rebuild-vs-incremental decision plus honest coverage
|
|
1648
|
+
(`indexedFileCount`/`truncated` and `refreshed`/`dropped`/`skipped`/`reused`
|
|
1649
|
+
counts) for both the session-start and cold-query (MCP) build paths; a
|
|
1650
|
+
swallowed snapshot **persist failure** as `persist_failed` (a silent stale
|
|
1651
|
+
index otherwise leaves no trace); and the safety refusal for a root at/above
|
|
1652
|
+
`$HOME`. The full-build collector (`collectWordIndexDocs`) now also returns a
|
|
1653
|
+
`skipped` count for files it enumerated but could not index (over the byte
|
|
1654
|
+
cap / unreadable), so a partial index is never reported as complete. The
|
|
1655
|
+
fragile string-parsing `dbg` adapter this replaces is removed.
|
|
1656
|
+
|
|
1657
|
+
- **Closed four observability gaps in recently-changed typos-config and project-snapshot code (refs #533)** — none of these change behavior, only
|
|
1658
|
+
what's now logged: (1) the typos LSP's inject-vs-step-aside decision
|
|
1659
|
+
(#967) now logs a `typos_config_resolved` phase in `latency.log` /
|
|
1660
|
+
`sessionstart.log` with `mode: "project_config" | "injected_default"` and
|
|
1661
|
+
the resolved `configPath`, so which typos config is actually active is no
|
|
1662
|
+
longer a guess; (2) a project-snapshot body persist that falls back to the
|
|
1663
|
+
synchronous main-thread gzip because the persist worker died/was
|
|
1664
|
+
unavailable now logs an explicit `project_snapshot_worker_fallback` phase
|
|
1665
|
+
with the `reason` (previously only visible via a test-only variable, or
|
|
1666
|
+
buried in an `offloaded:false` success line) — this is the +656MB-risk
|
|
1667
|
+
degraded path from #950; (3) a corrupt/truncated gzipped snapshot body
|
|
1668
|
+
(gunzip/JSON.parse failure) now logs `project_snapshot_body_corrupt`
|
|
1669
|
+
before failing open to a rebuild, instead of being indistinguishable from
|
|
1670
|
+
"no snapshot yet"; (4) dropping the in-process authoritative snapshot
|
|
1671
|
+
entry for an oversized (>24MB) body now logs
|
|
1672
|
+
`project_snapshot_authoritative_dropped_oversized` — low-priority, but no
|
|
1673
|
+
longer silent.
|
|
75
1674
|
|
|
76
1675
|
- **Review-graph checkpoint discards and write failures are now observable**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1676
|
+
(refs #936, #533) — a present resume checkpoint that's rejected now logs
|
|
1677
|
+
`checkpoint_discarded` with a `reason` (`corrupt`, `version_mismatch`,
|
|
1678
|
+
`not_in_progress`, `git_stamp_mismatch`, `ignored_ids_mismatch`,
|
|
1679
|
+
`removed_file`, `all_stale`) instead of silently falling back to a full cold
|
|
1680
|
+
rebuild, and a failed checkpoint write (worker error/death, promote failure,
|
|
1681
|
+
sync-write failure) logs `checkpoint_write_failed` — so "why isn't my
|
|
1682
|
+
checkpoint resuming / persisting?" is diagnosable from `review-graph.log`.
|
|
84
1683
|
|
|
85
1684
|
- **The review-graph full build is now resumable across sessions** (refs #936
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
1685
|
+
limit 2) — a cold full build (walk + tree-sitter parse of every source file)
|
|
1686
|
+
previously restarted from scratch every session, so on a large repo with
|
|
1687
|
+
short-lived sessions it could never finish. The extraction loop now
|
|
1688
|
+
periodically checkpoints the PRE-resolution graph plus the exact set of files
|
|
1689
|
+
already folded into it (with content hashes) to a dedicated
|
|
1690
|
+
`review-graph.checkpoint.json.gz`, and a later session resumes from it,
|
|
1691
|
+
re-walking only files that changed/appeared since. The checkpoint lives in
|
|
1692
|
+
its own file (never the authoritative `review-graph.json.gz`) and its
|
|
1693
|
+
hydrated graph carries `persistCoverage.inProgress` on top of `partial`, so
|
|
1694
|
+
no reader (`getCachedReviewGraph`, `loadPersistedGraph`) can ever serve or
|
|
1695
|
+
launder a mid-build checkpoint as a complete graph (honesty doctrine, #533).
|
|
1696
|
+
Resume equivalence to a cold build is guaranteed by `addFileToGraph`'s
|
|
1697
|
+
per-file contribution being order-independent (all cross-file linking is
|
|
1698
|
+
deferred to `resolveDeferredSymbolEdges`): content-changed processed files
|
|
1699
|
+
are evicted and re-walked, orphaned placeholder nodes are pruned, and any
|
|
1700
|
+
removed file, ignored-id-set change, version bump, or git-identity mismatch
|
|
1701
|
+
fails open to a cold build rather than risking a wrong graph. Checkpoint
|
|
1702
|
+
stride/interval are tunable via `PI_LENS_GRAPH_CHECKPOINT_EVERY_FILES` /
|
|
1703
|
+
`PI_LENS_GRAPH_CHECKPOINT_MIN_INTERVAL_MS`.
|
|
105
1704
|
|
|
106
1705
|
- **Bundled default typos allowlist for projects without their own config**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1706
|
+
(fixes #967) — the `typos` auxiliary LSP now injects a small pi-lens-shipped
|
|
1707
|
+
`_typos.toml` (`rules/typos/_typos.toml`, `[default.extend-identifiers]`
|
|
1708
|
+
only: `dito`, `unparseable`) via `initializationOptions.config` whenever a
|
|
1709
|
+
project has no `typos.toml`/`_typos.toml`/`.typos.toml` of its own. A
|
|
1710
|
+
project's own config still always wins outright: when one is found,
|
|
1711
|
+
pi-lens injects nothing at all (rather than letting typos-lsp merge ours
|
|
1712
|
+
on top and risk our defaults outranking the team's own allowlist on key
|
|
1713
|
+
collisions).
|
|
115
1714
|
|
|
116
1715
|
- **Session-start latency is attributable end to end** (refs #948) — latency
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1716
|
+
telemetry now separates host boot from pi-lens evaluation and records quick
|
|
1717
|
+
and full session-start totals, pre-handler/bootstrap work, runtime reset,
|
|
1718
|
+
log cleanup, LSP reset, sequence/snapshot reads (including snapshot bytes),
|
|
1719
|
+
and delayed warmup scan/profile/index phases.
|
|
121
1720
|
|
|
122
1721
|
- **Review-graph LSP fallback nodes** (refs #307) — when tree-sitter yields
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
1722
|
+
zero declarations, the builder may use `documentSymbol` from an already-live,
|
|
1723
|
+
already-open capable server. Nodes carry `provenance: "lsp"`, hierarchical
|
|
1724
|
+
containment survives persistence (including flat native-TypeScript-7 results
|
|
1725
|
+
reconstructed through `containerName`), productive tree-sitter files never
|
|
1726
|
+
pay the request, and unavailable/failed fallback attempts degrade without
|
|
1727
|
+
opening or spawning while remaining visible in `review-graph.log`.
|
|
129
1728
|
|
|
130
1729
|
- **Standalone out-of-band review-graph build CLI** (refs #924) — `npx pi-lens
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
1730
|
+
build-graph [--cwd <dir>]` reuses the session builder and queued atomic
|
|
1731
|
+
persist path for CI/cron, forces the debounced snapshot write before exit,
|
|
1732
|
+
and prints file/node/edge/element counts, JSON bytes, and duration. Unsafe
|
|
1733
|
+
roots, build errors/skips, persist failures, and persist-cap trips exit
|
|
1734
|
+
non-zero with their reason instead of silently leaving no snapshot.
|
|
136
1735
|
|
|
137
1736
|
- **`/lens-perf` surfaces slow phases in-session** (closes #767) — the command
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
1737
|
+
shows independent top-five p50 and p99 rankings with sample counts for both
|
|
1738
|
+
the current process session and the machine-wide active `latency.log` window.
|
|
1739
|
+
It flushes pending writes, streams a tail bounded by the log rotation
|
|
1740
|
+
threshold (`PI_LENS_MAX_LOG_SIZE_MB`, 10MB by default), caps retained samples,
|
|
1741
|
+
and reports malformed/truncated input instead of silently reading it as clean.
|
|
1742
|
+
Session startup total and scan-context computation are now logged as phases so
|
|
1743
|
+
the startup regressions that motivated the command are visible there too.
|
|
145
1744
|
|
|
146
1745
|
### Changed
|
|
147
1746
|
|
|
148
1747
|
- **Session-start perf: bound the change-log replay + defer log cleanup (closes #1019)** —
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
1748
|
+
the interactive session-start path recomputed the project sequence by reading
|
|
1749
|
+
the ENTIRE append-only change log and folding every line — and each fold does a
|
|
1750
|
+
`normalizeMapKey`/`realpathSync.native()` syscall per historical entry, so the
|
|
1751
|
+
`session_start_sequence_read` phase (measured ~94ms, ~47% of a 200ms warm start)
|
|
1752
|
+
grew unbounded with total log length, not the working set. The project snapshot
|
|
1753
|
+
now embeds the DERIVED sequence index (`projectSeq` + per-file `fileSeqByPath`)
|
|
1754
|
+
as of its `seq` (`clients/project-snapshot.ts`), mirrored into the tiny
|
|
1755
|
+
`project-snapshot.meta.json` sidecar so session-start can hydrate it WITHOUT
|
|
1756
|
+
parsing the 40-112MB body (preserving the #947 skip-stale optimization).
|
|
1757
|
+
`readLatestProjectSequence` (`clients/project-changes.ts`) gained an optional
|
|
1758
|
+
base param: it hydrates that index (O(files-in-snapshot), keys already
|
|
1759
|
+
normalized → no per-key `realpath`) and folds ONLY entries with
|
|
1760
|
+
`seq > snapshot.seq` — O(changes-since-snapshot). The fold uses the SAME
|
|
1761
|
+
`Math.max`/`normalizeMapKey` the full replay does, so the result is
|
|
1762
|
+
byte-identical to a full replay (proven by an equivalence test suite covering
|
|
1763
|
+
no-new-entries, new/existing files, deletes, gaps/out-of-order, and the empty
|
|
1764
|
+
log) and order-independent. Correctness-first fallbacks to a full replay: a
|
|
1765
|
+
legacy/missing meta with no embedded index, a version-mismatched meta, and a
|
|
1766
|
+
snapshot whose `seq` is AHEAD of the log (truncation/rotation) — it never serves
|
|
1767
|
+
a wrong seq. Wired into BOTH the quick/interactive and the full session-start
|
|
1768
|
+
paths. Secondary: `log_cleanup` (~7ms) was moved off the synchronous critical
|
|
1769
|
+
path into a deferred `setImmediate` (it still runs every session and notifies
|
|
1770
|
+
async — nothing on the hot path consumed its result).
|
|
172
1771
|
|
|
173
1772
|
- **Config-consistency pass on the #166 flag registry (refs #166, #533)** — the
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
1773
|
+
#883 registry core (scope split, precedence, negation) was audited consistent
|
|
1774
|
+
and left untouched; the gaps were all in #533 malformed/unknown-input
|
|
1775
|
+
signalling, asymmetric between the global and project loaders. (1) The
|
|
1776
|
+
project loader (`.pi-lens.json`) now warns once on an unrecognized top-level
|
|
1777
|
+
key — a typo like `maxProjectFile` or `lps` previously did nothing silently —
|
|
1778
|
+
while tolerating the foreign namespaces the shared file legitimately carries
|
|
1779
|
+
for the LSP loader (`servers`, `serverOverrides`, `disabledServers`,
|
|
1780
|
+
`warmFiles`, plus `$schema`); a user-level-only lens key placed at project
|
|
1781
|
+
scope (e.g. `lsp`, `tests`, `delta`) gets a distinct "not honored at project
|
|
1782
|
+
scope" signal instead of being lumped in with typos. (2) The recognized-key
|
|
1783
|
+
catalogs are single-sourced (#883): `GLOBAL_NON_FLAG_CONFIG_SECTIONS` and
|
|
1784
|
+
`PROJECT_FOREIGN_CONFIG_NAMESPACES` are declared once beside `LENS_FLAGS`, and
|
|
1785
|
+
both loaders derive flag sections from the registry, so adding a namespace is
|
|
1786
|
+
a one-line edit and adding a flag needs none — replacing a drift-prone inline
|
|
1787
|
+
literal set in the global loader. (3) Three global scalars
|
|
1788
|
+
(`dispatch.runnerTimeoutFloorMs`, `widget.visible`, `format.mode`) that
|
|
1789
|
+
silently coerced a present-but-malformed value to `undefined` now warn on
|
|
1790
|
+
invalid input through the same path as `actionableWarnings.autoFix.maxFixes`,
|
|
1791
|
+
while staying silent when the key is absent (no false warnings). (4) Removed
|
|
1792
|
+
five dead global-only accessors that bypassed the precedence chain
|
|
1793
|
+
(`getGlobalAutoformatEnabled`, `getGlobalAutofixEnabled`,
|
|
1794
|
+
`getGlobalImmediateFormatDefault`, `getGlobalContextInjectionEnabled`,
|
|
1795
|
+
`getGlobalTurnSummaryEnabled`) — no non-test callers existed. (5) Updated
|
|
1796
|
+
`docs/globalconfig.md`, whose "unknown keys are ignored" claim was stale.
|
|
198
1797
|
|
|
199
1798
|
- **Normalized call-graph impact checks into structured `ProjectDiagnostic`s**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
1799
|
+
(refs #179) — the last remaining adapter gap in #179's normalization sweep.
|
|
1800
|
+
`runtime-turn.ts`'s turn-end call-graph impact check (WillBreak/MayBreak
|
|
1801
|
+
callers of symbols edited this turn, via `impact()`/`call-graph.ts`) was
|
|
1802
|
+
advisory-text only, unlike the other 9 runners already normalized through
|
|
1803
|
+
`runner-adapters/*.ts`. Added `callGraphImpactToProjectDiagnostics`
|
|
1804
|
+
(`project-diagnostics/runner-adapters/call-graph-impact.ts`), a pure mapper
|
|
1805
|
+
attributing each diagnostic to the CALLER's file (no line — `impact()`'s
|
|
1806
|
+
`ImpactResult` never carries one, and the underlying call-graph symbol
|
|
1807
|
+
table only has a placeholder `line: 1`, so inventing one would be a fake
|
|
1808
|
+
location). Severity is deliberately capped below this codebase's
|
|
1809
|
+
"blocking" hard-stop tier for BOTH WillBreak and MayBreak (#533 honesty):
|
|
1810
|
+
`impact()` is a name-resolution heuristic with no type information, so a
|
|
1811
|
+
resolved caller is never a confirmed break. Wired at turn-end (not
|
|
1812
|
+
`fresh-fetch.ts`/`ANALYZER_IDS`) since impact analysis is inherently
|
|
1813
|
+
per-turn-edit, not a whole-project scan — same reasoning that keeps
|
|
1814
|
+
test-runner's own turn-end fire out of that registry. The existing
|
|
1815
|
+
human-readable advisory string is kept as-is (separate surface, same
|
|
1816
|
+
dual-surface pattern already used by knip's turn-end delta). The call-graph
|
|
1817
|
+
block now runs BEFORE the single `writeProjectDiagnosticsDeltaReport`
|
|
1818
|
+
serialization point (alongside knip's delta push) rather than after it —
|
|
1819
|
+
previously it pushed into `projectDiagnosticsDelta`/`projectDiagnosticsSources`
|
|
1820
|
+
past the one-shot write, so a call-graph-only turn persisted nothing and a
|
|
1821
|
+
mixed turn dropped the call-graph entries, leaving `lens_diagnostics` (a
|
|
1822
|
+
pure reader of the persisted report) unable to ever surface the findings
|
|
1823
|
+
(#533). Covered by a new `handleTurnEnd`-level regression test asserting the
|
|
1824
|
+
persisted report for both call-graph-only and mixed turns.
|
|
226
1825
|
|
|
227
1826
|
- **Parallelized the per-turn madge dependency check** (refs #766) — the
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
- **Every runtime toggle is now settable from BOTH the CLI and
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
missing `lens-turn-summary`).
|
|
1827
|
+
turn-end circular-dependency pass previously ran one `await checkFile()`
|
|
1828
|
+
per import-changed file in a sequential `for…await` loop, serializing N
|
|
1829
|
+
madge subprocess spawns. `DependencyChecker.checkFilesBatch()` now runs
|
|
1830
|
+
those spawns concurrently (bounded to 6 in flight) via a new turn-end
|
|
1831
|
+
entry point. `DependencyChecker` keeps its circular-dep findings
|
|
1832
|
+
(`lastCircular`/`circularFiles`) as shared instance state that a single-file
|
|
1833
|
+
check overwrites wholesale, so naively parallelizing risked one file's
|
|
1834
|
+
spawn clobbering a sibling's write depending on subprocess completion
|
|
1835
|
+
order. The fix keeps every spawn's result local until all have settled,
|
|
1836
|
+
then folds them into the shared state in original file order — matching
|
|
1837
|
+
the sequential loop's file-by-file overwrite exactly, just without waiting
|
|
1838
|
+
for each subprocess before starting the next. Behavior-preserving: same
|
|
1839
|
+
per-file results, same final circular-dep state, same `dbg` logs — only
|
|
1840
|
+
wall-clock time changes.
|
|
1841
|
+
|
|
1842
|
+
- **Every runtime toggle is now settable from BOTH the CLI and `~/.pi-lens/config.json`, driven by one declarative registry (closes #166)** —
|
|
1843
|
+
the flag/config mapping used to live in two disconnected places (twelve
|
|
1844
|
+
`pi.registerFlag` calls in `index.ts` and an if/else chain in
|
|
1845
|
+
`resolvePiLensFlagWithSource`), which let coverage gaps open and persist.
|
|
1846
|
+
Seven flags were registered on the CLI but fell straight through the
|
|
1847
|
+
resolver, so config could never set them: `--no-lens`, `--no-lsp`,
|
|
1848
|
+
`--no-tests`, `--no-delta`, `--lens-guard`, `--no-opengrep`, and
|
|
1849
|
+
`--no-read-guard`. They now read `lens.enabled`, `lsp.enabled`,
|
|
1850
|
+
`tests.enabled`, `delta.enabled`, `guard.enabled`, `opengrep.enabled`, and
|
|
1851
|
+
`readGuard.enabled` (positive keys — a `--no-*` flag is the key set `false`).
|
|
1852
|
+
The four actionable-warning toggles went the other way: they were
|
|
1853
|
+
config-only and are now registered CLI flags too
|
|
1854
|
+
(`--lens-actionable-warnings`, `--lens-actionable-warning-actions`,
|
|
1855
|
+
`--lens-actionable-warning-autofix`, `--lens-actionable-warning-all`).
|
|
1856
|
+
`clients/lens-flag-registry.ts` is the single source both surfaces read, so
|
|
1857
|
+
they cannot diverge again; the wiring test derives its expected flag set from
|
|
1858
|
+
it rather than restating it (the old hand-written list had already drifted,
|
|
1859
|
+
missing `lens-turn-summary`).
|
|
262
1860
|
|
|
263
1861
|
- **`actionableWarnings.autoFix.maxFixes` is now actually read** (#166) —
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
- **Flag provenance gained an `env` tier and reports the global tier more
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
resurrect one after the build completes and retires it, and falling back to a
|
|
295
|
-
synchronous write when the worker is unavailable. Best-effort as before — a
|
|
296
|
-
lost checkpoint only costs a cold rebuild.
|
|
1862
|
+
documented in `globalconfig.md` since #792 but no loader ever parsed it, so
|
|
1863
|
+
`agent_end` always used the hardcoded default of 5 no matter what the config
|
|
1864
|
+
said. Same defect class as the seven flags above: a documented config key
|
|
1865
|
+
with nothing wiring it to the code that consumes it. Accepts any
|
|
1866
|
+
non-negative whole number; `0` keeps the warning report while applying
|
|
1867
|
+
nothing.
|
|
1868
|
+
|
|
1869
|
+
- **Flag provenance gained an `env` tier and reports the global tier more accurately** (#166) — `PI_LENS_NO_CONTEXT_INJECTION` moved out of a
|
|
1870
|
+
hardcoded check in `index.ts` into the registry's `env` binding, so it
|
|
1871
|
+
resolves through the same chain as everything else (env → cli →
|
|
1872
|
+
nested-project → project → global → default) and reports `source: "env"`.
|
|
1873
|
+
The global tier now reports `source: "global"` whenever the key is present,
|
|
1874
|
+
not only when it happens to differ from the built-in default. Affects debug
|
|
1875
|
+
and mutation-skip log lines only, no resolved values change.
|
|
1876
|
+
|
|
1877
|
+
- **Unknown top-level keys in `~/.pi-lens/config.json` now warn once instead of being dropped silently** (#166, refs #533) — a typo like `lps` for `lsp`
|
|
1878
|
+
previously did nothing with no signal. The recognized-key set is derived from
|
|
1879
|
+
the flag registry (`LENS_FLAGS`) plus the non-flag sections (`ignore`,
|
|
1880
|
+
`dispatch`, `actionableWarnings`, `widget`) and `$schema`, so it stays in sync
|
|
1881
|
+
with the single source of truth; anything else logs one `[pi-lens] ignoring
|
|
1882
|
+
invalid global config …: unknown key "…"` line and is ignored.
|
|
1883
|
+
|
|
1884
|
+
- **The review-graph resume checkpoint (#936) now offloads its gzip to the shared persist worker (refs #958, #883)** — mid-build checkpoint writes
|
|
1885
|
+
previously ran a synchronous `gzipSync` of the growing graph on the event
|
|
1886
|
+
loop; they now stream the stringify+gzip through the same worker the
|
|
1887
|
+
authoritative snapshot uses (via the newly-shared `writeGzipStageFile` core),
|
|
1888
|
+
generation-gated so a slow write can't clobber a newer checkpoint or
|
|
1889
|
+
resurrect one after the build completes and retires it, and falling back to a
|
|
1890
|
+
synchronous write when the worker is unavailable. Best-effort as before — a
|
|
1891
|
+
lost checkpoint only costs a cold rebuild.
|
|
297
1892
|
|
|
298
1893
|
- **Project snapshot body is now written gzipped by a worker thread** (refs
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1894
|
+
#958 item 2) — the snapshot body (40-112MB observed) is persisted as
|
|
1895
|
+
`project-snapshot.json.gz`, with the `JSON.stringify` + gzip run on a worker
|
|
1896
|
+
thread off the save path, mirroring the review graph's
|
|
1897
|
+
`persist-worker.ts`/generation-gated-promotion pattern (gzip measured 5-10x
|
|
1898
|
+
on top of the #957 compaction win; the review-graph's own measurement was
|
|
1899
|
+
60MB → 1.4MB). A slow worker write for generation N is discarded rather than
|
|
1900
|
+
promoted over a newer generation N+1 already on disk, and the loader still
|
|
1901
|
+
reads the previous uncompressed `project-snapshot.json` for one compatibility
|
|
1902
|
+
release so an upgrade never loses a snapshot. The save path deliberately does
|
|
1903
|
+
NOT sync-gzip (the #950 review measured a naïve sync gzip regressing host
|
|
1904
|
+
memory by +656MB); when the worker is unavailable/dies the pending body falls
|
|
1905
|
+
back to a synchronous main-thread gzip write, surfaced via the logger rather
|
|
1906
|
+
than silently presented as saved (#533). Read-your-writes across the async
|
|
1907
|
+
promotion is preserved by an in-process authoritative "latest write" that
|
|
1908
|
+
`loadProjectSnapshot` consults before disk, so the merge-write callers
|
|
1909
|
+
(`saveRuntimeProjectSnapshot`, word index, reverse deps) never observe a
|
|
1910
|
+
stale body in the promotion window.
|
|
316
1911
|
|
|
317
1912
|
- **Session warmup refreshes the word index incrementally** (refs #958) —
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1913
|
+
the persisted serializer now carries per-file mtimes. Startup still performs
|
|
1914
|
+
the bounded source walk, but reuses unchanged postings, re-tokenizes only
|
|
1915
|
+
stale/new files, and drops deleted files; legacy indexes, refresh failures,
|
|
1916
|
+
and file-set churn above 30% fall back to a full rebuild. The current derived
|
|
1917
|
+
file cap and `truncated` state are re-evaluated on every refresh, and
|
|
1918
|
+
`warmup_word_index` telemetry records mode/refreshed/dropped/reused counts.
|
|
324
1919
|
|
|
325
1920
|
- **Project scans feed compact structural IR into review-graph builds** (refs
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
1921
|
+
#939) — each fully completed scanner file publishes content-hash-bound
|
|
1922
|
+
imports, reexports, function summaries, symbols, and references. A following
|
|
1923
|
+
or overlapping graph build reuses only exact-hash, successful entries;
|
|
1924
|
+
stale, failed, absent, and cold one-shot paths parse normally. The handoff
|
|
1925
|
+
retains neither source content nor WASM trees, and cancelled scans expose
|
|
1926
|
+
only files completed before cancellation.
|
|
332
1927
|
|
|
333
1928
|
- **Session-start performance: LSP pre-warm, snapshot meta-gate, compact cached snapshot** (refs #947) — three
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
1929
|
+
measured startup wins: (1) the dominant-language LSP pre-warm was gated on
|
|
1930
|
+
full startup mode that the first-session heuristic never allows (82 quick
|
|
1931
|
+
vs 0 full starts in 31k dogfood log lines), so it now runs inside the
|
|
1932
|
+
quick-mode +2s background warmup pass — once per process,
|
|
1933
|
+
generation-guarded, honoring subagent light mode, warm-attach, the
|
|
1934
|
+
`no-lsp` flag, and the `canWarmCaches` guard, with a `warmup_lsp_prewarm`
|
|
1935
|
+
phase record; (2) session start no longer sync-parses the whole
|
|
1936
|
+
`project-snapshot.json` body (110-130ms at 40MB, ~0.5s at 112MB) before
|
|
1937
|
+
checking freshness — the tiny `project-snapshot.meta.json` sidecar is read
|
|
1938
|
+
first and a stale seq/version skips the body parse entirely
|
|
1939
|
+
(`session_start_snapshot_load` records `skippedStale: true`), with
|
|
1940
|
+
missing-meta installs falling back to the legacy parse; (3) the snapshot
|
|
1941
|
+
is serialized compactly (~30% smaller) and its parsed body is cached
|
|
1942
|
+
in-process per (path, mtime), so `saveRuntimeProjectSnapshot` and the
|
|
1943
|
+
session-start/word-index/scan-context consumers no longer re-parse a file
|
|
1944
|
+
pi-lens itself wrote seconds earlier.
|
|
350
1945
|
|
|
351
1946
|
- **Warm LSP names enrich tree-sitter read expansion** (refs #158) — partial
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1947
|
+
read expansion keeps tree-sitter's line boundaries authoritative, but an
|
|
1948
|
+
already-open document with an already-active LSP can now replace the display
|
|
1949
|
+
name/kind from `documentSymbol` (including `Class.method` ancestry) within a
|
|
1950
|
+
150 ms best-effort ceiling. Cold, closed, unsupported, timed-out, or failed
|
|
1951
|
+
servers retain the tree-sitter identity, and `ts_range_expanded` records
|
|
1952
|
+
whether enrichment succeeded.
|
|
358
1953
|
|
|
359
1954
|
- **Installer subprocesses are lifetime-coupled** (refs #945) — npm, pip, gem,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1955
|
+
and archive extraction now use the shared safe-spawn path, await full Windows
|
|
1956
|
+
process-tree termination on timeout, and synchronously clean registered
|
|
1957
|
+
installer children during parent exit/signals.
|
|
363
1958
|
|
|
364
1959
|
- **Managed tool installs are cross-process serialized** (refs #945) — a
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
1960
|
+
dependency-free atomic lock protects the shared tools tree, verifies owners
|
|
1961
|
+
before stale recovery, bounds lock waits with an honest error, and rechecks
|
|
1962
|
+
discovery after acquisition to avoid duplicate package-manager runs.
|
|
368
1963
|
|
|
369
1964
|
- **Ordinary tests never install managed tools** (refs #945) — Vitest sets
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1965
|
+
`PI_LENS_DISABLE_TOOL_INSTALL=1`, its prewarm step creates a local synthetic
|
|
1966
|
+
oxlint probe-cache entry without networking, and one-shot analysis explicitly
|
|
1967
|
+
awaits probe-cache persistence before exit.
|
|
373
1968
|
|
|
374
1969
|
- **Installer orphan/locking regressions are process-tested** (refs #945) —
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1970
|
+
fake package-manager coverage verifies Windows timeout tree-kill, exactly one
|
|
1971
|
+
install across concurrent processes, explicit install-disable refusal, and
|
|
1972
|
+
Vitest's default no-install environment.
|
|
378
1973
|
|
|
379
|
-
- **Downgrade TypeScript `unsafe-regex` to advisory and suppress escaped-before-
|
|
380
|
-
assignment false positives** (refs #932) — the coarse dynamic `RegExp`
|
|
1974
|
+
- **Downgrade TypeScript `unsafe-regex` to advisory and suppress escaped-before- assignment false positives (refs #932)** — the coarse dynamic `RegExp`
|
|
381
1975
|
heuristic no longer blocks edits and recognizes escape/replace calls in a
|
|
382
1976
|
same-file identifier initializer; structural ReDoS detection remains with
|
|
383
1977
|
the `redos-nested-quantifier` ast-grep rule.
|
|
384
1978
|
|
|
385
|
-
- **Review-graph persistence no longer serializes or compresses on the event
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
and write time, and whether the work was offloaded.
|
|
1979
|
+
- **Review-graph persistence no longer serializes or compresses on the event loop (refs #939)** — debounced snapshots are materialized in one lazy,
|
|
1980
|
+
unref'd worker and streamed through gzip into the new canonical
|
|
1981
|
+
`review-graph.json.gz` cache. The main thread promotes only the current
|
|
1982
|
+
generation's atomic staged file, so the synchronous CLI/exit flush can
|
|
1983
|
+
supersede an in-flight worker without a stale overwrite. Loads retain one
|
|
1984
|
+
release of fallback support for legacy uncompressed `review-graph.json`
|
|
1985
|
+
snapshots; worker failures are logged and degrade to a synchronous persist.
|
|
1986
|
+
Persist telemetry now records element count, raw/gzip bytes, serialization
|
|
1987
|
+
and write time, and whether the work was offloaded.
|
|
395
1988
|
|
|
396
1989
|
- **Raise and make the review-graph persist ceiling useful** (refs #936) — the
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
1990
|
+
default `GRAPH_PERSIST_MAX_ELEMENTS` cap is now 500,000 (still overrideable
|
|
1991
|
+
through `PI_LENS_GRAPH_PERSIST_MAX_ELEMENTS`), matching measured startup
|
|
1992
|
+
load/reindex costs and allowing the ~208,000-element #919 repository to
|
|
1993
|
+
persist completely. Above the cap, persistence retains whole-file node groups
|
|
1994
|
+
in existing reverse-dependency-centrality order plus induced edges that fit,
|
|
1995
|
+
instead of dropping the snapshot. Exact total-versus-persisted node/edge
|
|
1996
|
+
counts are stored and surfaced; read-only queries may use the partial graph,
|
|
1997
|
+
while incremental builds reject it as a complete base.
|
|
405
1998
|
|
|
406
1999
|
- **Logger hot paths now coalesce queued lines and rotate during long sessions** (refs #935) — contiguous NDJSON entries drain through one append up to each truncate boundary while retaining peek-then-remove exit safety and one-write cross-process atomicity. `sessionstart.log` now uses one shared asynchronous writer for ordinary diagnostics (with the crash-adjacent LSP launch write intentionally synchronous), and latency/cascade/tree-sitter/bus-event logs enforce the existing 10 MB cap in process.
|
|
407
2000
|
|
|
408
|
-
- **Incremental review-graph updates avoid redundant whole-graph copies and
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
defers its O(graph) array materialization until the quiet-window flush.
|
|
2001
|
+
- **Incremental review-graph updates avoid redundant whole-graph copies and index rebuilds (refs #939)** — file re-extraction now rebuilds derived indexes
|
|
2002
|
+
once, immutable edges are array-copied without cloning every edge object, the
|
|
2003
|
+
updated graph itself becomes the workspace snapshot, and debounced persistence
|
|
2004
|
+
defers its O(graph) array materialization until the quiet-window flush.
|
|
413
2005
|
|
|
414
2006
|
- **Reverse-dependency indexes update at import-edge granularity** (refs #939)
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
2007
|
+
instead of rebuilding from every graph edge after a one-file edit. Body-only
|
|
2008
|
+
edits reuse the cached index without rewriting the project snapshot; import
|
|
2009
|
+
changes patch only the touched `imports` and `importedBy` buckets.
|
|
418
2010
|
|
|
419
2011
|
- **Review-graph file-cap degradation is now explicit and count-honest** (refs
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
2012
|
+
#921) — `project_report` says a capped project has “more than N files” instead
|
|
2013
|
+
of presenting the cap+1 early-exit sentinel as an exact count. `module_report`
|
|
2014
|
+
now marks graph-backed `usedBy`, blast-radius provenance, and
|
|
2015
|
+
`semantic.source` as `unavailable:file-cap` and emits an actionable warning
|
|
2016
|
+
with the cap plus both `.pi-lens.json#maxProjectFiles` and
|
|
2017
|
+
`PI_LENS_REVIEW_GRAPH_MAX_FILES` controls, keeping disabled data distinct from
|
|
2018
|
+
a genuinely empty/cold graph.
|
|
427
2019
|
|
|
428
2020
|
- **Project scans release every scan-local fact after each file** (refs #886,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
2021
|
+
#939) instead of retaining source content, imports, summaries, and other
|
|
2022
|
+
derived per-file facts until the scan ends. The live dispatch store remains
|
|
2023
|
+
untouched; only the scanner-owned store is cleared.
|
|
432
2024
|
|
|
433
2025
|
- Repair eight non-compiling Java, C++, CSS and PHP tree-sitter rules (refs #884).
|
|
434
2026
|
|
|
435
2027
|
- Repair four non-compiling Go, Rust, and Kotlin tree-sitter rules (refs #884).
|
|
436
2028
|
|
|
437
2029
|
- **Project diagnostics now use one file-major scan pass** (refs #896) —
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
2030
|
+
tree-sitter rules, fact rules, and bundled ast-grep share each eligible
|
|
2031
|
+
file's content read while retaining their individual extension/size gates,
|
|
2032
|
+
diagnostic ordering, cancellation behavior, and latency telemetry. Full
|
|
2033
|
+
review-graph builds likewise hash the bytes already read for extraction
|
|
2034
|
+
instead of rereading every file after the graph is built.
|
|
443
2035
|
|
|
444
2036
|
- **A project scan runs its rule set in one tree walk, not one walk per rule**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
2037
|
+
(refs #675) — `runQueriesOnFile` compiles a language's rules into a single
|
|
2038
|
+
multi-pattern query and maps matches back to the owning rule, keeping per-rule
|
|
2039
|
+
metavars, predicates, post-filters, caps and ordering. Measured at 3.3× on
|
|
2040
|
+
identical matches; the scan's tree-sitter phase went **14.4s → 3.6s** and a
|
|
2041
|
+
full `mode=full` scan of pi-lens **24.4s → 12.2s**.
|
|
450
2042
|
|
|
451
2043
|
- **Project scans parse each file once** (refs #675) — the tree-sitter rules and
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
2044
|
+
the fact rules both tree-sitter-parse the same files, and running them as two
|
|
2045
|
+
whole-project sweeps meant the second one re-parsed every file the first had
|
|
2046
|
+
already parsed and evicted (357 of 357 first touches missed on capacity in a
|
|
2047
|
+
500-file `mode=full` scan). They now share one file-major pass: **722 parses →
|
|
2048
|
+
367, 892ms → 437ms of parse time, zero capacity misses**, same diagnostics in
|
|
2049
|
+
the same order. The 50-entry cache is unchanged and was never the problem —
|
|
2050
|
+
within a phase it already ran at a 96.8% hit rate, any capacity short of the
|
|
2051
|
+
full working set saves nothing, and one big enough to hold it would cost
|
|
2052
|
+
150-300MB of resident WASM trees.
|
|
461
2053
|
|
|
462
2054
|
- **Tree-sitter cache pressure is measurable** (refs #675): every production
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
2055
|
+
consumer, including partial-read expansion, uses the shared process-wide
|
|
2056
|
+
client, and scope-isolated cache counters plus parser timing are emitted for
|
|
2057
|
+
project scans and full review-graph builds.
|
|
466
2058
|
|
|
467
2059
|
- **Dead incremental-parse cache API removed.** `TreeCache.incrementalUpdate`,
|
|
468
|
-
|
|
469
|
-
|
|
2060
|
+
`calculateEdit`, and `invalidate` had no callers; the header's "10-100×
|
|
2061
|
+
speedup on edits" was advertising a path nothing ever took.
|
|
470
2062
|
|
|
471
2063
|
- **Faster LSP teardown and tool probes** (refs #448) — `killProcessTree`
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
2064
|
+
resolves on the child's exit event instead of sleeping the full 1.5s
|
|
2065
|
+
SIGTERM→SIGKILL escalation window (saves ~1.5s per graceful client
|
|
2066
|
+
shutdown); tool availability checks skip `--version` spawns for binaries
|
|
2067
|
+
that aren't on disk and no longer re-verify a probe-cache-validated managed
|
|
2068
|
+
install on every analyze.
|
|
477
2069
|
|
|
478
2070
|
### Fixed
|
|
479
2071
|
|
|
480
2072
|
- **Read-before-edit guard now survives a session resume (#1041)** — the
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
2073
|
+
guard's read-set was in-memory only, so a `pi --session <id>` resume (which
|
|
2074
|
+
resets the runtime to a fresh empty guard) falsely `zero_read`-blocked the
|
|
2075
|
+
first edit of any file read in the prior session. The read-set now rides the
|
|
2076
|
+
same #190 `PersistedSessionState` save/load path that widget diagnostics use:
|
|
2077
|
+
`ReadGuard.exportState()`/`importState()` persist and rehydrate `reads`,
|
|
2078
|
+
reconciling each read against current disk (line-hash verified) so a resume
|
|
2079
|
+
drops any read whose file changed or vanished — a rehydrated read never masks
|
|
2080
|
+
a real staleness. Forked sessions adopt the parent's read-set via the same
|
|
2081
|
+
in-memory hand-off as widget state. Backward-compatible: the field is optional
|
|
2082
|
+
and pre-#1041 persisted sessions load cleanly as "no prior reads".
|
|
491
2083
|
|
|
492
2084
|
- **`turn_summary_emit` quiet-window task no longer fails on a stale pi ctx (the #483 quiet window's most frequent live-dogfood error — 55×)** —
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
2085
|
+
the `agent_settled` quiet-window task `turn_summary_emit` reads the
|
|
2086
|
+
`lens-turn-summary` flag through `pi.getFlag()` (via `getLensFlag`). The task
|
|
2087
|
+
holder's `pi` is refreshed on every activation, but the quiet window is fired
|
|
2088
|
+
fire-and-forget from `agent_settled`, so an interim session
|
|
2089
|
+
replacement/reload (`ctx.newSession/fork/switchSession/reload` — common in the
|
|
2090
|
+
in-process subagent flow #473) invalidates that captured `pi`; the very next
|
|
2091
|
+
`pi.getFlag()` then throws the SDK's stale-ctx guard. That throw sat OUTSIDE
|
|
2092
|
+
the existing `sendMessage` try/catch, so it escaped the task and the scheduler
|
|
2093
|
+
logged `quiet_window: task "turn_summary_emit" failed: … stale after session
|
|
2094
|
+
replacement or reload` — 55 times across two weeks of dogfooding, the single
|
|
2095
|
+
most frequent error in `~/.pi-lens/sessionstart.log`, and it fired even with
|
|
2096
|
+
the feature OFF (the flag READ throws before the flag value is known). Fix:
|
|
2097
|
+
the task now recognises the stale-ctx throw (new `isStaleExtensionCtxError`
|
|
2098
|
+
message matcher) at both pi touch points (flag read + emit) and degrades to a
|
|
2099
|
+
logged no-op — the session that run's summary belonged to is gone, so there is
|
|
2100
|
+
nothing to emit into — while a genuine error still propagates to the scheduler
|
|
2101
|
+
(recorded `ok:false`). User impact was minimal (a lost summary for the exact
|
|
2102
|
+
settle that raced a session swap, plus log noise), not silent feature
|
|
2103
|
+
degradation: in steady state the task runs `ok:true`. Separately, the
|
|
2104
|
+
quiet-window runner now logs the failing task's **stack** (not just `${err}`'s
|
|
2105
|
+
message) so the next failure is diagnosable from the log alone; it remains
|
|
2106
|
+
strictly non-fatal (per-task try/catch, `runQuietWindow` never rethrows, and
|
|
2107
|
+
`agent_settled` invokes it fire-and-forget — a failing task can never break the
|
|
2108
|
+
turn). Regression test drives the registered task against a `pi` whose
|
|
2109
|
+
`getFlag` throws the stale-ctx error and asserts a no-op (no throw, no emit);
|
|
2110
|
+
it fails pre-fix.
|
|
519
2111
|
|
|
520
2112
|
- **Word index no longer serves stale postings after a case/separator-divergent edit (refs #1025, closes #1025 item #2)** —
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
2113
|
+
the word index's `docLengths`/`forward`/`fileMtimes` maps are keyed on file
|
|
2114
|
+
paths, but the full/incremental BUILD keyed on the on-disk casing the
|
|
2115
|
+
`readdirSync` walk reports while the per-edit UPDATE seams
|
|
2116
|
+
(`clients/dispatch/integration.ts`, `clients/mcp/analyze.ts`) key via
|
|
2117
|
+
`path.resolve()` on the raw tool-input path. When those forms differed only by
|
|
2118
|
+
separator (`\` vs `/`, always) or case (on a case-insensitive FS), the plain-`Map`
|
|
2119
|
+
keying made `forward.has(doc.path)` MISS, so the stale build-form entry was never
|
|
2120
|
+
removed — a duplicate doc entry with stale postings served until a full rebuild,
|
|
2121
|
+
silently degrading `symbol_search` relevance (the path-key divergence class,
|
|
2122
|
+
#1020/#210). Fix: a new typed **`PathKeyedMap<V>`** primitive
|
|
2123
|
+
(`clients/path-keyed-map.ts`) folds every key through a caller-supplied
|
|
2124
|
+
normalizer INTERNALLY, so keying a raw path is structurally impossible; the word
|
|
2125
|
+
index now routes all three path maps through it using the shared, exported
|
|
2126
|
+
`wordIndexKey` (= `normalizeEphemeralMapKey` — cheap slash-fold + win32-lowercase,
|
|
2127
|
+
NO `realpathSync`, so the hot BM25 path adds no filesystem I/O), and the
|
|
2128
|
+
token-keyed `postings` cleanup compares `WordHit.file` through the same
|
|
2129
|
+
normalizer, and the incremental-refresh set-difference
|
|
2130
|
+
(`refreshWordIndexIncrementally`) now computes its `current`/`oldSet` keys in
|
|
2131
|
+
that same normalized space (previously raw-keyed — a file whose stored display
|
|
2132
|
+
path became the edit form would have been double-counted as churn and
|
|
2133
|
+
dropped-then-re-added, defeating the incremental path and opening a
|
|
2134
|
+
drop-before-readd regression window). `PathKeyedMap` stores each value's
|
|
2135
|
+
ORIGINAL display path, so `symbol_search`/rendering surfaces still see the
|
|
2136
|
+
on-disk form, not the folded key. Build-form, edit-form, and refresh keys now
|
|
2137
|
+
converge on ONE entry regardless of casing/separator. The runtime-coordinator suspect maps (#1025 item #4) are left
|
|
2138
|
+
for a follow-up (unproven trigger, and their correct normalizer is the
|
|
2139
|
+
realpath-based `normalizeMapKey` matching the file's own `_fileSeq` convention —
|
|
2140
|
+
a larger, higher-risk change across the concurrent-session drain logic).
|
|
549
2141
|
|
|
550
2142
|
- **Cascade honesty: degraded/uncomputed impact no longer renders as clean (refs #1023, closes #1023)** —
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
2143
|
+
the cascade impact subsystem previously emitted an all-clear that was
|
|
2144
|
+
indistinguishable from "genuinely nothing impacted" whenever it could NOT
|
|
2145
|
+
compute impact: the review graph was size-skipped (`too_many_files` over
|
|
2146
|
+
`PI_LENS_REVIEW_GRAPH_MAX_FILES`) or root-skipped (`unsafe_root`), the changed
|
|
2147
|
+
file had no graph node, or the deferred compute threw — a silent under-report
|
|
2148
|
+
exactly where downstream breakage is most likely (large/over-cap monorepos)
|
|
2149
|
+
(#533). Impact results now carry a first-class `indeterminate` marker
|
|
2150
|
+
(`clients/review-graph/types.ts`) set ONLY on a degraded/cold/errored/
|
|
2151
|
+
missing-node compute; the already-known degraded state is threaded from
|
|
2152
|
+
`getLastGraphBuildInfo().mode`/`skipReason` at the compute call site (no
|
|
2153
|
+
re-derivation), a `missing_node` marker comes straight from
|
|
2154
|
+
`computeImpactCascade`, and a thrown compute is tagged in the pipeline catch. A
|
|
2155
|
+
new `indeterminate` `CascadeSkipReason` flows to the EXISTING turn-end seam
|
|
2156
|
+
(`clients/runtime-turn.ts`), which renders a short honest note in the ADVISORY
|
|
2157
|
+
tier ("Cascade could not compute downstream impact … a clean cascade result
|
|
2158
|
+
does not cover them") instead of silence. The advisory tier (not the blocker
|
|
2159
|
+
tier) is deliberate: an over-cap monorepo skips the graph on every edit, so a
|
|
2160
|
+
blocker would fire hard and never clear turn state every turn (over-escalation,
|
|
2161
|
+
the mirror of the silent-all-clear bug). A HEALTHY graph with a genuinely empty
|
|
2162
|
+
dependent set stays silent (keyed strictly off the degraded mode/marker, never
|
|
2163
|
+
off `neighbors.length === 0` alone — no crying wolf). Companion: the Tier-3 quiet-window reconcile
|
|
2164
|
+
(`clients/lsp/cascade-tier.ts`) now RE-INJECTS a `resolved-found` cold-neighbor
|
|
2165
|
+
error through the same neighbor→turn-end formatting via an `onResolvedFound`
|
|
2166
|
+
callback wired in `index.ts`, instead of leaving it logs-only. The
|
|
2167
|
+
neighbor-touch/freshness pipeline (neighborTouchCache, recentlyCleanNeighborCache,
|
|
2168
|
+
the 40-neighbor budget, tier-aware touch model, deferred cascade flow) is
|
|
2169
|
+
untouched.
|
|
578
2170
|
|
|
579
2171
|
- **Disposition marks under a nested monorepo language root are no longer silently dropped (refs #1030, closes #1030; pairs with #1024)** —
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
2172
|
+
a `false-positive`/`flagged`/`defer` mark recorded via `lens_diagnostic_mark`
|
|
2173
|
+
was invisible to the per-edit dispatch filter for every file living under a
|
|
2174
|
+
nested language-root marker (`packages/app/tsconfig.json`, a nested
|
|
2175
|
+
`package.json`/`pyproject.toml`/`go.mod`/etc.) in a monorepo. The mark tool
|
|
2176
|
+
writes dispositions keyed on the **project root** (`runtime.projectRoot`), but
|
|
2177
|
+
`dispatcher.ts`'s `applyDispositions` read keyed on `ctx.cwd`, which
|
|
2178
|
+
`createDispatchContext` resolves to the nearest **language root** via
|
|
2179
|
+
`resolveLanguageRootForFile` (a dispatch-internal tool/config-resolution
|
|
2180
|
+
detail). Because both the anchor AND the persisted store location
|
|
2181
|
+
(`getProjectDataDir`, keyed on `cwd`) derive from that base, write and read
|
|
2182
|
+
computed different anchors AND opened entirely different
|
|
2183
|
+
`diagnostic-dispositions.json` files — a total, deterministic failure (not
|
|
2184
|
+
probabilistic) for every file under a nested marker. The tool returned success
|
|
2185
|
+
while dispatch never saw the mark (a #533 honesty failure); `suppress` survived
|
|
2186
|
+
only via its independent inline `pi-lens-ignore` comment. Fixed by reading the
|
|
2187
|
+
disposition layer from the project root (`ctx.projectRoot`, already computed in
|
|
2188
|
+
`createDispatchContext`), matching the write side — anchor base and store base
|
|
2189
|
+
now agree on both sides. `resolveLanguageRootForFile` is untouched and still
|
|
2190
|
+
drives actual dispatch/tool-root resolution. Orthogonal to #1024 (which fixes
|
|
2191
|
+
the path *form* of the anchor); the two compose for a monorepo-stable anchor.
|
|
2192
|
+
No new dependencies.
|
|
601
2193
|
|
|
602
2194
|
- **Disposition anchor path-form stability (refs #1024, closes #1024)** — the
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
2195
|
+
`dd:`/`ddw:` disposition anchors (clients/diagnostic-dispositions.ts) are now
|
|
2196
|
+
derived from a single canonical path form, closing a #533-adjacent
|
|
2197
|
+
dropped-signal bug (sibling of #210/#1020). `relativeFile` — the chokepoint
|
|
2198
|
+
both `computeStrictAnchor` and `computeWeakAnchor` route through — computed
|
|
2199
|
+
`path.relative` on whichever raw path form the caller happened to hold: the
|
|
2200
|
+
mark tool (tools/lens-diagnostic-mark.ts) passes a RAW `cwd` /
|
|
2201
|
+
`path.resolve(cwd, arg)`, while the dispatch read side
|
|
2202
|
+
(clients/dispatch/dispatcher.ts `createDispatchContext`) passes
|
|
2203
|
+
`normalizeMapKey`-canonicalized (realpath'd) cwd/filePath. When the two forms
|
|
2204
|
+
diverged (Windows drive/segment case, symlink/realpath), the agent's own
|
|
2205
|
+
`false-positive`/`flagged` mark anchored under one id and the later
|
|
2206
|
+
`applyDispositions` lookup under another, so the mark was invisible and the
|
|
2207
|
+
"resolved" diagnostic kept re-firing / re-blocking every turn. Fix routes
|
|
2208
|
+
BOTH `cwd` and `filePath` through `normalizeMapKey` inside `relativeFile`
|
|
2209
|
+
(the same normalizer the read side already relies on — the #210 "every guard
|
|
2210
|
+
map keys through `normalizeFilePath`" invariant), so write and read derive
|
|
2211
|
+
identical anchors regardless of the form the caller passed. Anchor semantics
|
|
2212
|
+
are unchanged: strict (`dd:`, line-hash) vs weak (`ddw:`) split, the prefixes,
|
|
2213
|
+
and the `..`-escape fallback all behave exactly as before — only the path
|
|
2214
|
+
component is now form-stable. Existing on-disk marks written under the old raw
|
|
2215
|
+
form re-anchor to the canonical id; a stale orphan is re-derivable and simply
|
|
2216
|
+
reappears once for the agent to re-mark (identical to today's pre-fix
|
|
2217
|
+
behavior), so no read-time legacy fallback is needed.
|
|
626
2218
|
|
|
627
2219
|
- **`lens_diagnostics mode=all` no longer replays a resolved blocker (refs #1020, closes #1020)** —
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
2220
|
+
the widget-state `files` map was keyed by the raw, non-normalized path string,
|
|
2221
|
+
so the SAME file could land under two different key forms in one session:
|
|
2222
|
+
the forward-slash form (`C:/…/x.ts`) the LSP client + cascade fold produce via
|
|
2223
|
+
`normalizeFilePath`, and the backslash form (`C:\…\x.ts`) that `mode=full`'s
|
|
2224
|
+
clean reconcile (`result.filePath`) and `path.resolve`/event inputs produce on
|
|
2225
|
+
Windows. A stale blocking entry and the fresh clean entry then coexisted as two
|
|
2226
|
+
separate map entries. `mode=full` rendered clean because its merge re-keys every
|
|
2227
|
+
summary through `path.resolve`, but `mode=all`'s `formatAllMode` reads
|
|
2228
|
+
`getFileDiagnosticSummaries()` verbatim with no dedup, so it saw both and
|
|
2229
|
+
rendered the stale entry's `blocking:1` as a 🔴 — a resolved state that replayed
|
|
2230
|
+
as still-broken on every `mode=all` (worst on Windows + resumed sessions), and a
|
|
2231
|
+
#533 honesty failure since the tool prompt tells agents to use `mode=all` to
|
|
2232
|
+
verify no blockers remain. Fixed at source: every write/read seam on the `files`
|
|
2233
|
+
map (and the `diagnosticsWriteGuard`) now folds its key through one normalizer,
|
|
2234
|
+
`normalizeEphemeralMapKey` (slash-fold + win32-lowercase, no filesystem I/O —
|
|
2235
|
+
chosen over the `realpathSync`-backed `normalizeMapKey` because this is a hot
|
|
2236
|
+
write path), and `importWidgetState` folds persisted keys on rehydrate so a
|
|
2237
|
+
`/`-key snapshot and a fresh `\`-key write collapse across a resumed session.
|
|
2238
|
+
The human-readable display path on each record is preserved verbatim (rendering
|
|
2239
|
+
and path-relative math unchanged). No new dependencies.
|
|
648
2240
|
|
|
649
2241
|
- **Cache-friendly ephemeral context injection (refs #1016, closes #1016)** — the
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
- **Fixed: `ts-ssrf` no longer flags fixed/const endpoint URLs built with
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
all under the process-creation pressure of a full parallel test run.
|
|
833
|
-
Switched both to the already-hardened `safeSpawn` (`clients/safe-spawn.ts`,
|
|
834
|
-
#817) — cached PATH+PATHEXT resolution, direct `.exe`/`.com` spawn with no
|
|
835
|
-
shell involved — the same deterministic resolution production dispatch
|
|
836
|
-
already relies on. The third tracked item
|
|
837
|
-
(`startup-overhead.test.ts`'s "quick mode self-reports within 100ms")
|
|
838
|
-
was already reworked to a generous fixed budget + `retry: 2` in an earlier
|
|
839
|
-
pass; confirmed still adequate, no further change needed.
|
|
2242
|
+
`context` event handler in `index.ts` now splices pi-lens's ephemeral turn-end
|
|
2243
|
+
findings in **immediately before the final message** instead of prepending them
|
|
2244
|
+
at message index 0. Prepending flipped `messages[0]` every turn, which
|
|
2245
|
+
invalidated the entire prompt-cache prefix on every prefix-caching provider
|
|
2246
|
+
(Anthropic, Bedrock, and OpenAI — all key the cache on the exact token prefix).
|
|
2247
|
+
Inserting before the last message keeps `messages[0]` (the real first user turn)
|
|
2248
|
+
byte-stable so the prior conversation stays cached, while still keeping the real
|
|
2249
|
+
user prompt as the trailing message — preserving the trailing-`user` cache
|
|
2250
|
+
breakpoint and the historical `fe0ed5da` guarantee that input is never empty (the
|
|
2251
|
+
existing transcript is always preserved, never dropped; an empty transcript falls
|
|
2252
|
+
back to the prior prepend-only behavior). Because the `context` event fires
|
|
2253
|
+
before **every** provider call (not just at turn boundaries), a trailing-role
|
|
2254
|
+
guard (`isPlainUserPrompt`) only splices before the last message when it is a
|
|
2255
|
+
plain user prompt; when the tail is a mid-loop `tool_result` (or assistant/tool
|
|
2256
|
+
message) the findings are **appended** after the whole transcript instead — a
|
|
2257
|
+
pure append that both preserves the `tool_use`↔`tool_result` adjacency all three
|
|
2258
|
+
providers require (splicing between them is an HTTP 400) and keeps the entire
|
|
2259
|
+
prior transcript as an untouched cache prefix. Changes shipped `dist` behavior
|
|
2260
|
+
(the handler lives in the bundled dist path).
|
|
2261
|
+
|
|
2262
|
+
- **Fixed: `ts-ssrf` no longer flags fixed/const endpoint URLs built with `new URL(...)` as SSRF sinks, while still catching tainted URLs (closes #1000, refs #533, #963)** — a project-wide pi-free scan flagged fixed outbound
|
|
2263
|
+
OAuth/profile endpoints as SSRF: `fetch(authUrl.toString())` where
|
|
2264
|
+
`authUrl = new URL("auth/authorize", \`${BASE_URL_CLINE}/\`)` with a fixed
|
|
2265
|
+
(module-const or imported) base. The `ts_ssrf_sink` post-filter already
|
|
2266
|
+
exempted a `fetch()` of a file-local `const` literal string (#963) but had no
|
|
2267
|
+
awareness of the `new URL(literalPath, fixedBase)`shape, so
|
|
2268
|
+
`url.toString()`/`url.href` fell through to the broad taint heuristic. Extended
|
|
2269
|
+
the post-filter to also exempt `fetch(u.toString())`/`fetch(u.href)` when `u`
|
|
2270
|
+
resolves (same file) to a clean `const u = new URL(<literalPath>, <fixedBase>)`
|
|
2271
|
+
— literal path plus a base that is a literal, a file-local literal `const`, or
|
|
2272
|
+
an import binding (the`URL` ctor may be aliased, e.g. `NodeURL`). Query params
|
|
2273
|
+
added later via`searchParams.set(...)` don't control origin/path and never
|
|
2274
|
+
taint the destination. Still fires (regression fixtures added) when the base
|
|
2275
|
+
or path comes from a function parameter, `process.env`/config (including a
|
|
2276
|
+
const initialized from`process.env`), request data, or a parsed
|
|
2277
|
+
redirect/`location`value — a missed SSRF stays far worse than one FP.
|
|
2278
|
+
Adversarial review then closed two bypasses in that exemption: (1) a
|
|
2279
|
+
post-construction origin mutation (`u.host = req.x` / `u.href = …`/
|
|
2280
|
+
`u["host"] = …`, incl. augmented assignments) now re-taints the receiver and
|
|
2281
|
+
fails closed — only query-only writes (`u.search`/`u.searchParams`) stay
|
|
2282
|
+
exempt; (2) a request-tainted function *parameter* base is no longer exempted
|
|
2283
|
+
just because an unrelated same-named module-level`const` literal exists
|
|
2284
|
+
(scope-aware shadow check; imported bases stay trusted).
|
|
2285
|
+
|
|
2286
|
+
- **Fixed: `no-javascript-url`/`no-javascript-url-js` no longer flag defensive `javascript:`-URL filters (refs #533)** — a dogfood run flagged code that was
|
|
2287
|
+
*rejecting* `javascript:` links (e.g. `url.startsWith("javascript:")` inside a
|
|
2288
|
+
guard) as if it were introducing one. The rule matched the `javascript:`
|
|
2289
|
+
string literal alone with no way to tell "used as a sink" from "used to
|
|
2290
|
+
detect/block it". Narrowed both rules to also exclude the literal when it's
|
|
2291
|
+
the needle in `.startsWith`/`.endsWith`/`.includes`/`.indexOf`/`.search`/
|
|
2292
|
+
`.match`, or one side of an `===`/`!==`/`==`/`!=` comparison — deliberately
|
|
2293
|
+
NOT `.replace`/`.replaceAll`, since the literal there could be the malicious
|
|
2294
|
+
replacement argument rather than the defensive search argument, and
|
|
2295
|
+
excluding the whole call would hide that true positive. Both new exclusions
|
|
2296
|
+
deliberately avoid `stopBy: end` (unlike the pre-existing test-literal
|
|
2297
|
+
exclusion, which needs it) so the literal must be a DIRECT argument/operand
|
|
2298
|
+
of the defensive call/comparison, not merely somewhere in its subtree — an
|
|
2299
|
+
adversarial review caught that `stopBy: end` let a real sink smuggled
|
|
2300
|
+
inside the call/comparison (e.g. `candidates.includes(link.href =
|
|
2301
|
+
"javascript:...")`, a comma-operator sink, or `(a.href = "javascript:...")
|
|
2302
|
+
=== expected`) go unflagged. Added valid fixtures reproducing the filter FP
|
|
2303
|
+
and invalid fixtures for the replace-based sink and all three smuggled-sink
|
|
2304
|
+
bypasses, confirmed against the real ast-grep 0.45.0 binary.
|
|
2305
|
+
|
|
2306
|
+
- **Contained spawn/callback failures that could crash the host (pidusage bug class) (refs #533)** — a best-effort telemetry sampler recently killed a live
|
|
2307
|
+
pi host with `Error: spawn UNKNOWN` (uncaughtException) because a bundled
|
|
2308
|
+
dep's `child_process.spawn(...)` threw SYNCHRONOUSLY with no try/catch, from
|
|
2309
|
+
a detached context the caller's `try/await/catch` could not contain. Audited
|
|
2310
|
+
every spawn/detached-callback site in `clients/` and hardened the at-risk
|
|
2311
|
+
best-effort/background ones to the repo's existing guard shape (try/catch
|
|
2312
|
+
around the synchronous `spawn` + resolve gracefully, matching the async
|
|
2313
|
+
`'error'`-listener path): `safeSpawnAsync`'s core spawn (the shared wrapper
|
|
2314
|
+
dozens of best-effort callers rely on to NEVER reject), its Windows
|
|
2315
|
+
`killPidTreeSync` (runs from `process` exit/signal handlers, where a throw
|
|
2316
|
+
becomes an uncaughtException during shutdown; the POSIX branch was already
|
|
2317
|
+
guarded), `SgRunner.exec`'s three ast-grep spawn branches, all six installer
|
|
2318
|
+
tool-discovery/verify probes, the PSScriptAnalyzer runner's `spawnPs`, and
|
|
2319
|
+
the MCP `analyzeFileFresh` worker fork. A synchronous spawn throw now
|
|
2320
|
+
resolves the operation's normal failure value instead of escaping as an
|
|
2321
|
+
unhandledRejection/uncaughtException. Behavior-preserving otherwise.
|
|
2322
|
+
|
|
2323
|
+
- **Fixed: opengrep security findings now surface project-wide in `lens_diagnostics mode=full` (refs #585, #584, #533)** — opengrep's
|
|
2324
|
+
whole-tree CLI scan ran at session-start and cached its findings, and the
|
|
2325
|
+
cache-only extractor registry (`project-diagnostics/extractors.ts`) even
|
|
2326
|
+
registered an `"opengrep"` row — but #585 replaced that registry in
|
|
2327
|
+
production with `fetchFreshProjectDiagnostics`
|
|
2328
|
+
(`project-diagnostics/fresh-fetch.ts`), whose `ANALYZER_IDS` list omitted
|
|
2329
|
+
opengrep. Net: opengrep scanned + cached, yet nothing production read it
|
|
2330
|
+
back, so semgrep-grade security findings (ERROR→blocking, CWE-tagged) never
|
|
2331
|
+
reached the agent for unedited/project-wide files — a scan-and-orphan
|
|
2332
|
+
honesty gap (#533). `fetchFreshProjectDiagnostics` now runs opengrep the
|
|
2333
|
+
same way it runs gitleaks/trivy: an availability probe (opengrep is
|
|
2334
|
+
structurally always-on, so no static project-type gate), then a fresh scan
|
|
2335
|
+
that JOINS the session-start scan of the same root via
|
|
2336
|
+
`SecurityScanClient.dedupeScan` rather than double-spawning a heavy scan,
|
|
2337
|
+
with the result written back to cache and adapted to project diagnostics.
|
|
2338
|
+
The per-edit aux-LSP push path (`clientScope: with-auxiliary`,
|
|
2339
|
+
`AUXILIARY_LSP_PROFILES`) was verified already-working and is untouched;
|
|
2340
|
+
opengrep stays excluded from the push-only LSP workspace sweep (#584). The
|
|
2341
|
+
now-dead cache-only `extractCachedProjectDiagnostics` reader — the parallel
|
|
2342
|
+
registry that shadowed `ANALYZER_IDS` and let opengrep silently diverge — was
|
|
2343
|
+
removed so a single source of truth (#883) remains.
|
|
2344
|
+
|
|
2345
|
+
- **Fixed: test-runner findings now surface project-wide in `lens_diagnostics mode=full` (closes #1004, refs #585, #533)** — the same `ANALYZER_IDS`
|
|
2346
|
+
omission #585/#1003 fixed for opengrep also orphaned test-runner: the
|
|
2347
|
+
per-edit turn_end test fire (`runtime-turn.ts`) caches failures under
|
|
2348
|
+
`"test-runner-findings"`, but `fetchFreshProjectDiagnostics`
|
|
2349
|
+
(`project-diagnostics/fresh-fetch.ts`) never read that cache back, so test
|
|
2350
|
+
failures never reached the agent for unedited/project-wide `mode=full`
|
|
2351
|
+
calls. Unlike opengrep/gitleaks/trivy, this is wired as a CACHE-READ (not a
|
|
2352
|
+
fresh scan): test-runner has no "whole project" run to trigger — turn_end
|
|
2353
|
+
only ever runs the targeted, cascade-aware test files touched by that
|
|
2354
|
+
turn's edits — so `fetchFreshProjectDiagnostics` now peeks at the same
|
|
2355
|
+
`"test-runner-findings"` cache key (never re-running the suite, never
|
|
2356
|
+
writing back) and adapts it via the existing
|
|
2357
|
+
`testRunnerFindingsToProjectDiagnostics`. Added a coverage guardrail
|
|
2358
|
+
(`tests/clients/project-diagnostics/analyzer-coverage.test.ts`) that greps
|
|
2359
|
+
the real session-start (`runHeavyweightTask`) and EVERY turn_end
|
|
2360
|
+
`cacheManager.writeCache` call site in `runtime-turn.ts` (excluding a
|
|
2361
|
+
short, explicit bookkeeping exclude-list — `errorDebt`/`turn-end-findings`/
|
|
2362
|
+
`turn-end-findings-last` — that isn't analyzer-shaped) and asserts every
|
|
2363
|
+
remaining id is a member of `ANALYZER_IDS`, so this whole #585 bug class
|
|
2364
|
+
can't silently regress again for a future analyzer added to EITHER writer.
|
|
2365
|
+
Adversarial review also caught two honesty/fidelity gaps in the initial
|
|
2366
|
+
fix: (1) test-runner is cache-read, edit-scoped (the targeted test files
|
|
2367
|
+
touched by the most recent turn's edits), unlike every other analyzer's
|
|
2368
|
+
fresh whole-project scan — `mode=full` now emits an explicit "coverage is
|
|
2369
|
+
edit-scoped, NOT a full-project run" caveat whenever test-runner
|
|
2370
|
+
contributed findings (not only when cold), and a `stale` cached result
|
|
2371
|
+
(the turn advanced before the test run finished) is prefixed
|
|
2372
|
+
`[stale — from a prior turn]` in its diagnostic message, matching the
|
|
2373
|
+
one-shot turn-context message's own stale wording; (2) `TestFailure.location`
|
|
2374
|
+
(`"file.ts:42"`, set by the vitest/jest JSON parser in
|
|
2375
|
+
`test-runner-client.ts`) never reached `ProjectDiagnostic.line`, so every
|
|
2376
|
+
test-runner finding rendered as `L?:` — `runner-adapters/runner-findings.ts`
|
|
2377
|
+
now parses a numeric trailing `:line[:col]` out of `location` (left alone
|
|
2378
|
+
for pytest/mix's non-numeric `location` strings, which carry a test name or
|
|
2379
|
+
module, not a line).
|
|
2380
|
+
|
|
2381
|
+
- **Guarded Windows CPU/RSS resource sampling so a best-effort sampler can no longer crash the pi host (refs #620, #533)** — `clients/resource-sampler.ts`
|
|
2382
|
+
sampled CPU%/RSS via `pidusage`, whose Windows path shells out to `gwmi`
|
|
2383
|
+
through an internal `spawn(..., { shell: "powershell.exe" })` that has no
|
|
2384
|
+
try/catch and runs from inside a ChildProcess `close` callback. Under real
|
|
2385
|
+
Windows handle/commit pressure that spawn can throw `spawn UNKNOWN`
|
|
2386
|
+
(errno -4094) **synchronously in that detached callback**, which the call
|
|
2387
|
+
site's `try { await pidusage() } catch {}` cannot catch → `uncaughtException`
|
|
2388
|
+
→ the whole host process dies (observed live). pidusage 4.0.1 offers no
|
|
2389
|
+
option to avoid the gwmi path. The sampler now runs its OWN fully guarded
|
|
2390
|
+
`Get-CimInstance Win32_Process` query on Windows (never calling `pidusage`
|
|
2391
|
+
there), mirroring the existing `findDescendantPidsWindows` guard: a
|
|
2392
|
+
synchronous spawn throw, a child `error` event, and a non-zero/garbage exit
|
|
2393
|
+
all resolve to a partial/empty map — the sampler can now only ever lose a
|
|
2394
|
+
data point, never throw into the heartbeat/spawn path. RSS comes from
|
|
2395
|
+
`WorkingSetSize`; CPU% is preserved via the same KernelModeTime/UserModeTime
|
|
2396
|
+
delta-over-elapsed-wall-time computation gwmi uses (a small per-pid history
|
|
2397
|
+
tracks the prior cumulative CPU time). Linux/macOS keep using `pidusage`
|
|
2398
|
+
(procfs/`ps` — not the crash vector) unchanged. A pid absent from the
|
|
2399
|
+
returned map still means "unsampled this tick", never zero.
|
|
2400
|
+
|
|
2401
|
+
- **Stabilized the two remaining flaky/environment-sensitive tests tracked in #902.** LSP workspace-diagnostics sweep flush-count assertions
|
|
2402
|
+
(`workspace-diagnostics-sweep-batch-open.test.ts`,
|
|
2403
|
+
`workspace-diagnostics-sweep-preopen-chunk.test.ts`) pinned an exact flush
|
|
2404
|
+
count, but the pre-open pass's real `fs.promises.readFile` genuinely races
|
|
2405
|
+
the real 100ms `WatchedFilesQueue` debounce timer — under full-suite
|
|
2406
|
+
scheduler contention that can legitimately fragment or merge a chunk's
|
|
2407
|
+
flush by one. Replaced the exact-count assertions with invariant checks
|
|
2408
|
+
(coalescing happened; nowhere near one flush per file; no lost/duplicated
|
|
2409
|
+
URIs) that tolerate that jitter without losing the #608/#621 regression
|
|
2410
|
+
coverage — a test-robustness fix, not a product bug. Separately, two
|
|
2411
|
+
ast-grep dispatch test harnesses
|
|
2412
|
+
(`ast-grep-rule-tests.test.ts`, `ast-grep-catalog-rules.test.ts`) shelled
|
|
2413
|
+
out to the real `ast-grep` CLI via a raw `execFileSync(..., { shell: true
|
|
2414
|
+
})` per call — an uncached cmd.exe wrapper on Windows whose own exit code
|
|
2415
|
+
can mask the real child's and which can intermittently fail to spawn at
|
|
2416
|
+
all under the process-creation pressure of a full parallel test run.
|
|
2417
|
+
Switched both to the already-hardened `safeSpawn` (`clients/safe-spawn.ts`,
|
|
2418
|
+
#817) — cached PATH+PATHEXT resolution, direct `.exe`/`.com` spawn with no
|
|
2419
|
+
shell involved — the same deterministic resolution production dispatch
|
|
2420
|
+
already relies on. The third tracked item
|
|
2421
|
+
(`startup-overhead.test.ts`'s "quick mode self-reports within 100ms")
|
|
2422
|
+
was already reworked to a generous fixed budget + `retry: 2` in an earlier
|
|
2423
|
+
pass; confirmed still adequate, no further change needed.
|
|
840
2424
|
|
|
841
2425
|
- **`pi-lens build-graph` honestly reports a capped, over-the-cap persist**
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
2426
|
+
(closes #924, refs #936 limit 3) — the CLI's build-attempt check no longer
|
|
2427
|
+
mistakes the benign "succeeded, but persisted a partial subgraph" reason
|
|
2428
|
+
(#960's over-cap circuit-breaker) for a hard failure: it now exits 0 and
|
|
2429
|
+
prints a `PARTIAL persist` line with `persistedNodes=X/Y`,
|
|
2430
|
+
`persistedEdges=X/Y`, and the cap that was hit, instead of either silently
|
|
2431
|
+
reporting "done" or failing loudly on a build/persist that actually
|
|
2432
|
+
succeeded. `flushReviewGraphPersist()` now returns the persisted
|
|
2433
|
+
`coverage` so any standalone caller can make the same distinction.
|
|
850
2434
|
|
|
851
2435
|
- **`ts-ssrf` no longer trusts naming convention as proof of a fixed URL**
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
2436
|
+
(fixes #963) — the post-filter now resolves a bare `fetch(IDENT)` argument
|
|
2437
|
+
against the file's AST and exempts it only when `IDENT` provably resolves
|
|
2438
|
+
to a single `const` declarator initialized with a string literal (or a
|
|
2439
|
+
template literal with no `${...}` substitutions). SCREAMING_SNAKE_CASE
|
|
2440
|
+
naming is no longer sufficient by itself: `const TARGET_URL = req.query.url`
|
|
2441
|
+
still flags, since the identifier's *initializer* — not its name — is
|
|
2442
|
+
what's checked. Ambiguous/shadowed declarations, `let`/`var` bindings,
|
|
2443
|
+
reassigned identifiers, and unresolved identifiers all fall through to the
|
|
2444
|
+
existing broad heuristic and keep being flagged.
|
|
861
2445
|
|
|
862
2446
|
- **Remaining test-suite spawns routed off `shell:true` to close the Windows-spawn flake class** (refs #902) —
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
2447
|
+
`tests/clients/ast-grep-rule-precedence-followups.test.ts` (CLI probe + `runCli`),
|
|
2448
|
+
`tests/clients/coderabbit-ast-grep-rules.test.ts` (vendored-catalog smoke),
|
|
2449
|
+
`tests/clients/dispatch/runners/ast-grep-playground-verify.test.ts` (async `runVerify`),
|
|
2450
|
+
`tests/clients/dead-code-client.test.ts` (vulture probe), and
|
|
2451
|
+
`tests/clients/lsp/clangd-lazy-indexing.test.ts` (clangd `where`/`which` probe) now spawn
|
|
2452
|
+
through the hardened `safeSpawn`/`safeSpawnAsync` (`clients/safe-spawn.ts`, #817) instead of
|
|
2453
|
+
a raw `execFileSync`/`spawnSync`/`spawn`/`execSync` with a fresh, uncached `cmd.exe` wrapper
|
|
2454
|
+
per call — the documented intermittent ENOENT/EAGAIN source under windows-latest CI's
|
|
2455
|
+
parallel process-creation pressure. Completes the sweep PR #993 started for the two
|
|
2456
|
+
ast-grep runner test files; `git` spawns are left as-is (no `.cmd` shim, so out of the flake
|
|
2457
|
+
class) and the `mcp/*` and MCP harness spawns that need bidirectional stdin streaming are
|
|
2458
|
+
left as-is (`safeSpawnAsync` only exposes a close-based result, not a live stdin/stdout pipe).
|
|
875
2459
|
|
|
876
2460
|
- **Tree-sitter post-filters no longer leave silently dead rules** (refs #879) —
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
2461
|
+
the 25 unknown filter references were resolved by implementing eight bounded,
|
|
2462
|
+
fail-open-on-filter-error AST checks, expressing two conditions directly in
|
|
2463
|
+
their queries, and removing fifteen rules whose promised semantic/framework
|
|
2464
|
+
analysis could not be supported honestly. Coarse resource, assertion, and
|
|
2465
|
+
sensitive-`memset` heuristics are advisory rather than blocking.
|
|
882
2466
|
|
|
883
2467
|
- **Capped word indexes disclose partial coverage** (refs #928) — snapshots now
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
2468
|
+
persist indexed-file count and truncation state, and both `symbol_search`
|
|
2469
|
+
surfaces report coverage instead of presenting capped zero-hit results as
|
|
2470
|
+
authoritative.
|
|
887
2471
|
|
|
888
2472
|
- **`pilens_health` keeps disabled LSP servers visible** (refs #927) —
|
|
889
|
-
|
|
890
|
-
|
|
2473
|
+
permanently broken server/root pairs now render with their failure count, and
|
|
2474
|
+
temporary circuit-breaker cooldowns expose their retry deadline.
|
|
891
2475
|
|
|
892
2476
|
- **Cold `symbol_search` failures are now observable and honest** (refs #926) —
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
2477
|
+
unavailable results distinguish an active build, a safety refusal, and the
|
|
2478
|
+
last build's failure, while background build/persist errors reach a persistent
|
|
2479
|
+
NDJSON log.
|
|
896
2480
|
|
|
897
2481
|
- **Failed heavyweight analyzers no longer masquerade as clean runs** (refs #925) —
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
- **The footer refreshes as LSP servers come online during a cold
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
ES module and therefore reuses its corrupted Emscripten heap; restarting the
|
|
914
|
-
host is the isolation boundary.
|
|
2482
|
+
unsuccessful results are reported distinctly, omitted from cache so the next
|
|
2483
|
+
session retries, and valid fix-worklog records survive neighboring corrupt lines.
|
|
2484
|
+
|
|
2485
|
+
- **The footer refreshes as LSP servers come online during a cold `lens_diagnostics mode=full` sweep (refs #798)**, instead of showing
|
|
2486
|
+
`LSP Inactive` until turn end. The repaint captures UI methods during the
|
|
2487
|
+
active tool event, so async warm-up never touches a stale session context.
|
|
2488
|
+
|
|
2489
|
+
- **Tree-sitter WASM aborts are now visible instead of silently disabling structural analysis for the rest of the process (refs #915)**. The shared
|
|
2490
|
+
runtime records a process-wide, timestamped `restart_required` health state,
|
|
2491
|
+
logs a one-time actionable error, exposes it through `pilens_health`, and
|
|
2492
|
+
marks project-scan responses with `treeSitterStatus`. A poisoned scan remains
|
|
2493
|
+
truncated and never replaces the last complete snapshot. In-process retry is
|
|
2494
|
+
deliberately unsafe: every new client imports the same cached `web-tree-sitter`
|
|
2495
|
+
ES module and therefore reuses its corrupted Emscripten heap; restarting the
|
|
2496
|
+
host is the isolation boundary.
|
|
915
2497
|
|
|
916
2498
|
- **`pilens_rebuild` can no longer destroy an npm-installed pi-lens** (refs
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
2499
|
+
#920) — rebuilds are refused before spawning a package script unless the
|
|
2500
|
+
package is a source checkout with `tsconfig.dist.json` outside
|
|
2501
|
+
`node_modules`; installed servers also omit the tool from `tools/list`, so
|
|
2502
|
+
subagent allowlists cannot discover it.
|
|
921
2503
|
|
|
922
2504
|
- **Review-graph background failures are no longer silent** (refs #919) —
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
2505
|
+
`project_report`/`pilens_project_report` now surface the most recent build
|
|
2506
|
+
attempt and its terminal skip/failure reason, including the persistence
|
|
2507
|
+
element circuit-breaker. A dedicated `~/.pi-lens/review-graph.log` records
|
|
2508
|
+
build and persistence starts, successes, skips, and failures, so a rejected
|
|
2509
|
+
fire-and-forget build or an over-cap graph cannot look perpetually in progress.
|
|
928
2510
|
|
|
929
2511
|
- Resolve nested C# and F# project roots for dotnet builds (refs #895).
|
|
930
2512
|
|
|
931
|
-
- **A mid-scan tree-sitter WASM abort no longer replaces the authoritative
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
file counts and abort point, and leaves the previous complete cache intact.
|
|
2513
|
+
- **A mid-scan tree-sitter WASM abort no longer replaces the authoritative project-diagnostics snapshot with a silently truncated result (refs #891)**.
|
|
2514
|
+
The partial scan is returned with `scanTruncated`, logs its completed/total
|
|
2515
|
+
file counts and abort point, and leaves the previous complete cache intact.
|
|
935
2516
|
|
|
936
2517
|
- **Block-wrapped switch cases no longer report false fall-through errors**
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
2518
|
+
(refs #910) — `switch-case-termination` now follows trailing statement
|
|
2519
|
+
blocks to recognize a nested `break`, `return`, `throw`, or `continue`, while
|
|
2520
|
+
still flagging empty and non-terminating blocks.
|
|
940
2521
|
|
|
941
2522
|
- **Project-wide enumeration now covers every registered file kind** (refs
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
2523
|
+
#894) — `ALL_SCANNABLE_EXTENSIONS`, `WARMUP_SOURCE_EXTS`, and
|
|
2524
|
+
`SUPPORTED_FILE_KINDS` now derive from the single `KIND_EXTENSIONS`
|
|
2525
|
+
authority instead of three drifting language lists. TODO scans, symbol
|
|
2526
|
+
search indexing, dominant-language LSP warmup, and language-profile
|
|
2527
|
+
detection can now see Java, Swift, C/C++, PHP, and every other supported
|
|
2528
|
+
kind. Code kinds keep priority over data/doc kinds (json/yaml/markdown/…)
|
|
2529
|
+
inside the existing caps: the dominant-language LSP warm ranks code kinds
|
|
2530
|
+
first, and the capped warmup/word-index walks fill code files before
|
|
2531
|
+
non-code files, so a locale/fixture pile can't starve real languages.
|
|
2532
|
+
Package-manager lockfiles (package-lock.json, pnpm-lock.yaml, …) are
|
|
2533
|
+
filtered as generated artifacts, and the TODO scanner caps per-file reads
|
|
2534
|
+
at 512 KiB. A coverage guard makes a newly registered kind automatically
|
|
2535
|
+
enumerable — and classified as code or non-code — on both project-wide
|
|
2536
|
+
paths.
|
|
956
2537
|
|
|
957
2538
|
- **CMake files now reach a real LSP server** (refs #892) — the CMake policy's
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
2539
|
+
previous `lsp` fallback had no registered server and silently produced no
|
|
2540
|
+
diagnostics. `cmake-language-server` now covers both `.cmake` files and the
|
|
2541
|
+
canonical `CMakeLists.txt` basename, with managed pip installation.
|
|
961
2542
|
|
|
962
2543
|
- **Fish LSP policy is no longer dead wiring** (refs #893) — `fish-lsp` is now
|
|
963
|
-
|
|
964
|
-
|
|
2544
|
+
registered for `.fish` files and auto-installed through npm; `fish_indent`
|
|
2545
|
+
continues to run alongside it.
|
|
965
2546
|
|
|
966
2547
|
- **Editing an inherited tree-sitter rule now invalidates the inheriting language's RuleCache** (refs #878) — the cache key fingerprinted only the language's OWN rules directory, but `tsx` also runs the `typescript` rule set (`queriesForLanguage`), so a typescript-rule edit left the tsx entry's hash unchanged and stale compiled rules kept being served from the on-disk cache until a tsx rule happened to change. The fingerprint now covers the full effective rule set via `ruleFilesForLanguage`, a new loader-owned seam that derives from the same rule-source composition as rule selection, so the cache key can't drift from what the runner actually runs. The runner's cache-miss path also forces the query loader past its in-memory memo (`loadQueries(root, { force: true })`) — a correct key alone wasn't enough: the memo ignores rule-file mtimes, so within one process a miss re-persisted the PRE-edit rules under the fresh fingerprint and the staleness then survived restarts. The client's compiled-batch cache is likewise now keyed on rule content instead of rule ids — ids are stable across edits, so the shared client kept serving the pre-edit compiled patterns (and messages) for the process lifetime even after the reload. `CACHE_VERSION` bumped to `v6`.
|
|
967
2548
|
|
|
968
2549
|
- **Small edits no longer pay the entity-extraction cost** (refs #885) — the
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
2550
|
+
<5-line skip threshold only guarded the zero-diagnostics early return; a
|
|
2551
|
+
second `extractEntitySnapshot` block ran unconditionally, so trivial edits
|
|
2552
|
+
still spent ~500-800ms per dispatch and, on unsaved buffers, parsed stale
|
|
2553
|
+
disk content (thrashing the parse-cache entry). Extraction is now one
|
|
2554
|
+
threshold-guarded block that receives the same `file.content` override the
|
|
2555
|
+
diagnostics phase used.
|
|
975
2556
|
|
|
976
2557
|
- **The per-edit tree-sitter runner walks the tree once, not once per rule**
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
2558
|
+
(refs #888) — the dispatch hot path ran ~30-40 `runQueryOnFile` walks per
|
|
2559
|
+
edit behind a concurrency limiter that could not parallelize synchronous
|
|
2560
|
+
WASM. It now calls `runQueriesOnFile` once (#675 batching) and distributes
|
|
2561
|
+
the per-rule results; the per-rule `maxResults(10)` cap and modified-ranges
|
|
2562
|
+
gating are unchanged.
|
|
982
2563
|
|
|
983
2564
|
- **A transient grammar-load failure no longer disables batched rule scans for the process lifetime** (refs #889) — `compileQueryBatch` cached `null` on ANY `build()` failure, including a transient `loadLanguage()` miss (offline lazy grammar fetch, mid-scan load error), so every later scan fell back to the per-rule path and paid ~3.3× forever. Load failures are now distinguished from genuine batch-compile failures: they are not cached (the next scan retries the load and recovers), with retries bounded at 3 consecutive load failures per rule set before the miss is cached, so a grammar that never loads doesn't hot-loop. Deterministic compile failures still cache `null` permanently. The query/batch cache keys also switched from a collision-prone 32-bit hash to the full pattern text.
|
|
984
2565
|
|
|
985
2566
|
- **module-report parses plain JS under the correct tree-sitter grammar** (closes
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
2567
|
+
#887) — `tsLangForFile` hand-rolled a local extension map that sent
|
|
2568
|
+
`.js`/`.mjs`/`.cjs` to the typescript grammar and `.jsx` to tsx, while every
|
|
2569
|
+
other tree-sitter consumer resolves those extensions to `javascript` via the
|
|
2570
|
+
shared `EXT_TO_LANG` (`clients/tree-sitter-shared.ts`), so each plain-JS file
|
|
2571
|
+
was parsed and cached twice under two grammars (TreeCache keys are
|
|
2572
|
+
`languageId:path`) and ran TS-grammar symbol queries on JS trees. It now
|
|
2573
|
+
routes the extension-split kinds (jsts, and the c-vs-cpp split for cxx)
|
|
2574
|
+
through the shared `resolveTreeSitterLanguage`, keeping the historical kind
|
|
2575
|
+
default only for extensions the shared map does not cover (`.svelte`/`.vue`,
|
|
2576
|
+
the C++ module-interface/Objective-C tail). The symbol extractor gains a
|
|
2577
|
+
dedicated `javascript` defs/refs/import query set: the TypeScript symbol
|
|
2578
|
+
queries do NOT compile against the javascript grammar
|
|
2579
|
+
(`interface_declaration`/`type_alias_declaration`/`type_identifier` do not
|
|
2580
|
+
exist there — a query naming them fails with "Bad node name"), so the
|
|
2581
|
+
javascript set is the same queries minus the type-only patterns, with class
|
|
2582
|
+
names matched as `(identifier)` instead of `(type_identifier)`.
|
|
1002
2583
|
|
|
1003
2584
|
- **A rule whose query fails to compile now warns once instead of silently reporting nothing** (refs #884) — both compile paths (`compileQueryBatch`'s per-rule drop and `compileRawQuery`, which every per-edit `runQueryOnFile` call falls back on) previously either `dbg()`-logged (invisible without verbose mode) or returned `null`/`[]` with no trail at all. They now call a shared `reportQueryCompileFailure`, mirroring the existing unimplemented-`post_filter` warning: one `console.error` per broken rule id, not per file. A new compile-guard test (`tests/clients/tree-sitter-rule-compile-guard.test.ts`) compiles every non-disabled shipped rule against its real grammar and caught the 32 rules #884 reports as currently broken (tracked there in a shrink-only `KNOWN_BROKEN` allowlist so follow-up fix PRs are forced to remove their entries, and the list can't grow or go stale unnoticed).
|
|
1004
2585
|
|
|
1005
2586
|
- **TreeCache mtime false misses and FIFO eviction** (closes #890) — a content
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
- **Eight enabled typescript/javascript tree-sitter rules whose queries never
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
`no_terminating_statement`), which the batch runner had been failing closed on.
|
|
2587
|
+
hash match is now authoritative: a save-without-change (same bytes, newer
|
|
2588
|
+
mtime) is a cache hit that refreshes the entry's stat metadata instead of
|
|
2589
|
+
invalidating and re-parsing, and `get()` re-inserts hit entries so eviction
|
|
2590
|
+
is true LRU — hot per-edit files are no longer evicted by scan traffic.
|
|
2591
|
+
|
|
2592
|
+
- **Eight enabled typescript/javascript tree-sitter rules whose queries never compiled (refs #884)** — each had been silently dead since authoring because
|
|
2593
|
+
its query failed to compile against the real grammar, so it matched nothing
|
|
2594
|
+
and reported no diagnostics. Repaired against the actual node/field names and
|
|
2595
|
+
verified end-to-end (matches the bug, leaves correct code alone):
|
|
2596
|
+
`empty-switch-case`, `switch-case-termination`, `switch-case-termination-js`
|
|
2597
|
+
(switch cases carry their statements as direct `body:` children, not a
|
|
2598
|
+
`consequence: (statement_block)`); `infinite-loop` (`while (true)` wraps the
|
|
2599
|
+
condition in a `parenthesized_expression`; `for (;;)` has an `empty_statement`
|
|
2600
|
+
condition, not `(null)`); `duplicate-function-arg` (typescript parameters are
|
|
2601
|
+
`required_parameter`, not bare `(identifier)`; now also catches non-adjacent
|
|
2602
|
+
duplicates); `mixed-async-styles` (no `async_modifier` node — match the `async`
|
|
2603
|
+
token); `switch-non-case-labels` (JS) (a `labeled_statement` lives inside a
|
|
2604
|
+
`switch_case`, not directly under `switch_body`); and `ts-insecure-random`
|
|
2605
|
+
(the inline `(?i)` regex flag is invalid in JS `RegExp` — dropped the
|
|
2606
|
+
redundant name predicate and let the post-filter do the case-insensitive
|
|
2607
|
+
check, walking up from the `Math.random()` call so chained forms like
|
|
2608
|
+
`Math.random().toString(36)` are still attributed to their binding). Also
|
|
2609
|
+
implemented the four post-filters these rules referenced but that were never
|
|
2610
|
+
defined (`is_empty_block`, `no_break_or_return_in_body`, `same_param_name`,
|
|
2611
|
+
`no_terminating_statement`), which the batch runner had been failing closed on.
|
|
1032
2612
|
|
|
1033
2613
|
- **A column-0 comment after a `query: |` block no longer breaks the rule**
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
2614
|
+
(refs #884) — the query-block extractor kept every line more-indented than the
|
|
2615
|
+
key (to preserve `#eq?`/`#match?` predicate lines) but did not stop at a
|
|
2616
|
+
document-level `# …` comment sitting between the block and the next key, so the
|
|
2617
|
+
comment was appended to the query and made it fail to compile. This is what
|
|
2618
|
+
kept `mixed-async-styles` dead even after its query was otherwise correct.
|
|
1039
2619
|
|
|
1040
2620
|
- **Seven ruby security rules never compiled and never produced a finding** (refs
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
2621
|
+
#884) — `ruby-command-injection`, `ruby-eval`, `ruby-insecure-deserialization`,
|
|
2622
|
+
`ruby-insecure-random`, `ruby-open-struct`, `ruby-string-eval` and
|
|
2623
|
+
`ruby-weak-hash` were authored against JavaScript-grammar node names
|
|
2624
|
+
(`call_expression`, `method_call`, `command`, `interpolated_string`) that do not
|
|
2625
|
+
exist in tree-sitter-ruby, so every one failed to compile with a "Bad node name"
|
|
2626
|
+
error and was silently skipped. Each query is rewritten against the real ruby
|
|
2627
|
+
grammar (`call` with `method:`/`receiver:` fields, `scope_resolution` receivers
|
|
2628
|
+
for `Digest::MD5`, `string` for interpolated literals), with `#match?`/`#eq?`
|
|
2629
|
+
predicates moved inside the pattern's outermost parens. Per-rule positive and
|
|
2630
|
+
negative fixtures (`system` vs `File.read`, `Marshal.load` vs `YAML.safe_load`,
|
|
2631
|
+
`rand` vs `SecureRandom`, `OpenStruct.new` vs `Struct.new`, `Digest::MD5` vs
|
|
2632
|
+
`Digest::SHA256`, string vs block `class_eval`) pin the security intent.
|
|
1053
2633
|
|
|
1054
2634
|
- **Six python rules never compiled or never fired** (refs #884) —
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
2635
|
+
`python-empty-except` used a `body:` field that doesn't exist on
|
|
2636
|
+
`except_clause`; `in-operator-unsupported` used bare `"in"`/`"not"` `"in"`
|
|
2637
|
+
tokens the grammar doesn't expose that way (`not in` is a single token) and
|
|
2638
|
+
only matched `identifier` targets, so `x in None` never matched at all;
|
|
2639
|
+
`no-super-torchscript` looked for a `(call function: (identifier))` inside the
|
|
2640
|
+
decorator (`@torch.jit.script` has no call — it's a bare `attribute`) and
|
|
2641
|
+
expected the decorator directly on the method rather than the class;
|
|
2642
|
+
`notimplemented-boolean-context` used `("and" | "or")` (not valid tree-sitter
|
|
2643
|
+
query syntax — alternation is `[...]`) inside a non-existent `binary_operator`
|
|
2644
|
+
form and `unary_operator operator: ("not")`, when python's logical `not` is
|
|
2645
|
+
its own `not_operator` node with an `argument:` field;
|
|
2646
|
+
`yield-return-outside-function` referenced a `yield_expression` node type that
|
|
2647
|
+
doesn't exist in tree-sitter-python. All five queries are rewritten against
|
|
2648
|
+
the real grammar (verified via AST dumps against `tree-sitter-python.wasm`);
|
|
2649
|
+
`no-super-torchscript` and `in-operator-unsupported` gained
|
|
2650
|
+
`torchscript_super_call` / `check_in_operator_types` post-filters
|
|
2651
|
+
(`clients/tree-sitter-client.ts`) since neither had a working implementation
|
|
2652
|
+
behind their declared `post_filter` name. `exit-signature-check` compiled
|
|
2653
|
+
fine but its `@PARAM1?`/`@PARAM2?`/`@PARAM3?` captures never matched anything
|
|
2654
|
+
— the `?` quantifier was written after the capture name instead of the node,
|
|
2655
|
+
so the captured names were literally `PARAM1?`/`PARAM2?`/`PARAM3?` while the
|
|
2656
|
+
post_filter read `captures.PARAM1`; fixed to capture the whole `parameters`
|
|
2657
|
+
node and count named children instead (per-slot optional quantifiers turned
|
|
2658
|
+
out to match every valid sub-alignment, not just the maximal one, producing
|
|
2659
|
+
spurious duplicate matches for a fully-correct signature).
|
|
1080
2660
|
|
|
1081
2661
|
- **Project scans run tree-sitter rules for every supported language, not just 10 extensions** (closes #882, refs #877, #880) — the scanner's `TREE_SITTER_EXT_TO_LANG` covered only ts/tsx/js/py/go/rs/rb, so files whose grammars and non-disabled rule dirs already exist (c, cpp, csharp, css, php, java, kotlin) were silently skipped by the tree-sitter phase of project scans. It now derives the shared per-edit resolver (`EXT_TO_LANG`) so c/cpp/csharp/php/css and the `.tsx`→tsx / `.jsx`→javascript nuances can't drift from the per-edit path, and layers java/kotlin on top (grammars + rule dirs exist but no per-edit `appliesTo`). A regression test asserts the map covers every non-disabled rule dir whose grammar is loadable.
|
|
1082
2662
|
|
|
1083
2663
|
- **`.dart` files are now included in project-wide source enumeration** (closes #880, refs #876) — `ALL_SCANNABLE_EXTENSIONS` (`clients/source-filter.ts`) and `WARMUP_SOURCE_EXTS` (`clients/language-profile.ts`) were missing `.dart`, so Dart projects were fully supported per-edit (LSP, `dart-analyze`, `dart format`, autofix) but skipped by project-wide scans and cold-start language-profile warmup.
|
|
1084
2664
|
|
|
1085
2665
|
- **Tree-sitter rules were compiled against the wrong grammar** — a compiled
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
2666
|
+
query is bound to the language it compiled against, and running it on a tree
|
|
2667
|
+
from another grammar returns zero matches silently. Rules now compile against
|
|
2668
|
+
the language the file is parsed as. This is why the javascript→typescript rule
|
|
2669
|
+
merge never fired a single diagnostic in its life. That merge stays off — the
|
|
2670
|
+
rules are written against typescript node types and misfire on JS — but the
|
|
2671
|
+
same fix is what makes `.tsx` work.
|
|
1092
2672
|
|
|
1093
2673
|
- **Project scans ran rules from `-disabled/` directories** — the per-edit runner
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
2674
|
+
excluded them, the scanner read the raw loader map and ran them anyway.
|
|
2675
|
+
**1,936 of a 500-file scan's 2,590 tree-sitter findings came from rules that
|
|
2676
|
+
were explicitly switched off.** Rule-set selection now goes through one seam,
|
|
2677
|
+
`queriesForLanguage`.
|
|
1098
2678
|
|
|
1099
2679
|
- **`.tsx` parsed with the typescript grammar**, which produces ERROR nodes on
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
2680
|
+
JSX, and under a different language id than the fact providers use — so every
|
|
2681
|
+
`.tsx` file was parsed twice, once wrongly. It now parses as `tsx` and
|
|
2682
|
+
inherits the typescript rule set, which is rule-for-rule identical on both
|
|
2683
|
+
grammars. `.tsx` also gets those rules in the per-edit runner now, where it
|
|
2684
|
+
previously saw only its own two JSX rules.
|
|
1105
2685
|
|
|
1106
2686
|
- **An unimplemented `post_filter` reported every raw match instead of none.**
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
2687
|
+
40 of the 84 filters rules reference have no implementation; the default now
|
|
2688
|
+
drops the match and warns once. `duplicate-function-arg` (whose
|
|
2689
|
+
`same_param_name` filter was never implemented) was reporting 59 phantom
|
|
2690
|
+
duplicates across 60 files.
|
|
1111
2691
|
|
|
1112
2692
|
- **YAML scalars kept their trailing comments** in the rule loader's hand-rolled
|
|
1113
|
-
|
|
1114
|
-
|
|
2693
|
+
parser — `post_filter: not_in_test_block # skip test blocks` carried the
|
|
2694
|
+
comment as part of the filter name, so the filter silently never applied.
|
|
1115
2695
|
|
|
1116
|
-
- **tree-sitter runner no longer double-increments already 1-indexed
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
one-time, not a regression.
|
|
2696
|
+
- **tree-sitter runner no longer double-increments already 1-indexed line/column (refs #448)** — dispositions recorded against the old off-by-one
|
|
2697
|
+
are anchored (`clients/diagnostic-dispositions.ts`) to the wrong physical
|
|
2698
|
+
line's content and will resurface once after upgrading; this is expected and
|
|
2699
|
+
one-time, not a regression.
|
|
1121
2700
|
|
|
1122
2701
|
### Security
|
|
1123
2702
|
|
|
1124
2703
|
- **Debug logs redact credential-shaped text before it reaches disk** (closes
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
2704
|
+
#327). The shared NDJSON boundary scrubs private keys, provider tokens, AWS
|
|
2705
|
+
access keys, and JWT/JWE compact tokens with deterministic linear scanners.
|
|
2706
|
+
The synchronous crash-adjacent LSP launch diagnostic uses the same redactor.
|
|
1128
2707
|
|
|
1129
2708
|
## [3.8.73] - 2026-07-28
|
|
1130
2709
|
|
|
@@ -1133,15 +2712,15 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
1133
2712
|
### Changed
|
|
1134
2713
|
|
|
1135
2714
|
- **`@types/js-yaml` removed; js-yaml 5 migration complete** (closes #391) — with
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
2715
|
+
js-yaml 5.2.2 in (#843) the package ships its own declarations, so the stub
|
|
2716
|
+
types dev-dependency is gone and the lockfile regenerated. This completes the
|
|
2717
|
+
v5 migration started with the namespace-import compatibility pass below.
|
|
1139
2718
|
|
|
1140
2719
|
- **`js-yaml` imports are compatible with v5** (refs #391, #843) — use the
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
2720
|
+
package's namespace exports so the central accessor and YAML rule tooling work
|
|
2721
|
+
with both the current v4 dependency and the upcoming v5 release. `@types/js-yaml`
|
|
2722
|
+
remains temporarily because js-yaml 4.3.0 needs its declarations; remove it
|
|
2723
|
+
with the v5 Dependabot upgrade once the bundled declarations are active.
|
|
1145
2724
|
|
|
1146
2725
|
- **`build:dist` now compiles with TypeScript 7.0.2** (refs #809) — the dist emit pin moves from `typescript@6` to the same major as the dev dependency (#600); emitted output was verified byte-identical across all 328 files before the bump.
|
|
1147
2726
|
|
|
@@ -1154,17 +2733,17 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
1154
2733
|
- **Missing tree-sitter defect-class metadata is now covered** (refs #863) — loop-termination and `finally` control-flow rules now declare their taxonomy classes, keeping the rule audit complete.
|
|
1155
2734
|
|
|
1156
2735
|
- **MCP smoke deadlines scale under CI load (refs #860)** — the shared MCP
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
2736
|
+
test harness keeps its 20s local default, accepts the
|
|
2737
|
+
`PI_LENS_TEST_TIMEOUT_SCALE` multiplier, and the analyze-graph smoke pays
|
|
2738
|
+
server startup through a throwaway health call before its assertions.
|
|
1160
2739
|
|
|
1161
2740
|
- **TypeScript 7 compatibility fixes (refs #809)** — narrowed ast-grep search test details and made the LSP `workspace/applyEdit` response overload-compatible, unblocking Dependabot PR #600.
|
|
1162
2741
|
|
|
1163
2742
|
- **ast-grep NAPI fallback now runs TSX-tagged rules and reports unsupported-language skips** (refs #282) — `.tsx` files use the `tsx` grammar for rule-language scoping, preserving the exact TypeScript/JavaScript twin behavior from #657; rules for languages the fallback cannot evaluate are logged once per rule instead of disappearing as zero matches.
|
|
1164
2743
|
|
|
1165
2744
|
- **CSS server root-policy coverage is hermetic on Windows** (refs #855) — the
|
|
1166
|
-
|
|
1167
|
-
|
|
2745
|
+
server-policy fixture now pins its nearest `package.json` inside the temporary
|
|
2746
|
+
workspace, so a marker in the host user's profile cannot change the result.
|
|
1168
2747
|
|
|
1169
2748
|
- **Multi-client LSP shutdown no longer serializes the grace-period tail** (#851) — retiring clients now tear down concurrently while generation handoff still waits for every client to settle; instance-registry child removals serialize their read-modify-write without serializing process kills.
|
|
1170
2749
|
|
|
@@ -1368,6 +2947,8 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
1368
2947
|
- **`fact-rules` diagnostics now stamp `tool: "fact-rules"` instead of their own rule id, fixing scattered turn-summary grouping** (#578) — `clients/dispatch/types.ts`'s `Diagnostic` contract documents `tool` as "which runner produced this" and `rule` as "the specific check within it"; `ast-grep`'s runner follows it correctly (one `tool: "ast-grep"` value shared across every pattern), but all 13 rules under `clients/dispatch/rules/*.ts` (the `fact-rules` runner) stamped their own rule id into `tool` instead (`{ tool: "high-fan-out", rule: "high-fan-out" }`), so `clients/turn-summary.ts`'s collapsed one-liner — which groups by `event.tool` — scattered N fact-rule findings into N separate per-rule-name buckets instead of one clean `fact-rules N` bucket like every other runner. Fixed at the emission source in all 13 files (`async-noise.ts`, `async-unnecessary-wrapper.ts`, `cors-wildcard.ts`, `error-obscuring.ts`, `error-swallowing.ts`, `high-complexity.ts`, `high-fan-out.ts`, `high-import-coupling.ts`, `missing-error-propagation.ts`, `no-commented-credentials.ts`, `pass-through-wrappers.ts`, `placeholder-comments.ts`, `unsafe-boundary.ts`) — `rule` is untouched, so `detectFactRuleId` (`clients/dispatch/integration.ts`, rule-first with an id-prefix fallback) and `code-quality-warnings.ts`'s `warning.rule ?? warning.tool` grouping both keep resolving to the specific rule id. Also closed a latent gap the fix surfaced: `error-obscuring.ts` and `error-swallowing.ts` had no `rule` field at all (only `tool` carried the rule id), which would have made `code-quality-warnings.ts`'s `rule ?? tool` fallback collapse them into `"fact-rules"` too post-fix — both now set `rule` explicitly, matching the other 11 rules' existing shape. New test in `tests/clients/turn-summary.test.ts` constructs three fact-rule diagnostics with distinct `rule` values sharing `tool: "fact-rules"` and asserts they collapse into a single `fact-rules 3` bucket via `formatTurnSummaryLine`.
|
|
1369
2948
|
- **`clientScope: "all"` (the standalone `lsp_diagnostics` tool and `lens_diagnostics_full`) now gets per-server-aware diagnostics timeouts instead of one flat cap for every spawned server** (#573, found investigating the #570 incident: a 150-file `lens_diagnostics_full` scan took 114s and saturated the TypeScript LSP server) — `LSPService.touchFile` (`clients/lsp/index.ts`) already computed a `perServerTimeout` that reads each server's own `aggregateWaitMs` budget from `server-strategies.ts` (TS ~1s, rust-analyzer 3s, opengrep 3.5s, …), but only wired it up for the single-server `"primary"` hot path (#203) and, later, `clientScope: "with-auxiliary"` (#242). `clientScope: "all"` fell through to the pre-#203 flat `callerCap ?? modeFloor` branch — a leftover from #203 explicitly deferring the "full/cascade path," never revisited when #242 added per-server budgeting for auxiliaries. Every server (fast primary, slow auxiliary) was held to one shared number, either wasting time on a fast server capped to a slow auxiliary's ceiling (multiplied across a full-workspace scan) or starving a slow auxiliary of the time its own strategy says it needs. Fix: `clientScope === "all"` is now included alongside `"with-auxiliary"` in the `perServerTimeout` branch, so each spawned server's individual `waitForDiagnostics` call is bounded by `min(callerCap, ownStrategyBudget)` rather than the flat number. The touch's overall detection deadline (used only for the `lsp_diagnostics_timeout` latency log) is unchanged — it's still `Math.max(...)` over every spawned server's timeout, so "all" still waits for the slowest server before logging a timeout; nothing about auxiliary coverage regresses. `envWait` (`PI_LENS_LSP_DIAGNOSTICS_MAX_WAIT_MS`) and the existing `"primary"`/`"with-auxiliary"` behavior are untouched — this is purely additive coverage of the previously-flattened `"all"` case. Evaluated whether the richer live capability-matrix data (`getCapabilitySnapshots()`'s push/pull `mode`, `diagnosticProviderKind`) should feed `perServerTimeout` beyond what `server-strategies.ts` already encodes; found no case in the current budgeting logic it would improve (the existing `aggregateWaitMs`/`silentOnClean` table already differentiates push vs. pull servers where it matters) — left as a documented non-integration rather than added complexity; `cascade-tier.ts`'s existing capability-matrix consumption is a separate lane (cascade-lane skip-decision, not touch-time budgeting) and is unaffected. New tests in `tests/clients/lsp/service-touch-collect.test.ts` (`#573`): each server on the `"all"` scope gets its own caller-cap-bounded deadline rather than a shared flat number; a fast primary's own wait isn't held to a slow auxiliary's larger budget; a tight caller cap still binds every server as a ceiling; the env override still wins; and a regression guard confirming `"primary"`/`"with-auxiliary"` per-server behavior is unchanged.
|
|
1370
2949
|
- **A timed-out LSP diagnostics check no longer presents as a confirmed-clean result and no longer erases known-good diagnostic state** (#570, found via live log analysis on a dogfooding project) — `LSPService.touchFile` (`clients/lsp/index.ts`) already tracked `notifyWriteTimedOut`/`diagnosticsTimedOut` per touch (logged to `lsp_touch_file` latency events) but never used them to gate anything: an inconclusive empty `collected` result was cached and returned identically to a genuinely server-confirmed empty result. Concretely, a timeout unconditionally deleted `lastKnownDiagnostics`/`lastKnownContentHash` for the file (so a hot-path consumer like `actionable-warnings` at `turn_end` would see "no known diagnostics" for a file that may still have real errors), and callers of `touchFile` (the per-edit dispatch runner, the `lsp_diagnostics` tool) had no way to distinguish "confirmed 0" from "timed out, defaulted to 0". Fix: `touchFile` now computes `inconclusive = notifyWriteTimedOut || diagnosticsTimedOut` (deliberately touch-wide/conservative — `collected` merges diagnostics across every spawned server, so even a partial per-server timeout means the merge may be incomplete) and (1) skips the `lastKnownDiagnostics` set-or-delete block entirely when inconclusive, leaving whatever was cached from the last confirmed check untouched; (2) flags the returned diagnostics array with a non-enumerable `inconclusive: true` bonus field (a plain array otherwise — existing callers that only read it as an array are unaffected) so callers that care can check `.inconclusive` without a breaking return-type change across `touchFile`'s ~10 production call sites. Two consumers wired: `clients/dispatch/runners/lsp.ts` (the per-edit path feeding the footer/widget via `recordDiagnostics`) now returns `status: "skipped"` (same treatment as "no LSP client was ready") instead of `"succeeded"` with an empty diagnostics list when the touch was inconclusive — this automatically feeds the existing dispatcher coverage-notice mechanism (`getCoverageNotice`), so an inconclusive edit is flagged the same way a fully-skipped one is. The standalone `lsp_diagnostics` tool (`tools/lsp-diagnostics.ts`, and its `pilens_lsp_diagnostics` MCP mirror, which reuses the same `createLspDiagnosticsTool()`) now threads the priming `touchFile`'s `inconclusive` flag through `collectDiagnosticsForFile`/`collectFileDiagnosticResult` and folds a timed-out check into the existing #533 "unconfirmed" bucket (never a bare "0 diagnostics"), while distinguishing WHY in the rendered text/compact-render/batch-and-directory tallies ("N timed out" vs. #533's "cannot confirm clean — push-only, silent-on-clean") via a new per-result `timedOut` field and `timedOutFiles`/`unconfirmedReasonClause` aggregation, rather than collapsing the two distinct reasons into one misleading message. The non-timeout path is unchanged: a genuinely fast, confirmed empty result still clears the cache and reports clean exactly as before. New tests: `tests/clients/lsp/service-touch-collect.test.ts` (`#570` describe block — a timed-out touch does NOT clear a prior confirmed non-empty `lastKnownDiagnostics` record; a confirmed non-timeout empty result still clears it as before), `tests/clients/dispatch/runners/runner-status-semantics.test.ts` (the lsp runner returns `"skipped"` on an inconclusive touch), `tests/tools/lsp-diagnostics.test.ts` (`#570` describe block — single-file/batch renders distinguish timed-out from confirmed-clean/silent-unconfirmed).
|
|
2950
|
+
- **Turn-end inline blockers now reconcile against disk — a blocker recorded for a file that has since been deleted can never be cleared (clearInlineBlockers only fires on a later dispatch of the same path, which a deleted file never gets), so it re-surfaced as an "Unresolved from this turn" blocker every turn and could keep the #1063 git guard conservative for the whole session (closes #1245, refs #279/#297/#298 reconcile-on-display discipline)** — RuntimeCoordinator._pendingInlineBlockers (a PathKeyedMap) is now reconciled against disk at every read: getInlineBlockersSnapshot() drops entries whose file no longer exists (the map is tiny and reads are bounded — once per turn_end / tool_result — so the probe cost is negligible), and the git guard's aggregate _gitGuardHasBlockers check reads the reconciled snapshot instead of the raw map size, so a deleted file's blocker no longer counts. The empty-vs-stale distinction is preserved: a blocker for a live file still gates exactly as before. The reconcile rebuilds the map from existence-checked survivors instead of deleting in place — delete-time key normalization lowercases a gone file's tail on Windows while set-time realpaths it, so a delete-based reconcile missed mixed-case filenames (a verified live gap, covered by a dedicated regression test). Regression tests record a blocker for a temp file (lowercase and mixed-case), delete the file, and assert the snapshot and git-guard status both clear (fail pre-fix).
|
|
2951
|
+
|
|
1371
2952
|
- **`lens_diagnostics` mode=full and `lsp_diagnostics` now reconcile fresh scan results into the footer/widget-state cache** (#571) — `recordDiagnostics` (`clients/widget-state.ts`), the sole writer to the footer's `allDiagnostics` store, previously had exactly one caller: `pipeline.ts`'s per-edit dispatch. A `lens_diagnostics` mode=full workspace scan or a standalone `lsp_diagnostics` check fetches fresh, authoritative diagnostics for files it examines but, until now, only reported that data back to the caller — never reconciled it into the footer. Practical consequence: if file A's diagnostics only became stale/fresh because of a change in file B (e.g. a shared interface), and A itself was never directly re-edited, the footer for A could stay stale indefinitely even after a scan proved the fresher truth. Both tools now call a new shared choke point, `clients/widget-state.ts`'s `reconcileScanDiagnostics(filePath, diagnostics, confirmed, writeIndex?)`, for each file they get a result for; `index.ts` injects the same monotonic `RuntimeCoordinator.nextWriteIndex()` source `pipeline.ts`'s per-edit writes draw from, so the existing `WriteOrderingGuard` (#555/#560) can't be clobbered by (or clobber) a concurrent, genuinely newer per-edit write for the same file. Guardrail: a result the check can't vouch for is never reconciled. `#570` landed in the same window as this fix (see above) and its `inconclusive` signal (`touchFile`'s non-enumerable `.inconclusive` flag, set when the notify write or the diagnostics wait itself timed out) is what BOTH tools now key off: `lsp_diagnostics` reuses it directly (threaded from `collectFileDiagnosticResult`/`runFileDiagnostics`'s own `timedOut`/`confirmation` fields), and `lens_diagnostics` mode=full's per-file LSP sweep (`runWorkspaceDiagnostics`) reads the same flag off each `touchFile` call's result and ORs it with its own outer per-file deadline/throw (`LSPWorkspaceDiagnosticResult.timedOut`) — either reason skips reconciliation for that file. No new batching/throttling was added for full-scan bursts — `recordDiagnostics`'s render trigger is a standard TUI dirty-flag request (already exercised by today's per-edit multi-file cascades without dedicated batching) and each write is independent/synchronous, so a scan touching many files behaves the same as N sequential per-edit writes already do. New tests: `tests/clients/widget-state.test.ts` (`reconcileScanDiagnostics`'s confirmed/unconfirmed gating and write-ordering-guard interaction), `tests/tools/lens-diagnostics.test.ts` and `tests/tools/lsp-diagnostics.test.ts` (each tool's confirmed-vs-timed-out/unconfirmed reconciliation wiring, including batch mode).
|
|
1372
2953
|
- **Native TS7 no longer inherits classic typescript-language-server's `silentOnClean` cascade fast path** (#558, reverts #541) — PR #541 (2026-07-11) classified TS7's native `tsc --lsp --stdio` launch variant (PR #526) as `silentOnClean`, letting the cascade lane (`clients/lsp/cascade-tier.ts`) skip its in-lane diagnostic wait for native-ts7 edits on the strength of a clean-signal probe run that appeared to show it silent, same as classic. A 2026-07-12 dual-environment re-measurement (nightly CI on Linux and a live local run on Windows dev, same `typescript@7.0.2` both times) found native-ts7 now publishes 2 version-less diagnostic sets on the clean transition (`cleanPubs=2(v:0)`) — it is NOT silent, a drift from the #541 measurement, and skipping the wait could miss or delay real diagnostics on native-ts7 edits. `cascade-tier.ts`'s classifier again routes a `launchVariant === "native-ts7"` snapshot through the fail-safe full-wait path; `server-strategies.ts`'s `silentOnClean: true` for `"typescript"` is effectively classic-only again. Classic typescript-language-server is unaffected — re-confirmed silent (`cleanPubs=0(v:0)`) in the same run. `scripts/probe-clean-signal.mjs`'s nightly drift check no longer routes native-ts7 rows through classic's shared marker; it now compares them against an explicit `false` expectation, so a future TS7 build that goes silent again surfaces as a `silent-not-marked` signal instead of being silently skipped.
|
|
1373
2954
|
- **LSP diagnostics no longer briefly cache stale results after rapid edits** — `textDocument/publishDiagnostics` pushes were written into the diagnostics cache (`pushDiagnostics`) unconditionally, even when the server's own reported `version` field showed the push was computed against an EARLIER edit than the one currently in flight (e.g. the server was still finishing analysis of edit N when edit N+1 already landed). `isVersionStale()` (`clientWaitForDiagnostics`'s staleness check) only gated whether a diagnostics *wait* resolved early — it was never consulted by the plain read path (`client.getDiagnostics()`/`getAllDiagnostics()`/`pruneDiagnostics()`), so a late push could get cached and served as "current" until the next genuinely fresh push overwrote it: diagnostics would transiently show stale results right after a rapid edit, then self-correct a moment later once real analysis caught up. The `publishDiagnostics` handler in `clients/lsp/client.ts` now drops a push before it reaches the cache (no `pushDiagnostics.set`, no `diagnosticsVersion` bump, no `diagnostics` event emit) whenever the push reports a version that's behind the currently-tracked document version for that path — checked at write time (after the debounce timer fires, not at notification-receipt time) so a push that arrives fresh but whose debounce window straddles a later edit is still caught. Version-less servers (no `version` field reported) are unaffected — that remains an intentional, documented tradeoff, unchanged by this fix. A dropped push correctly emits nothing, so a pending `clientWaitForDiagnostics` call still falls through to its other resolution paths (a later genuinely-fresh push, or the existing timeout backstop) rather than resolving on stale data. Deliberately out of scope, left as known follow-ups: the pull-diagnostics path (`clientRequestPullDiagnostics`/`clientRequestWorkspaceDiagnostics`) has no version stamp to compare against in this codebase's current handling, so nothing analogous is applied there; and `diagnosticsVersion` remains a single global counter rather than per-path, so an unrelated path's fresh push can still satisfy a wait baselined on this path's version — both are separate, larger-blast-radius changes.
|