@herbertgao/pi-extensions 2026.8.6 → 2026.8.8
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 +8 -6
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +320 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +79 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +27 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +66 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +5 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +174 -100
- package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +10 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +5 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +140 -97
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +63 -28
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +86 -38
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +76 -9
- package/node_modules/@narumitw/pi-btw/README.md +120 -111
- package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-btw/package.json +10 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +999 -0
- package/node_modules/pi-lens/README.md +117 -3
- package/node_modules/pi-lens/config/ruff/core.toml +42 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
- package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +381 -13
- package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +129 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +206 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
- package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
- package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +84 -14
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +25 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +32 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +47 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +127 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +188 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +21 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +316 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
- package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
- package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +87 -8
- package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +9 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
- package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +128 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1660 -164
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2423 -151
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
- package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
- package/node_modules/pi-lens/dist/clients/module-report.js +22 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
- package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +23 -9
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +8 -3
- package/node_modules/pi-lens/dist/clients/read-guard.js +253 -10
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +352 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +373 -9
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +620 -67
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +61407 -48577
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +476 -57
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/docs/agent-guide.md +6 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +19 -5
- package/node_modules/pi-lens/docs/globalconfig.md +3 -0
- package/node_modules/pi-lens/docs/language-coverage.md +71 -5
- package/node_modules/pi-lens/docs/mcp.md +2 -2
- package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
- package/node_modules/pi-lens/docs/settings.md +19 -1
- package/node_modules/pi-lens/docs/usage.md +1 -0
- package/node_modules/pi-lens/package.json +27 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
- package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
- package/node_modules/pi-mcp-adapter/README.md +47 -5
- package/node_modules/pi-mcp-adapter/cli.js +163 -1
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +25 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
- package/node_modules/pi-mcp-adapter/index.ts +219 -32
- package/node_modules/pi-mcp-adapter/init.ts +36 -4
- package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
- package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +3 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
- package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
- package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +2 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
- package/node_modules/pi-mcp-adapter/utils.ts +70 -0
- package/node_modules/pi-web-access/CHANGELOG.md +37 -0
- package/node_modules/pi-web-access/README.md +32 -13
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +40 -5
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +60 -61
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +4 -2
- package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
- package/node_modules/pi-web-access/parallel.ts +2 -2
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +44 -22
- package/node_modules/pi-web-access/tavily.ts +16 -4
- package/node_modules/pi-web-access/utils.ts +87 -0
- package/node_modules/pi-web-access/valyu.ts +199 -0
- package/package.json +14 -14
|
@@ -16,6 +16,1003 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
16
16
|
|
|
17
17
|
### Security
|
|
18
18
|
|
|
19
|
+
## [4.1.1] - 2026-08-21
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Prompt-cache misses now say why, and mixed injections split by source (refs #1071)** — a `cache_usage` record carries `interTurnGapMs` and a `cacheMissCause` verdict of `ttl-expired`, `prefix-broke`, `partial-eviction`, or `unknown`, so the dominant cache cost is readable from `latency.log` instead of reconstructed by hand-joining timestamps. The gap is measured to request time, so a slow response is not mistaken for idle time; `gapBasis` names which endpoint produced the value. A `cache_context` record splits a mixed injection payload by contributing source, with per-source message count, characters, bytes, and an estimated token count. Token figures use a documented four-chars-per-token estimate and are never presented as provider-measured. The TTL threshold defaults to 60s and is overridable via `PI_LENS_PROVIDER_CACHE_TTL_MS`.
|
|
24
|
+
|
|
25
|
+
- **Quick-mode session_start now logs which steps it skipped (closes #1911)** — quick mode silently skipped slow tool probes, language profiling, preinstall, startup scans, and the error-debt baseline, with no record either way. `session_start` now emits one bounded `session_start_skipped_steps` latency record naming the skipped step set, so a reader can tell "quick mode correctly skipped these" from "the probes silently never ran".
|
|
26
|
+
|
|
27
|
+
- **Runner parsers are now held to real binary output (refs #1937)** — a parser written from documentation can be wrong in a way no hand-authored test sees, because the test asserts the same imagined shape. Captured real-bytes fixtures now live in `tests/fixtures/runner-output/`, each with a machine-generated provenance header naming the tool, version, and exact argv; a replay suite feeds those bytes through the runner, fails when the parser finds nothing, and fails when the runner's invocation drifts from the argv the fixture recorded. The sweep found four live instances: taplo spawned a `--output=json` flag taplo rejects, stylelint read stdout while stylelint 16+ reports on stderr, phpstan read the error COUNT as if it were the error array and discarded its file-independent findings entirely, and sqlfluff inserted `--dialect` between `--format` and its value so no unconfigured SQL project was ever linted. A scheduled `parser-smoke` lane now runs the tier-1 tools' real binaries over a planted violation, with a pass floor so a run that installed nothing cannot report green.
|
|
28
|
+
|
|
29
|
+
- **A runner that reads nothing out of a failing tool's output now leaves a record (closes #1948)** — when a dispatch runner's tool exits nonzero, prints output, and the runner's parser extracts zero diagnostics, the degradation ledger gets a bounded `runner-parsed-nothing` row naming the tool, the exit status, the output length, and the first output line. Until now that case was byte-for-byte identical to a clean file in the worklog, which is how five parser bugs (vale, taplo, stylelint, phpstan, sqlfluff) reported clean files for months. A genuinely clean run records nothing, so the ledger does not fill up with one row per save. The gate lives in one shared helper, `parseToolRun`, that fourteen runners now share, and a sweep test keeps a new runner from silently opting out through any of the older spawn-outcome primitives.
|
|
30
|
+
|
|
31
|
+
- **Mechanical lint batch: actionlint, PR-title lint, markdownlint, OSV scan (refs #1844)** — a new `lint.yml` workflow dogfoods actionlint against every workflow file, validates that every PR title carries a conventional prefix and an issue reference **in the title itself** (`scripts/check-pr-title.mjs` — a reference living only in the PR body no longer counts, since it never reaches the merge-commit subject), and lints Markdown docs with `markdownlint-cli2` under a repo-tuned config. A separate `osv-scan.yml` runs an advisory weekly `osv-scanner` sweep plus a scan on lockfile-touching PRs, writing results to the job summary.
|
|
32
|
+
|
|
33
|
+
- **Vale prose lint with a vendored Google style base (refs #1844)** — `.vale.ini` at the repo root scopes Vale to `docs/`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `CONTRIBUTING.md`, `.claude/agents/*.md`, and `.changelog/*.md`, using the Google style package vendored under `.vale/styles/Google/` (no `vale sync` at CI time) plus a `pi-lens/` style with three house rules: no `please` in instructions, an em-dash-density check, and a long-sentence check. A new advisory `vale` job in `lint.yml` runs it on every PR. Because the in-product `vale` runner (`clients/dispatch/runners/vale.ts`) activates on `.vale.ini` presence, dogfood sessions now surface Vale findings on doc edits too.
|
|
34
|
+
|
|
35
|
+
- **Husky-managed local git hooks (closes #1804)** — `npm install` now wires
|
|
36
|
+
a pre-commit hook (changelog fragment validation + `npm run lint`) and a
|
|
37
|
+
pre-push hook (build + a capped, full-path-resolved targeted `vitest`
|
|
38
|
+
selection for changed files, never the full suite; degrades to build-only
|
|
39
|
+
past 25 matched test files or a 2-minute shared test-lock wait). Both are
|
|
40
|
+
skippable with `PI_LENS_SKIP_HOOKS` set to any non-empty value, which
|
|
41
|
+
agents and CI should set; humans leave hooks on. Hook install itself is
|
|
42
|
+
skipped for CI and production/consumer installs, and never fails
|
|
43
|
+
`npm install` on error.
|
|
44
|
+
|
|
45
|
+
- **latency.log now says whether the LSP pool reused a client or spawned one (closes #1934)** — `lsp_client_selected` fired 5601 times in a 20.8h dogfood window carrying only `{serverId, candidateCount}`, so nothing in the log said whether the pool served a warm client or paid for a language-server spawn. This is the most expensive cache in pi-lens: a miss costs a process. The only estimate was a cross-record inference against `lsp_launch_candidate_success`, which counts something else, so a regression that halved pool reuse was invisible. The record now carries an `outcome` field with one of three values on the same record and the same denominator. `warm-reuse` means the pool served an already-connected client. `cold-spawn` means this selection waited on a spawn, whether it started that spawn or joined one already in flight, so a session-start burst where many files wait on the same spawn reads as several cold spawns and deflates the apparent reuse rate for that window. `spawn-failure` means a spawn ran and failed. Reuse rate is `warm-reuse / (warm-reuse + cold-spawn)`. A spawn failure is deliberately distinct from a clean decline: no root, a breaker already open, host trust refused, or a binary absent while installs are disabled. Declines emit no outcome record and keep their existing ones, so `lsp_client_unavailable` and the `outcome` field can both describe the same event and are counted separately. `getWarmClientForFile`, the warm-only lookup the cascade quiet window and read expansion call per file, previously returned `undefined` and emitted nothing; it now emits a bounded `lsp_warm_client_missing` record naming the candidate server and root, rising-edge per candidate set with the exact count in the degradation ledger.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Search credit ignores pipe truncation (closes #1908)** — `grep -C2 pattern
|
|
50
|
+
file | head -1` no longer credits context lines the pipe cut off. The
|
|
51
|
+
read-guard now detects a truncating pipe tail (`head`, `tail`, `sed q`)
|
|
52
|
+
downstream of a line-numbered grep and falls back to match-line-only
|
|
53
|
+
credit, so a later edit to the uncredited context still requires a read.
|
|
54
|
+
|
|
55
|
+
- **Cascade tier-3 registry now resets at session_start (closes #1910)** — the outstanding-touch registry and its sweep-scoped expired/evicted counters (`clients/lsp/cascade-tier.ts`) used to survive a session replacement, so a new session inherited the prior session's outstanding touches and a stray eviction or expiry landed on the next session's first reconcile gauge. `handleSessionStart` now clears both, primary-only, same as every other per-session latch in that reset block.
|
|
56
|
+
|
|
57
|
+
- **Read-guard eviction telemetry is now always on (closes #1913)** — the
|
|
58
|
+
first time a file's read-guard record cap trims in a session, it now emits
|
|
59
|
+
a `read_cap_trimmed` log line (file, evicted count, credit-vs-genuine
|
|
60
|
+
split, raw read count) regardless of `PI_LENS_READ_GUARD_VERBOSE`, so a
|
|
61
|
+
live eviction regression is visible by default instead of only under
|
|
62
|
+
verbose logging. Later trims on the same file keep updating the running
|
|
63
|
+
totals (queryable via `ReadGuard.getTrimStats`, and via the degradation
|
|
64
|
+
ledger's own tally) without flooding the log on every subsequent trim.
|
|
65
|
+
|
|
66
|
+
- **agent_settled no longer crashes with extension_error when the session is replaced mid-run** —
|
|
67
|
+
stale extension ctx reads are guarded and the ambient abort signal is
|
|
68
|
+
cleared (closes #1924; thanks @Pluto-Yt).
|
|
69
|
+
|
|
70
|
+
- **A session event on a replaced session no longer reports a pi-lens handler error (closes #1925)** — pi invalidates a captured extension ctx on `newSession`/`fork`/`switchSession`/`reload`, and an event already queued when that happens still reaches pi-lens carrying the dead ctx. `tool_result`, `turn_start`, `agent_end`, and `turn_end` each read a ctx property before any guard, so the SDK's `assertActive()` error was caught by `ExtensionRunner.emit` and reported as an extension error against pi-lens. That report named the wrong cause, said nothing about which handler was affected, and counted nothing. All five session-event registrations, including `agent_settled` (#1924), now go through one wrapper in `clients/session-event-guard.ts`: it probes the ctx once before dispatch, still classifies a stale error that arrives mid-handler, and records every skip in the degradation ledger under `extension-ctx-stale` plus a bounded `session_event_stale_ctx_skip` row in `latency.log`, keyed by event name. A new registered-or-fail sweep reds any future `pi.on` or `pi.on?.` registration that is neither wrapped nor given a stated reason.
|
|
71
|
+
|
|
72
|
+
- **Git installs start about 750ms faster (refs #1926)** — pi supplies `typebox` and `@earendil-works/pi-tui` from its own runtime, but pi-lens declared both as runtime dependencies. A `git:` install therefore vendored a private second copy of each, and Node evaluated that whole extra module graph every time the extension loaded. Both are now optional peer dependencies, so nothing vendors them. The `PI_TIMING` module import for the dogfood git install drops from 941ms to 147-196ms, and the extensions block drops from 2165ms to 1449ms. One trade-off, stated plainly: because the bare specifiers no longer resolve on disk, pi loads the entry through jiti's transform path instead of a native import, so the FIRST start after an install, an update, or a jiti cache eviction is slower, around 5.6 to 8.2 seconds on the 4MB bundle. Every start after that is the fast one.
|
|
73
|
+
|
|
74
|
+
- **Updating pi-lens no longer costs a five-second first session (refs #1926)** — pi loads the extension through jiti, which transforms the ~4MB `dist/index.js` bundle and caches the result. Every `git:` install or update produced a new bundle, so the next interactive session paid that transform: 4847ms of `module import`, against a 138ms steady state. The `prepare` chain now runs the transform itself, in `scripts/warm-loader-cache.mjs`, writing the same cache entry pi reads. The first session after an update is warm. The step runs last, is best-effort, and never fails an install; set `PI_LENS_SKIP_WARM_CACHE` to skip it. Each run appends one line to `~/.pi-lens/install.log`.
|
|
75
|
+
|
|
76
|
+
- **A session swap that starves `session_start` or `context` is now counted, not misreported as a pi-lens crash (closes #1929)** — both handlers survived a dead extension ctx already, but each logged it as `session_start crashed: …` or `context event error: …` and recorded nothing, so a replacement that kept starving them was invisible in aggregate. Both now run through the shared stale-ctx wrapper and leave one bounded `extension-ctx-stale` record keyed to the event name. `context` uses a new value-returning wrapper variant that states its stale-path answer explicitly: `undefined`, pi's "this extension contributed nothing", so the host keeps its own message list rather than receiving a half-built injection. Seven of twelve `pi.on` registrations are now wrapped, up from five.
|
|
77
|
+
|
|
78
|
+
- **Diagnostics scans stop logging a phantom ast-grep cache-stats record (closes #1935)** — `#1715` already grows the tree-sitter parse cache to span a project scan's file count, so the diagnostics-scan path does not run at the 50-entry interactive default; that part of `#1935` was resolved before this fix landed. What was still broken: every scan logged a second `cache_stats` record under scope `project_diagnostics_ast_grep_scan`, always all-zero, because ast-grep-napi parses through its own native engine and never touches the WASM tree cache that record claimed to measure. That vacuous record is now removed instead of wired to a cache it structurally cannot use; its one real value — ast-grep's own duration and file count, often a scan's most expensive phase — now rides an `astGrep` sub-field on the surviving `project_diagnostics_scan` record instead of disappearing. Added a memory-safety test pinning `treeCacheTotalBytes` at the existing 500-entry scan ceiling (`TREE_CACHE_SCAN_CAPACITY_CEILING`) using realistic, varying per-file sizes: worst case is 6-18MB of resident source bytes at that ceiling, and `TreeCache` enforces no per-entry byte cap of its own, only the entry count. A regression test pins the scanner to never emit the dead ast-grep scope.
|
|
79
|
+
|
|
80
|
+
- **`cache_context` no longer logs an unusable `unknown` prefix pair on nearly every record (closes #1938)** — `prefixObservation` and `firstMessageChange` were computed from a hash of the pre-injection prefix capped at 64 messages / 2,048 characters. Any transcript past that cap set `prefixHashTruncated`, which forced both fields to `"unknown"`. Every real session outgrows the cap within the first few turns, so the pair reported `unknown` on 97.2% and 7.6% of records respectively — cost without signal. This PR removes the whole family that existed only to support that pair: `prefixObservation`, `prefixObservationUnknown`, `prefixBaseline`, `firstMessageChanged`, `firstMessageChange`, `firstMessageHashTruncated`, `beforeFirstMessageHash`, `afterFirstMessageHash`, `beforePrefixHash`, `afterPrefixHash`, `prefixHashTruncated`, `prefixMessageCountTruncated`, `prefixContentHashTruncated` — 13 fields in all. `cache_prefix_break` already tracks first-message stability with an unbounded hash and never truncates; it remains the source of truth for cache-prefix breaks.
|
|
81
|
+
|
|
82
|
+
- **Review-graph Tier-3 build never grew the tree cache past the 50-entry default (closes #1941)** —
|
|
83
|
+
`builder.ts`'s full-project rebuild parses every non-jsts file through the
|
|
84
|
+
shared `TreeSitterClient`'s parse-tree cache but never called
|
|
85
|
+
`ensureTreeCacheCapacity`, the #1715 fix already wired into the
|
|
86
|
+
diagnostics scanner. A project with more than 50 non-jsts files evicted
|
|
87
|
+
and re-parsed files past the 50th on every Tier-3 build. The build now
|
|
88
|
+
grows the cache to its actual per-parse working set before extraction
|
|
89
|
+
starts — the full file list on a cold build, or the checkpoint's remaining
|
|
90
|
+
files on a resumed build, since resumed files are reused from the
|
|
91
|
+
checkpoint graph and never re-parsed.
|
|
92
|
+
|
|
93
|
+
- **A demoted blocker no longer shouts STOP, and it stops re-serving forever (closes #1944)** — the past-EOF gate moved a blocker whose file had shrunk past the cited lines into the advisory channel, but the advisory embedded the blocker body verbatim, so the agent still read "🔴 STOP — 11 issue(s) must be fixed" with line numbers the file no longer had. Nothing retired the record either, so it re-served on every turn end for the rest of the session (measured live at 80+ minutes). A demoted body now drops the STOP banner and the "must be fixed" imperative, renders a dead coordinate as `L<n> (line no longer exists)`, and — when no re-run could ever confirm it — is delivered once and then retired, with the suppression recorded in the degradation ledger under `demoted-finding-retired`. `lens_diagnostics mode=delta` also stops printing 🔴 on a row whose coordinate it just replaced with the stale marker.
|
|
94
|
+
|
|
95
|
+
- **The oxlint runner now surfaces warning-severity findings instead of discarding them (closes #1947)** — oxlint exits 0 whenever nothing at error severity was found, and warning is oxlint's own default severity. The runner treated any exit 0 as "no findings" and returned early, so a real capture of oxlint on an unused variable — a full JSON report, exit 0 — was thrown away. The runner now parses stdout unconditionally and decides on the parsed diagnostic count instead of the exit code: zero diagnostics is still a clean `succeeded`/`none`, one or more is `failed` with `semantic: "warning"` (or `"blocking"` when a diagnostic is error severity), matching the mapping the runner already used for the nonzero-exit case. A captured real-bytes fixture (`tests/fixtures/runner-output/oxlint/warning-exit-zero.captured.json`, oxlint 1.79.0, exit 0) pins the behavior.
|
|
96
|
+
|
|
97
|
+
- **Merge-train warden's `update-branch` kick 403s under branch protection (#1959)** — the workflow's job permissions granted `pull-requests: write` but not `contents: write`. The PUT `pulls/N/update-branch` endpoint has GitHub create a merge commit on the PR branch, which needs write access to repository contents, not just to the pull request object; the inline comment attributing it to `pull-requests: write` was wrong. The warden also now tells apart the two reasons update-branch can 403: a fork-owned head PR records a distinct benign outcome (`update-branch-forbidden-fork`, logged, not a run failure), while an own-branch PR still fails the run loudly, since that case is a real permissions bug like this one.
|
|
98
|
+
|
|
99
|
+
- **The blind review-graph read no longer drops a valid snapshot on a HEAD-only move (closes #1961)** — `getCachedReviewGraph` verified the persisted snapshot's git stamp against the current HEAD, so a plain `git commit` made every reader see "graph: cold" until something rebuilt in-process. Measured stamp lifetime across four workspaces was 737s to 1562s (median), and two of the four were already missing at probe time. The read path now verifies tree IDENTITY, never revision — the same policy the build path's tier-2 load has followed since #300. A snapshot stamped for a different worktree is still dropped, since nothing downstream of this read content-verifies it. A snapshot stamped at a different HEAD is served, and both `project_report`'s trust notes and `module_report`'s warnings say which commit it was built at. That caveat is derived per call from the snapshot's stamped commit and the worktree's HEAD right now, so it never names a commit that has stopped being HEAD and it clears itself when HEAD returns to the stamped one. The mid-build resume checkpoint carried the same defect and got the same fix: it hashes every processed file on resume, so revision equality proved nothing the hashes did not. Both read verdicts now write a bounded `review-graph.log` record (`snapshot_read_dropped`, `snapshot_read_drifted`), rising-edge per workspace off the `review-graph-snapshot-read` ledger kind — the drop used to be silent, which is why it hid for weeks.
|
|
100
|
+
|
|
101
|
+
- **A settled review-graph build no longer suppresses every later build, and `project_report` only claims a retry when one started (closes #1962)** — `buildOrUpdateGraph`'s dedupe cache deleted its entry on rejection only, so a settled promise for a skipped or completed build answered every later call for the same key for the rest of the process. Only the dispatch pipeline's per-invocation `clearGraphCache()` ever removed it, and the background build `project_report` kicks off never goes through the pipeline: four calls over 37s produced one `build_started` record and three "A retry was started." messages with no retry behind them. The entry now lives only while its build is pending, so genuine concurrency still dedupes and a settled build never latches. `project_report` asks the builder whether a build is already in flight before it claims one started, and reports the current attempt rather than replaying the first one as if it were fresh. A `.pi-lens.json` at the repo root raises `reviewGraph.maxFiles` above the derived 1000-file cap this repo trips, so the tool works on this repo at all.
|
|
102
|
+
|
|
103
|
+
- **A language server that dies mid-session now records why, and the liveness probe stops provoking the deaths (closes #1969)** — an ast-grep child closed with `code=1` and empty stderr 14 times in one day. The visible fallout was 19 `lsp_client_skipped_broken` cooldowns and 32 coverage gaps; the cause was recorded nowhere. Two changes. First, an unprompted process `close` now writes an `lsp-server-unexpected-close` degradation-ledger entry naming the server, the exit code, the signal, and whether stderr carried anything. It is written on `close` rather than `exit`, so "stderr was empty" is a fact about the server and not a race with the pipe, and it is gated on `shutdownRequested`, so evictions and ordinary teardown record nothing. Second, the `pingLiveness` round-trip (#1277) no longer hardcodes `workspace/symbol`. It picks a method the server advertised, best first: `workspace/symbol`, then `textDocument/documentSymbol` or `textDocument/hover` on an already-open document, then nothing. ast-grep's tower_lsp backend logged "got a 'workspace/symbol' request, but it is not implemented" on every probe; it advertises `hoverProvider`, so it keeps a real round-trip. When no probe is available the ledger records `lsp-liveness-probe-unsupported`, because liveness derived from process and connection state alone is the weaker verdict and must not pass as the strong one. Also fixed on the same path: `createLSPClient`'s initialize-timeout catch killed the hung child without claiming the kill, so its own teardown reported as a crash. That fabricated the exact `code=1 signal=none stderr=empty` signature this change exists to make trustworthy, and it had been writing a false `lsp_server_unexpected_exit` latency line on every failed handshake since the #615 follow-up.
|
|
104
|
+
|
|
105
|
+
- **Log writer reopens and retries once on a failed write instead of dropping records silently (refs #1970)** — `ndjson-logger.ts` is the shared write-plumbing behind every NDJSON sink (`latency.log`, `extension.log`, `tree-sitter.log`, `cascade.log`, `word-index.log`, `review-graph.log`, and any other `createNdjsonLogger` caller). This hardens the sink against reachable `appendFile` failures (ENOENT after the parent directory is deleted, EBUSY/EPERM under a syncing OneDrive folder or antivirus lock, EMFILE, ENOSPC) — the specific `ERR_STREAM_DESTROYED` reports that motivated this issue were root-caused to a different process's persistent-stream logger, not this sink, but the reachable-failure classes above land here today with no recovery and no observability. A write that throws now gets one reopen-and-retry (re-verify the parent directory, then retry the write once) before it counts as a loss, matching the `pi-analyze#15` shape. An unrecovered write is counted in a per-sink, in-memory tally (`writeFailures`) rather than thrown or silently discarded, and `degradation-ledger.ts` folds that tally into `getDegradationSummary()` at READ time under a new `log-sink-write-failure` kind — so `pilens_health` names the sink and the dropped-write count. The fold happens by reading ndjson-logger's own tally, never by writing a durable ledger row back through the sink that just failed, so a permanently dead sink cannot recurse into an unbounded chain of self-reporting writes. The tally resets alongside the rest of the ledger at `session_start`.
|
|
106
|
+
|
|
107
|
+
- **Warmup and workspace-scan walkers gate by file extension before consulting the ignore matcher, cutting a 31.7s stall to sub-second on repos with large ignored file piles (closes #1974, reported by @0xkite)** — `isIgnored` recompiles minimatch patterns per ancestor directory on every call; a runtime-output directory like `wal/` holding tens of thousands of `.log` files, gitignored only by a file-level `*.log` pattern (so the directory itself is never pruned), forced every one of those files through that cost. Six walkers checked `isIgnored` before their own cheap extension gate, paying the expensive check for files the gate would have dropped anyway: `collectSourceFilesForWarmup` (`language-profile.ts`, the reported 31.7s case), `JscpdClient.hasSourceFilesRecursive` (`jscpd-client.ts`), `classifyEntry` (`source-filter.ts`), `makeSourceCountVisitor` (`startup-scan.ts`), `getModuleSourceFiles` (`review-graph/workspace-modules.ts`), and `collectWorkspaceDiagnosticFiles` (`lsp/index.ts`). All six now run their extension (or LSP-server) gate first; output is unchanged, since both gates still have to pass to keep a file — only the order changed. A repo-wide sweep of all 14 files that call `isIgnored` found `tree-sitter-client.ts` and `tools/lsp-diagnostics.ts` already extension-first, and cleared the remaining per-event/no-ext-gate sites by construction; see the PR body for the full per-site table.
|
|
108
|
+
|
|
109
|
+
## [4.1.0] - 2026-08-20
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
|
|
113
|
+
- **CUE code formatting support** — `.cue` files are now formatted with `cue fmt` through the standard formatter pipeline, with smart-default policy (no config required, `cue` binary on PATH).
|
|
114
|
+
|
|
115
|
+
- **CUE tree-sitter grammar** — `.cue` files now parse under tree-sitter. No publisher ships a CUE wasm, so pi-lens builds one from a pinned upstream commit and commits it to `vendor/grammars/`; `scripts/check-grammar-provenance.mjs` re-hashes it against the pin in `scripts/grammars.lock.json` on every CI run, and the download path refuses a vendored grammar outright instead of retrying a URL that will never exist. This lands the parser only: no CUE symbol or import queries exist yet, so structural symbol search and import extraction still skip `.cue`, and search falls back to the word index. The query rules are tracked in #1522.
|
|
116
|
+
|
|
117
|
+
- **CUE Language Server support** — `.cue` files now resolve to a dedicated `CueServer` LSP entry launched via `cue lsp serve`, with `cue-lang/cue` registered as a managed GitHub-release tool for auto-install fallback. CUE is a tracked `FileKind` with project/root markers (`cue.mod`), dispatch policy, and an LSP handshake fixture. Coverage is syntax and parse diagnostics only: `cue lsp` leaves conflicting values and failed constraints to `cue vet`.
|
|
118
|
+
|
|
119
|
+
- **CUE symbol and import extraction (refs #1522, #1519)** — `.cue` files now get tree-sitter symbol (`#Definition`s as types, struct fields as properties, `let` bindings as variables) and import (`import "pkg"`) queries, so structural symbol search and import extraction cover CUE like any other language instead of falling back to the word index. Definitions are told apart from ordinary fields with a `#match?`/`#not-match?` text predicate on the `#`-prefix, since the grammar has no separate definition node kind.
|
|
120
|
+
|
|
121
|
+
- **`cue vet` auxiliary runner for CUE evaluation errors (refs #1522, #1519)** — `cue lsp` reports load and parse errors as you type but deliberately leaves conflicting values and failed constraints to `cue vet`. A new `cue-vet` dispatch runner covers that gap, running alongside the LSP on every `.cue` edit (the same "lsp covers part, a CLI covers the rest" shape as terraform's `lsp, tflint, trivy-config` group). It vets the whole PACKAGE (the touched file's directory), not the file alone — CUE packages are directory-scoped, so a single-file invocation false-positives on the normative split-schema/values authoring style — then filters the reported errors down to the ones that name the touched file, always surfacing an error it cannot attribute to any file rather than silently dropping it. The touched file's own content is checked for a `package` clause up front: a package-less file goes straight to single-file scope, because a directory holding both a packaged file and a package-less one vets clean at the directory level (the loose file is silently excluded from evaluation, not a failure), so waiting for the directory vet to fail can never catch it. When the directory itself fails outright (two+ differently-packaged files sharing it — also legal CUE), the runner falls back to the touched file alone as well. The package-clause check skips leading blank lines, `//` comments, and file-level `@attribute`s alike (`@extern(embed)` is the documented real case) — treating an attribute-led file as package-less would have false-positived a clean cross-file reference as "reference not found". It runs with `-c=false` so an ordinary schema-only file — no concrete data, a common CUE authoring pattern — doesn't fail vet's default concreteness gate; a real type conflict still reports. Verified against a real `cue v0.17.1` binary end to end, including the tool-smoke fixture's binding diagnostic, a valid multi-file package, a package-less standalone file, a two-package directory, a mixed packaged/package-less directory, and a clean package where a file opens with an attribute before its package clause.
|
|
122
|
+
|
|
123
|
+
- **Rendered-manifest validation for Helm charts, opt-in (refs #1283)** — `helm lint` checks a chart's source and cannot see what the chart produces, so a chart that lints clean can still fail to install. Setting `{"helm": {"renderValidation": {"enabled": true}}}` in `.pi-lens.json` makes pi-lens render the nearest chart with `helm template` into a scratch directory under the system temp dir and validate the output: a failed render (missing values key, nil pointer in a template expression, a dependency declared in `Chart.yaml` but absent from `charts/`) is reported as a blocking finding on the template that failed, every rendered document must declare `apiVersion` and `kind`, and — when `trivy.enabled` is also set — `trivy config` runs over the rendered manifests, which is the only way Trivy's Kubernetes policy checks can see a chart's real output. Findings map back to the source template through helm's own `# Source:` annotations. It is **off by default** because rendering executes chart-authored template code, and the switch is not sufficient on its own: rendering also requires host project trust, since `.pi-lens.json` is a tracked file that a cloned repository could ship pre-enabled, and the switch is read from the chart's own project root rather than the current directory. Nothing is written to the chart directory, and the scratch directory is removed on every exit path. A missing `helm` reports as runner-unavailable rather than as chart findings, and an IaC pass that could not run says so instead of reading as clean.
|
|
124
|
+
|
|
125
|
+
- **ast-grep rule: ban bare `object` parameter annotations (Refs #1597)** — Add `no-bare-object-param` to the shipped rule catalog (TypeScript only — JavaScript has no type annotations to match). Fixes the one pre-existing hit, `clients/host-ports.ts:8`'s `HostLogSink` callback type, by narrowing it to `Record<string, unknown>`.
|
|
126
|
+
|
|
127
|
+
- **Ported anti-slop's AI-authorship rules into the shipped ast-grep catalog (refs #1718)** — a 2026-08-19 maintainer-supervised comparison against dmmulroy/anti-slop's 15 Oxlint rules found six already covered here and adopted the rest that ast-grep's structural matcher can express: `no-unsafe-dictionary-any`/`no-unsafe-dictionary-unknown` (the `Record<K, V>` value-type arms, `error`/`hint`), `no-unknown-parameters`/`no-unknown-returns` (`hint`), `no-known-value-widening` (`hint`, narrowed to the annotated `const x: Record<K, V> = { ... }` sub-case), `require-safety-comment-for-as-unknown-as` (`hint`, scoped to `as unknown as` chains), `no-runtime-typeof` (`hint`, with type-guard/naming/`.d.ts` exemptions), and `no-shape-in-symbol-names` (`hint`). `no-bare-object-param` and `no-chained-type-assertions` were promoted to `error`, and `no-chained-type-assertions` dropped its `as unknown as` exemption for the shipped catalog (users get the stricter upstream form; see the rule's note). `no-widen-then-assert` was not ported — its upstream implementation needs cross-statement variable-resolution data flow ast-grep doesn't have. Two `Record<string, any>` production sites (`clients/cache/rule-cache.ts`, `clients/tree-sitter-query-loader.ts`) were narrowed to `Record<string, unknown>` as part of the adoption. AGENTS.md's AI-authorship-smells section records the confirmed-clean counts and cites the anti-slop source.
|
|
128
|
+
|
|
129
|
+
- **One primitive for "at most one execution per key" replaces ten hand-rolled copies (closes #1753)** — Ten sites
|
|
130
|
+
wrote the same five lines by hand, and four bugs came out of the copies: a
|
|
131
|
+
leak guard no test exercised (#1690), a late completion that evicted its own
|
|
132
|
+
successor (#1674), a trailing-rerun bit that shipped unpinned (#1687), and a
|
|
133
|
+
latch its own mutation matrix deleted as vacuous (#1722). `singleFlight` now
|
|
134
|
+
owns the share, the clear on both settlements, the successor check, and the
|
|
135
|
+
trailing-rerun coalescing, with a six-mutant matrix proving each guard reds a
|
|
136
|
+
test. Biome and ast-grep availability are converted first; their existing
|
|
137
|
+
tests, including #1690's leak proof, pass unchanged. A ratchet test fails any
|
|
138
|
+
new hand-rolled in-flight state that does not carry a reason.
|
|
139
|
+
|
|
140
|
+
- **Merge-train warden (refs #1844)** — A scheduled workflow now sweeps every open PR every 10 minutes and mechanizes what a human was doing by hand during the release drive: label and comment once (deduped) when a PR turns merge-conflicted (`mergeStateStatus: DIRTY`), call the native update-branch API when an armed auto-merge PR falls behind, and label and comment once (deduped) when a required check (`Unit tests`, `Lint & type-check`) fails on the current head. It never resolves conflicts, merges, or pushes to a PR branch. A new fast-fail CI job validates `.changelog/` fragments in under a second, ahead of the full Unit-tests lap, so a malformed fragment fails fast instead of after a full suite run.
|
|
141
|
+
|
|
142
|
+
- **Add autofixes for mechanical ast-grep rules (refs #1850)**
|
|
143
|
+
|
|
144
|
+
- **Record host-ready delay (refs #1859)** — Add latency telemetry for the monotonic interval from extension load-complete to the first `session_start`, including a host-stall suspicion flag.
|
|
145
|
+
|
|
146
|
+
- **Three CI smoke lanes for the lifecycle/scope defect classes of the 2026-08 arc (refs [#1605](https://github.com/apmantza/pi-lens/issues/1605))** — a new nightly `.github/workflows/lifecycle-smoke.yml` runs the real production code path (no mocks) against a real spawned process for three shapes that #1531–#1604 shipped and no existing smoke lane could catch: (1) PSScriptAnalyzer real-host `-File` classification under a forced `PSExecutionPolicyPreference=Restricted` on a real Windows runner (`scripts/smoke-psscriptanalyzer-classification.mjs`, both a `pwsh` leg and a Windows PowerShell 5.1 leg), verifying #1540's fix and probing #1604's open pwsh-7-`ConciseView` worry; (2) the availability-lifecycle latch class (#1490/#1494/#1495/#1496/#1537/#1535), driving zizmor's real `gh auth token` probe through a fixture `gh` shim (never the real `gh` CLI) to prove a transient failure records one degradation-ledger entry, then genuinely recovers across a real ~30s cooldown (`scripts/smoke-availability-lifecycle.mjs`); and (3) gitleaks's scratch-tree exclusion scope (#1562/#1574) against a real gitleaks binary and a fixture repo with a fake secret in a tracked file and another under a gitignored secrets-lane scratch dir, asserting exactly the tracked hit surfaces (`scripts/smoke-gitleaks-scratch-exclusion.mjs`). Each script is proven capable of failing: a targeted mutation reinstating the mapped defect turns every lane red before the fix, green after (see the PR body for the red-proof transcripts). Lanes 3/5/6 from the #1605 umbrella are out of scope here and tracked separately.
|
|
147
|
+
|
|
148
|
+
- **State-machine property tests for the availability latch (refs #1609)** — `tests/clients/dispatch/runners/availability-latch-properties.test.ts` runs a seeded, deterministic PRNG over random sequences of probe outcomes, time advances, session-start re-arms and caller retries against `createAvailabilityLatch`/`classifyProbeFailure`, asserting five invariants after every step: never durably latched on transient-only evidence, re-arm reachable at session_start, cooldown ladder monotone and capped, recovery reachable from every state, and degradation recorded exactly once per episode.
|
|
149
|
+
|
|
150
|
+
- **Reset-interleaving test explorer (refs #1840)** — `tests/support/reset-explorer.ts` runs an async path under test once per await point it exposes, firing a session-reset hook at that exact point each time, and checks caller-declared invariants after every run. The session-straddling defect shape (a reset landing mid-await, a budget re-arming, a write landing in the wrong session) recurred five times in one review window and every instance was caught only because a reviewer picked one await point by hand; this makes that exhaustive instead of lucky. Adopted on `clients/installer/managed-tool-refresh.ts`'s walk (the #1746 R2-F1 shape) as the reference consumer.
|
|
151
|
+
|
|
152
|
+
### Changed
|
|
153
|
+
|
|
154
|
+
- **Anti-slop rules: one promotion to `error`, six deliberate holds ([refs #1727](https://github.com/apmantza/pi-lens/issues/1727), [refs #1777](https://github.com/apmantza/pi-lens/issues/1777))** —
|
|
155
|
+
`require-safety-comment-for-as-unknown-as` moves from `hint` to `error`,
|
|
156
|
+
the tier that blocks a turn. Two changes earn that. The rule now excludes
|
|
157
|
+
test paths structurally, because a four-corpus census (pi-lens, pi core,
|
|
158
|
+
opencode, oh-my-pi) found 79–95% of `x as unknown as T` sites live in test
|
|
159
|
+
doubles; and each of the 16 remaining `clients/` casts was read and given a
|
|
160
|
+
truthful `SAFETY:` comment naming the invariant, with the one unjustified
|
|
161
|
+
cast replaced instead (`clients/runtime-context.ts` no longer casts `null`
|
|
162
|
+
into a cache entry's type). `no-chained-type-assertions` drops its
|
|
163
|
+
`as unknown as` arm, so the two rules now partition the assertion space
|
|
164
|
+
rather than both reporting the same site — every uncommented cast used to
|
|
165
|
+
raise two diagnostics for one defect. Both rules are wired into the CI
|
|
166
|
+
self-scan, which holds pi-lens's own tree at zero.
|
|
167
|
+
|
|
168
|
+
The other six #1727 rules — `no-known-value-widening`, `no-runtime-typeof`,
|
|
169
|
+
`no-shape-in-symbol-names`, `no-unknown-parameters`, `no-unknown-returns`,
|
|
170
|
+
`no-unsafe-dictionary-unknown` — stay at `hint`. Structural narrowing was
|
|
171
|
+
attempted on each and none reached a clean census; the residuals run from
|
|
172
|
+
3 to 3,239 legitimate hits per corpus. Every rule's note now carries its
|
|
173
|
+
four-corpus numbers and the reason it stopped, and AGENTS.md's severity
|
|
174
|
+
policy records the promotion procedure those censuses follow.
|
|
175
|
+
|
|
176
|
+
- **Credit external contributors ahead of v4.1.0** — Add bcachet, EarthChen,
|
|
177
|
+
Don-Yin, ELA718, sujeito-operator, and danblah to the contributors table,
|
|
178
|
+
and restore six contributors (3choBoomer, white-hat, marvtub, mjfaga,
|
|
179
|
+
aeturnal, floatGray) that the table listed but `.all-contributorsrc` had
|
|
180
|
+
dropped.
|
|
181
|
+
|
|
182
|
+
- **Docs: agent-guide reflects the stale-secrets ACTION NEEDED tier (refs #1522)** — `docs/agent-guide.md`'s honesty-contract table and blockers-vs-advisories section now describe the `🔑 ACTION NEEDED` tier #1627 added: a secrets finding whose cached file changed since the scan demotes to this tier, with its line number withheld, instead of dropping silently or reading as a plain advisory. CUE's LSP/formatter/tree-sitter documentation (`docs/features.md`, `docs/language-coverage.md`) was already brought current by #1520; this fragment covers the sweep verifying those pages and the wider today's-merges gap check, which found the rest of the surfaces (README.md, `docs/servercapabilities.md`'s generation note) already accurate.
|
|
183
|
+
|
|
184
|
+
- **Codified live-binary discipline in AGENTS.md, per maintainer directive** — new defect shape 16 requires every claim about an external tool's exit codes, output shape, or severity vocabulary to be verified against a real run before it ships in code, comments, tests, or rule notes, and requires tool-output fixtures to be captured from real runs, never hand-written. The rule generalizes a pattern from six PRs caught by review in one window: trivy's `--no-progress` rejection breaking two lanes — one read the usage text as clean, the other failed on every run (#1757/#1781), vulture's real exit code 3 vs. the assumed 1 (#1765), rustc/clippy's six severity levels vs. an assumed two (#1802/#1809), mypy's exit-2 syntax diagnostics (#1822), and biome/pyright field-shape mismatches (#1810/#1809). Also added a one-sentence cross-reference in `skills/pi-lens-write-ast-grep-rule/SKILL.md`'s fixture-validation section.
|
|
185
|
+
|
|
186
|
+
- **Ast-grep authoring skills now teach the `stopBy` boundary idiom, the four-tier severity policy, and suppression-fixture discipline** (refs #1794).
|
|
187
|
+
|
|
188
|
+
- **Added the 2026-08-17 through 2026-08-20 recurring defect shapes to the agent catalog** — reconciled the review arc into the existing path-key, process-lifetime, freshness, vacuous-test, and class-sweep guidance, and added eight distinct screens for session latches, cooldown cadence, correlation, provenance, concurrent generations, session-straddling writes, walk cursors, and shared-field writers.
|
|
189
|
+
|
|
190
|
+
- **docs: language-coverage lists mypy in the Python row** — mypy has been wired in dispatch (config-first, alongside pyright) since the plan.ts integration, but the coverage table never recorded it. The 2026-08-20 tool-gap survey's considered-and-skipped decisions (bandit, checkov, radon) are recorded in the table's notes so they are not re-litigated.
|
|
191
|
+
|
|
192
|
+
- **AGENTS.md: six new PR-discipline rules from the v4.1.0 review arc** — mutation-proof guards, governance suites after every master merge, composition proven by evidence on overlapping PRs, deferrals re-homed to issues, `file:line` for cited precedents, and bounded observability records that preserve identity; the blast-radius rule now requires measured cost deltas on hot paths.
|
|
193
|
+
|
|
194
|
+
- **One helper for bounded telemetry, and a sweep that keeps failure paths on it (refs [#1743](https://github.com/apmantza/pi-lens/issues/1743))** — four PRs in two days hand-built the same machinery: a failure path that needs a detailed `latency.log` record, an exact per-identity tally, and a bound so a storming subsystem cannot flood the log. `emitBounded`/`admitBounded` (`clients/bounded-telemetry.ts`) are now that machinery, and they are the intersection of those four, not a new framework. The rising edge is derived from the degradation ledger's own tally, so there is no second latch to re-arm at `session_start`. A per-turn cap carries the caller's turn index in the same object as the limit, so a cap nobody resets is unrepresentable. Every record stamps the discriminating identity into `metadata.identity`, so aggregating by phase still answers which file, method, or record is stuck. All four sites migrate behavior-identical: the orphan-forensics batch (#1705), the pull timeout and late-answer records (#1713), the navRequest rising edge per method and file (#1716), and the loop-block record (#1723). A new sweep, `tests/clients/bounded-telemetry-sweep.test.ts`, walks every `logLatency` call site and requires each failure-shaped phase to be either helper-emitted or listed with the mechanism that already bounds its volume, so a new unbounded failure-path record fails the build instead of shipping. Review round: the two per-file LSP skip records (`lsp_client_skipped_broken`, `lsp_client_skipped_unavailable_command`) also move to the helper — during an outage they fired once per file per touch, so a sweep over a few hundred files wrote a few hundred records describing one fact. They are now rising-edge per (server, file), with the exact totals in the ledger under the new `lsp-client-skipped-broken` and `lsp-client-skipped-unavailable-command` kinds. The sweep's predicate widened to catch them: outcome suffixes now include `_broken`, `_leak`, `_exit`, `_missing`, and `_abandoned`, and a new infix rule catches an outcome named mid-phase with its reason trailing, such as `lsp_client_skipped_unavailable_command`. `tools/` joined the scanned roots. The scanner uses #1755's shared `stripSource`, so a call named only in a comment is not read as a call.
|
|
195
|
+
|
|
196
|
+
- **One generation guard instead of four hand-rolled ones** — Add
|
|
197
|
+
`clients/generation-guard.ts`: a `GenerationSource` counter owned by the
|
|
198
|
+
seam that resets a store, plus a handle whose `guardedWrite` drops a
|
|
199
|
+
post-await write when the world it captured is gone, and records the drop in
|
|
200
|
+
the degradation ledger under `generation-guard-stale-write`. A keyed
|
|
201
|
+
`GenerationMap` covers stores invalidated per cwd or per request. Dispatch
|
|
202
|
+
availability and the LSP workspace-diagnostics cache now guard through it;
|
|
203
|
+
a test sweep requires every remaining hand-rolled generation compare to
|
|
204
|
+
carry a written reason.
|
|
205
|
+
|
|
206
|
+
- **Consolidated three duplicated test-fixture families onto shared support helpers (refs #1816)** — a 2026-08-20 survey found the same fixture hand-rolled dozens of times across the suite, each copy free to drift from the others and from the real interface it doubled. `tests/support/fake-child.ts` gives `safe-spawn.ts`'s child-process race tests one faithful `makeFakeChild` (a real `EventEmitter` with real `stdout`/`stderr` streams and a `kill()` that sets `killed = true`, matching Node's real behavior) instead of four separate copies, three of them hand-rolled `{on, emit}` objects that skipped the real `EventEmitter` seam the #1673 fidelity fix exists to exercise; `tests/clients/lsp/launch.test.ts`'s six verbatim `MockChildProcess` classes collapse to one parameterized by pid. Sixteen dispatch-runner test files' hand-rolled `createCtx` builders now delegate to `tests/support/runner-ctx.ts`'s `makeRunnerCtx`, resolving a `getFlag` divergence (`false` vs `undefined` across copies) by confirming — pre- and post-migration counts matched exactly (174 passed, 1 skipped in both) — that the only production read of `pi.getFlag` treats both as equally falsy. `tests/support/with-env.ts` gives `process.env` save/restore a single correct implementation (handles the "wasn't set before" case, unit-tested and mutation-proofed) and replaces four incompatible hand-rolled idioms across the `PI_LENS_DISABLE_TOOL_INSTALL` test files; two of those files' `afterEach` hard-restored the literal `"1"` instead of whatever was actually ambient before the file ran, correct only because `vitest-setup.ts` happens to default to `"1"` — `withEnv` fixes that as a side effect of the consolidation.
|
|
207
|
+
|
|
208
|
+
- **Workspace-scope LSP queries identify their serving client ([refs #1854](https://github.com/apmantza/pi-lens/issues/1854))** —
|
|
209
|
+
the existing `lsp_navigation_result` latency record now names the LSP server
|
|
210
|
+
that answered each no-path workspace query. Aggregated operation support
|
|
211
|
+
records a bounded per-capability contributor map, so multi-primary routing
|
|
212
|
+
defects are visible without adding another log surface.
|
|
213
|
+
|
|
214
|
+
- **Turn-end madge circular-dependency check is opt-in, default off (closes [#766](https://github.com/apmantza/pi-lens/issues/766))** — the per-turn-end madge pass only produced debug output while adding tail latency to every turn; it now runs only with `--lens-turn-end-madge` (or `turnEnd.madge.enabled: true`). User-facing madge diagnostics still come from the session-start project scan and `lens_diagnostics`.
|
|
215
|
+
|
|
216
|
+
- **Narrow `no-non-null-assertion` false positives (refs #1777)** —
|
|
217
|
+
`$M.get($K)!` guarded by `$M.has($K)` in the same function, and
|
|
218
|
+
`$A.pop()!`/`shift()!` inside a `while`/`for`/`if` whose condition checks
|
|
219
|
+
`$A.length`, no longer fire; both exclusions require the same receiver
|
|
220
|
+
and key/array metavariable binding, so a check on a different key, map,
|
|
221
|
+
or array still flags. The rule stays at `warning` — the re-census showed
|
|
222
|
+
the residual is still idiom-heavy rather than dominated by genuine
|
|
223
|
+
cross-boundary risk, so this is a precision fix, not a tier change. See
|
|
224
|
+
the rule's `note:` for the full four-corpus census.
|
|
225
|
+
|
|
226
|
+
- **A cascade that a slow prelude pushed out of the turn-end window now shortens its neighbour walk to land on time, when shortening it actually helps (refs [#1462](https://github.com/apmantza/pi-lens/issues/1462))** — the flat 40-neighbour cap and the 5000 ms turn-end settle wait were set in two different files with nothing relating them, and dogfood on 2026-08-15 measured what that costs: 50 cascades, median 30 ms, max 3.88 s at 40 neighbours, and a `reverse_deps_cache` refresh ahead of the graph build (2046 ms for `logger.ts`) put both tail runs past the cap. A run that misses the cap is not lost — `settleCascadeRuns` re-parks it, `agent_settled` gives it a second and more generous 15 s drain, and `beginTurn` keeps it pending until it resolves ([#1443](https://github.com/apmantza/pi-lens/issues/1443)) — so the finding arrives a turn late rather than never. That makes the real trade the full neighbour set one turn late against a shorter set on time with the remainder lost for good, and narrowing is only worth paying for when it genuinely converts a late run into an on-time one. The production pipeline now sizes the walk from the live `turn_end` settle clock when available; when that boundary is not observable, it keeps the flat cap, and direct legacy/test callers retain the old fallback. Budget-truncated runs are surfaced as incomplete advisories. In the production path, this gives three zones: the full walk still fits and nothing changes; the walk no longer fits but a shorter one does, which is the rescue band and the only place anything is narrowed; or not even a floor-sized walk fits any more, where the flat cap is kept and the carry-over delivers the whole set late — the cold-session case, since a fresh graph build measures up to ~19 s and a floor-sized stub there would drop 35 neighbours permanently and still miss. A fourth guard keeps `PI_LENS_CASCADE_SETTLE_WAIT_MS` from doubling as a neighbour-count knob: if the configured window could not fit a full walk even at zero prelude there is no late run to rescue, only every run to shrink, so the derivation stands down. At zero pre-turn gap the band is a settle-window elapsed time of 1.0–4.5 s, so the measured median 30 ms cascade is untouched and truncation stays confined to that band. [#1446](https://github.com/apmantza/pi-lens/issues/1446)'s `cascade_result` telemetry carries the evidence: `neighborBudget` is now the budget in force, alongside `neighborBudgetCeiling`, `budgetRemainingMs`, `budgetZone` (which of the four rules decided it) and `budgetDeliveryWindowMs` (the settle wait plus the quiet-window drain, so the whole deadline stack is on the record). Note when reading those rows that `budgetTruncated` inverts under a narrowed budget — it is eligible minus budget, so a smaller budget mechanically raises it; compare truncation within a zone, never across one. The per-neighbour cost the derivation divides by is an estimate (`PI_LENS_CASCADE_NEIGHBOUR_COST_MS`, default 100 ms against a measured 97 ms), and `npm run bench:cascade-budget -- --sweep` reports how the rescue degrades if that estimate is materially low. Maintainer follow-up: the settle clock now measures elapsed time against the coordinator's actual `turn_end` wait, not the compute's own age from the write, which could under-count by the full write-to-turn_end gap and narrow walks that had ample real headroom; raising `PI_LENS_CASCADE_NEIGHBOUR_BUDGET` far enough to silently disarm the rescue band now names itself once per session in the degradation ledger instead of failing silently.
|
|
227
|
+
|
|
228
|
+
- **Promote laundering, unknown-return, and Reflect rules to errors (refs #1856)** — Clean the remaining named return boundaries and structurally exempt standard Proxy trap forwarding before making these rules blocking.
|
|
229
|
+
|
|
230
|
+
- **Consolidate the registered-or-fail sweep machinery** — Add
|
|
231
|
+
`tests/support/sweep-kit.ts`, one home for the four pieces every sweep in
|
|
232
|
+
this repo hand-rolled: comment/string stripping with the string policy as a
|
|
233
|
+
caller option, registry semantics (registered-or-fail, exemptions that
|
|
234
|
+
require a reason, stale-entry self-detection), tag and evidence binding
|
|
235
|
+
(one seam per tag, call-shaped needles, nearest-exclusive assignment), and
|
|
236
|
+
the emptiness guard that fails a scan matching nothing. The kit's doc names
|
|
237
|
+
the attack catalogue the finding-delivery gate paid four review rounds to
|
|
238
|
+
learn, and its test suite carries one named fixture per attack. The
|
|
239
|
+
session-state conformance sweep now runs on the kit.
|
|
240
|
+
|
|
241
|
+
- **Turn-simulation harness and a session-state lifecycle registry (refs #1635)** — two pieces of test infrastructure for the two bug families that dogfooding still catches exclusively. The turn harness (`tests/support/turn-harness.ts`) scripts a sequence of edits, scans and turn ends against a real `handleTurnEnd`, on a logical clock and a temp-dir project, and returns what the agent actually sees at each turn end split into its real tiers — blocker, action-needed, advisory. Four incidents from 2026-08-18 land as its first scenarios. The gitleaks stale-replay scenario runs green and fails against the code from before #1627; the knip stale-cache scenario drives the real `KnipClient` against a real knip and passes now that #1637 has landed; the two whose fixes are not on master are skipped with the blocking issue named rather than rewritten to assert today's behavior. The session-state registry (`tests/support/session-state-registry.ts`) gives every session-scoped latch, hold, memo and dedupe set a declared reset policy, proves five of them re-arm when their reset runs, and checks each one's reset is genuinely reachable from `handleSessionStart` — derived from the source, not from a hand-kept list, because a reset nobody calls is exactly the shape of eight bugs in this arc. A derived sweep flags any `clients/` file holding module-level state paired with a reset seam that is neither registered nor exempted with a reason, and states plainly what its heuristic cannot see.
|
|
242
|
+
|
|
243
|
+
- **Test fixtures now match the real host's `session_start`/`tool_result` event shape (refs #1681)** — `tests/index-wiring.test.ts` fired `session_start` with `{ sessionId: "wiring-session" }` as the event payload; the real pi host never puts `sessionId` on that event (`packages/coding-agent/src/core/extensions/types.ts:561-568` in the host source — only `type`, `reason`, `previousSessionFile`), and `index.ts`'s own handler already documents this ("the STABLE session id comes from the session manager — the event carries none"). Fixed to `{ reason: "startup" }`, host-faithful, with `sessionId` staying where it belongs: on the mock `ctx` via `makeCtx`. A new scan, `tests/support/host-event-shape-scan.ts` plus its conformance test, pins every `session_start`/`tool_result` fixture repo-wide against `sessionId`/`provider`/`model` ever landing on the event object again — the same drift the umbrella issue's earlier `#1680` fix round hit, now caught mechanically instead of by a human noticing.
|
|
244
|
+
|
|
245
|
+
- **Pinned trivy's dependency-CVE disposition filter with a regression test (refs #1813)** — `trivyFindingsFiltered` (`clients/runtime-turn.ts:1516-1523`) filters trivy's CVE lane through the same `filterFindingsByDisposition` machinery as the five lanes #1798 already covered, but had no test proving it. The #1798 delta-verify neutered it back to the raw unfiltered cache read and 407 tests across 18 files stayed green. `tests/clients/runtime-turn-secrets-disposition.test.ts` now marks a CRITICAL trivy CVE false-positive through `markDisposition` and proves it stops rendering in the `handleTurnEnd` blocker tier, with the suppression staying visible as a `suppressed by disposition: ... (trivy 1)` trace.
|
|
246
|
+
|
|
247
|
+
### Fixed
|
|
248
|
+
|
|
249
|
+
- **Reach never-swept monorepo caches on workspace refresh ([closes #1707](https://github.com/apmantza/pi-lens/issues/1707))** —
|
|
250
|
+
a nested language-server root now clears its enclosing workspace-diagnostics
|
|
251
|
+
cache through the bounded session workspace-root chain.
|
|
252
|
+
|
|
253
|
+
- **LSP navigation verifies capabilities at the client boundary ([closes #1826](https://github.com/apmantza/pi-lens/issues/1826))** —
|
|
254
|
+
definition, hover, rename, code actions, and the remaining file-scoped
|
|
255
|
+
navigation methods now reject unsupported requests before dispatch. This
|
|
256
|
+
second check preserves the tool-layer gate and distinguishes unsupported
|
|
257
|
+
servers from supporting servers that return no results.
|
|
258
|
+
|
|
259
|
+
- **Distinguish a slow LSP warm spawn from no clients ([closes #1875](https://github.com/apmantza/pi-lens/issues/1875))** —
|
|
260
|
+
a read warm whose bounded wait expires now reports that its primary server
|
|
261
|
+
is still spawning. A touch reports no clients only when no matching spawn
|
|
262
|
+
remains in flight. The next read reuses the existing single-flight spawn.
|
|
263
|
+
|
|
264
|
+
- **Avoid doomed waits for historically slow LSP spawns ([refs #1884](https://github.com/apmantza/pi-lens/issues/1884))** —
|
|
265
|
+
a bounded warm touch now returns immediately when its matching server spawn
|
|
266
|
+
is in flight and a prior successful spawn took more than twice the budget.
|
|
267
|
+
Servers without history retain the existing bounded wait.
|
|
268
|
+
|
|
269
|
+
- **Re-arm direct-LSP and installer availability caches ([closes #1897](https://github.com/apmantza/pi-lens/issues/1897))** —
|
|
270
|
+
the primary `session_start` clears negative direct-LSP cooldowns and
|
|
271
|
+
resolved installer paths, so tools added between sessions are probed again.
|
|
272
|
+
|
|
273
|
+
- **Bound the cascade tier-3 reconcile backlog and drop the dead neighbour-touch cache ([closes #1899](https://github.com/apmantza/pi-lens/issues/1899))** —
|
|
274
|
+
the outstanding-touch registry now caps its entry count and its entry age
|
|
275
|
+
instead of growing until a quiet window arrives, every sweep writes a backlog
|
|
276
|
+
gauge to `cascade.log`, and an unresolved touch says which of the five causes
|
|
277
|
+
kept it unresolved. The same-write neighbour-touch cache is removed: its read
|
|
278
|
+
gate could only pass inside a single write, so it measured 0 hits across 236
|
|
279
|
+
cold touches.
|
|
280
|
+
|
|
281
|
+
- **Avoid false read-guard blocks after formatter writes ([closes #1903](https://github.com/apmantza/pi-lens/issues/1903))** —
|
|
282
|
+
Bash-invoked in-place formatters and fixers now refresh explicit file write
|
|
283
|
+
stamps. A uniquely resolved edit `oldText` also overrides coarse FileTime
|
|
284
|
+
staleness, while missing or ambiguous content evidence remains blocked.
|
|
285
|
+
|
|
286
|
+
- **Credit search hits with the lines the search actually showed ([closes #1904](https://github.com/apmantza/pi-lens/issues/1904))** —
|
|
287
|
+
a bare `grep -n` hit registered as a 5-line read, so the read-guard let edits
|
|
288
|
+
pass against lines the model never saw. A hit now credits its match line, plus
|
|
289
|
+
the context the command printed when it carried `-A`, `-B`, or `-C`. Each
|
|
290
|
+
record states the margin it credited and why. The range-snapshot ledger also
|
|
291
|
+
reports the caller's outcome (enforced or bypassed by content match) instead
|
|
292
|
+
of only its intent, and the per-file read store is bounded at 128 records.
|
|
293
|
+
|
|
294
|
+
- **A size-capped write no longer says both "attached" and "too large to attach" ([closes #1590](https://github.com/apmantza/pi-lens/issues/1590))** —
|
|
295
|
+
two layers used to phrase the post-autofix instruction. The pipeline claimed
|
|
296
|
+
the attached content was authoritative whenever autofix changed the target
|
|
297
|
+
file, and the tool-result layer, the only one that sees the 2 MiB attachment
|
|
298
|
+
cap and the per-command aggregate budget, appended "you must re-read, the
|
|
299
|
+
content is too large to attach" for that same file. A write whose post-fix
|
|
300
|
+
content exceeded the cap carried both sentences, and a multi-file bash write
|
|
301
|
+
past the shared budget did the same for the degraded path.
|
|
302
|
+
|
|
303
|
+
The pipeline now returns the changed-file data instead of a verdict, and
|
|
304
|
+
`handleToolResult` renders the one sentence from the one decision it owns.
|
|
305
|
+
The shared budget is threaded into each synthetic per-file call, so a bash
|
|
306
|
+
write decides each attachment once rather than attaching and then overruling
|
|
307
|
+
itself. The telemetry follows: `authoritative_content_attachment_decision`
|
|
308
|
+
logs one row per path, carrying `attached`, `size-capped`, or
|
|
309
|
+
`aggregate-budget-degraded`, where a degraded path used to log two rows and
|
|
310
|
+
rely on the later one winning.
|
|
311
|
+
|
|
312
|
+
- **Preserve biome's info/hint severity in dispatch** — `biome-check`
|
|
313
|
+
collapsed every non-error biome diagnostic to `warning`, even though
|
|
314
|
+
biome's own JSON declares `information` and `hint` tiers. Biome findings
|
|
315
|
+
now carry the same four-tier `Diagnostic.severity` ast-grep-napi adopted
|
|
316
|
+
in #1787: `information` maps to `info`, `hint` stays `hint`. Only `error`
|
|
317
|
+
still blocks. The on-demand ast-grep CLI path's `formatDiagnostics`
|
|
318
|
+
summary also gained an `info(s)` bucket — an info finding was previously
|
|
319
|
+
counted in the total but named in no tier line.
|
|
320
|
+
|
|
321
|
+
- **cuelsp's silent clean-open no longer reads as inconclusive (refs #1522, #1519)** — Measured directly against the real `cue lsp serve` v0.17.1 binary: a cold `didOpen` on an already-clean `.cue` file publishes nothing at all inside the wait budget, so a touch used to time out and report `inconclusive`. `SERVER_DIAGNOSTIC_STRATEGIES.cue` now marks it `silentOnClean`, so the shared push-only clean-confirm gate reads "no publish, notify succeeded" as confirmed clean — matching the behavior already shipped for typescript and marksman. An edited document still publishes normally (both the error and the empty array that clears it), so this only changes the cold-open case.
|
|
322
|
+
|
|
323
|
+
- **Preserve ast-grep rule severity tiers through dispatch** — The dispatch
|
|
324
|
+
runner collapsed every `warning`, `hint`, and `info` rule severity to
|
|
325
|
+
`warning`, so the quiet tier that 43 shipped rules declare did not exist
|
|
326
|
+
downstream. All four tiers now reach `Diagnostic.severity`. The turn-end
|
|
327
|
+
advisories say how many findings are hint or info, the code-quality report
|
|
328
|
+
spends its cap on warnings before hints, and hint-tier rules that carry a fix
|
|
329
|
+
still route to actionable warnings. The `lens_diagnostics` and TUI-footer
|
|
330
|
+
tallies count hint and info alongside warning, so a hint-only file is never
|
|
331
|
+
reported as clean. Only `error` blocks, exactly as before.
|
|
332
|
+
|
|
333
|
+
- **Persist degradation-ledger decisions in `latency.log`** — every accepted
|
|
334
|
+
once-record and every tally increment now writes a bounded
|
|
335
|
+
`degradation_ledger` row with its kind, subject, and current count. Scanner
|
|
336
|
+
coverage gaps and stalled LSP notify barriers now enter the ledger too, so a
|
|
337
|
+
session remains auditable even when no degradation summary reaches the
|
|
338
|
+
transcript.
|
|
339
|
+
|
|
340
|
+
- **A write that already handed back its post-fix content no longer also tells you to re-read it (closes [#1464](https://github.com/apmantza/pi-lens/issues/1464))** — since #1414 a `write` whose immediate autofix changed the file gets the full authoritative content attached to its own tool result, but the next turn still opened with "N file(s) were autofixed after your last turn: a.ts — re-read before editing". The nudge's relevance filter admits any path with read or edit history, and a file the agent just wrote is in edit history by definition, so the one case where the agent demonstrably holds the current bytes was the case that nudged hardest. The write path now tells the nudge accumulator what it delivered (`noteAuthoritativeContentAttachment`), passing the same boolean that drove the attachment rather than re-deriving the size cap or the per-command budget in the nudge layer. Suppression is exactly as narrow as the delivery: a size-capped attachment, a bash multi-file write whose aggregate budget degraded a per-file "attached" back to a re-read warning, a side-effect file the autofix changed but did not attach, and deferred `edit` autofix at `agent_end` all keep nudging. A later touch of the same path re-arms it too — the default deferred format at `agent_end` makes those attached bytes stale, and stale bytes are worth a re-read. The `agent_nudge` phase gains `filesContentDelivered` so over-suppression is auditable instead of only showing up as silence.
|
|
341
|
+
|
|
342
|
+
- **Knip's per-turn delta no longer reads a shifted finding as newly unused ([#1483](https://github.com/apmantza/pi-lens/issues/1483))** — the finding key included the line number, and the delta is filtered to exactly the files the edit touched, so lines shifted by that same edit landed right where the filter looks. A finding that only moved down now keys the same as before and drops out of the delta; a genuinely new finding still appears. The key now shares `stableFindingKey` with the dead-code delta ([#1477](https://github.com/apmantza/pi-lens/issues/1477)), which had the identical bug and the identical fix — one rule, one place, for both scanners.
|
|
343
|
+
|
|
344
|
+
- **A stalled PowerShell check no longer disables PSScriptAnalyzer for the rest of the session (closes #1490)** — The runner ran its own child processes to find a PowerShell interpreter and check for the PSScriptAnalyzer module, and every failure looked the same to it: a timeout, a Windows spawn error and a genuinely missing binary all reported nothing at all. Any of them was remembered as "PowerShell analysis is not available" until pi-lens restarted. Both probes now go through the shared spawn layer, so a timeout is distinguishable from an absence, and the verdict is owned by the same availability policy as knip, madge, Go and Rust: a stall is retried after a short wait, and only a genuine absence sticks. Each decision lands in `latency.log` as an `availability_decision` entry with its cause and timing.
|
|
345
|
+
|
|
346
|
+
- **A silent auxiliary no longer reads as confirmed clean (closes [#1493](https://github.com/apmantza/pi-lens/issues/1493))** — An auxiliary scanner that published nothing inside its own budget now narrows the touch's verdict to `confirmation: "partial"` and is named in `unconfirmedServerIds`, the same treatment #1470 gave a scanner cut off by the aux grace timer. Before this, silence only stayed honest when the scanner was the sole auxiliary and burned the whole touch deadline; with a second auxiliary answering fast, the wait settled early and the silence went unrecorded, so a touch claimed a clean bill of health for a server that said nothing. Both no-answer shapes now run through one policy, `auxiliaryCoverageGap` in `clients/lsp/diagnostic-binding.ts`. Genuinely clean files are unaffected: a scanner that ran to budget and published an empty set advances `diagnosticsVersion`, so it reads as `answered` and its touch stays `confirmed`. So does an unreporting scanner — silent or cut off — whose stored publication is already bound to the touch's exact content. Where the verdict does narrow, `pi-lens lsp-diagnostics` adds an "Auxiliary coverage INCOMPLETE" line naming the scanner, and the dispatch LSP runner reports an empty result as `skipped` rather than clean. `lsp_touch_file` carries the new `auxUnconfirmedServerIds` field, which joins to the `silent` rows in `lsp_aux_wait_outcome`.
|
|
347
|
+
|
|
348
|
+
- **A stalled version check no longer disables eslint, credo or clippy for the rest of the session (closes #1494)** — The per-directory probe these three runners share cached its verdict forever, so a single `eslint --version` that timed out on the first JS/TS save silently dropped eslint for that project until pi-lens restarted. Credo's 10-second `mix credo --version` on a cold BEAM and clippy's 8-second `cargo clippy --version` had the same failure mode, and clippy went further by treating the timeout as grounds for an install attempt. The verdict now goes through the shared availability policy from #1467/#1476: only a genuine absence sticks, while a timeout, abort or host stall is retried after a short cooldown. ESLint autofix in the post-write pipeline carried its own copy of the latch and is fixed the same way. Every decision is recorded in `latency.log` as an `availability_decision` entry with its cause, timing and retry window.
|
|
349
|
+
|
|
350
|
+
- **A stalled PATH lookup no longer disables a formatter until you edit a config file (closes #1495)** — Detecting rustfmt, shfmt, zig, dart, nixfmt, gofmt and about a dozen other formatters runs `which`/`where` on a 5-second budget. A single timeout dropped the formatter and then wrote that empty result into the detection cache, which is only invalidated when a formatter config file changes size or timestamp — so formatting stayed silently off for the rest of the session, looking to you like pi-lens deciding the file needed no formatting. PATH lookups now go through the shared availability policy: a timeout is retried after a short wait, only a genuine absence sticks, and an empty detection caused by a stalled probe is no longer cached at all. Editing a config file still re-checks PATH, so installing a formatter mid-session works as before. Each lookup is recorded in `latency.log` as an `availability_decision` entry with its cause.
|
|
351
|
+
|
|
352
|
+
- **A stalled package-manager check no longer downgrades pi-lens's own managed installs to npm (closes #1496)** — Checking which package manager is installed spawned a `where`/`which` probe on a 5-second budget, and a timeout was remembered as "not installed" for the rest of the session. The blast radius was internal: the resolver only serves installs into pi-lens's managed tools directory and `pilens_rebuild` on a pi-lens source checkout, never a user project's lockfile. A timed-out check is now retried after a short wait instead of latching, matching the #1467/#1476 policy already used for knip, madge, govulncheck, vulture, biome, ast-grep, Go, and Rust. Each decision is recorded in `latency.log` as an `availability_decision` entry with its cause and timing.
|
|
353
|
+
|
|
354
|
+
- **A repeatedly timing-out `go install` no longer re-compiles govulncheck every few minutes forever (closes #1497)** — #1489 rightly made a timed-out install transient, but it inherited the probe-class retry schedule (30 s doubling, capped at 5 min), calibrated for a 1.5–5 s version probe. On a host where the 60 s `go install` reliably exceeds its budget, steady state was a 60 s compile every 5 minutes with no terminal state. Install-class transient failures now escalate on their own two-rung ladder (5 min, then 10 min) and give up on the third attempt, spacing at most three ≤60 s compiles across ~15 minutes. Install-class failures also hold their own cooldown slot, so a cheap version probe failing in the meantime can no longer pull the next compile forward. The ceiling is terminal for the SESSION, not the process: it re-arms at the next `session_start` (or on a successful run), so a genuinely transient network failure still recovers without a host restart. When the ceiling is reached, the verdict says so — the degradation ledger records `install-retry-exhausted` (visible via `pilens_health`), the `availability_decision` row is marked latched with the install facts behind it, and the user-facing message reports a retry ceiling that clears next session instead of promising a retry that will not come.
|
|
355
|
+
|
|
356
|
+
- **Tool-availability records now show what actually failed, and a failed auto-install is no longer a silent verdict (closes #1500)** — When pi-lens decides a tool is unavailable it writes one `availability_decision` line to `latency.log`. Those lines reported the verdict but not the evidence, so "the tool is missing" looked identical whether a probe returned "not found" or an auto-install had just failed for a reason that would clear on a retry. Each line now carries the raw spawn facts (exit status, failure kind, errno, whether an install was attempted) and says whether the classification was derived from a probe or asserted by the caller. Security-scanner clients — gitleaks, trivy, opengrep, govulncheck — used to latch "not installed" after a failed install without recording anything at all; that write is now recorded like any other decision.
|
|
357
|
+
|
|
358
|
+
- **Transient security-scanner probe records now carry their retry schedule (closes #1501)** — When a `gitleaks`/`trivy`/`opengrep`/`govulncheck` version probe timed out, the `availability_decision` record in `latency.log` said the tool was off but not when it would be retried, because the record was emitted before the caller decided the cooldown. The shared seam now owns the transient cooldown and logs `retryAfterMs` alongside the existing outcome, cause, and timing fields, matching the shape the install and scan paths already emitted. Success and durable-absence records are unchanged.
|
|
359
|
+
|
|
360
|
+
- **Venv-resolved tool paths are no longer wrapped in literal quotes (closes #1508)** — When a linter such as `ruff` resolved through a project virtualenv (`.venv/bin/ruff`), the availability seam returned the path wrapped in double-quote characters — a leftover from the `shell: true` spawn era. Every spawn now runs with `shell: false` (#817), so the quotes became part of the filename, the probe failed with ENOENT, and the runner silently reported no diagnostics. The path is now returned verbatim on every platform, and the resolved command is pinned quote-free by regression tests. Reported by @phionax.
|
|
361
|
+
|
|
362
|
+
- **Auxiliary diagnostic waits and their outcome rows are now decided per file (closes [#1531](https://github.com/apmantza/pi-lens/issues/1531))** — `diagnosticsVersion` is a per-client counter that any file's publication advances, so on a client shared by two files in flight an unrelated publication both ended this file's wait early and read as evidence that the scanner had answered for it. Every store of fresh diagnostics now also stamps the path it was stored for (`diagnosticsVersionsByPath`, written by the new `bumpDiagnosticsVersion` seam and cleared with the rest of a path's state in `clearDiagnosticsForPath`), and both the `minVersion` freshness gate and the auxiliary evidence check read that per-path stamp. Direction was under-detection — a false `answered`, never a false finding — but it also skewed `lsp_aux_wait_outcome`, the rows used to reason about auxiliary health. Interleavings that used to record a false `answered` now record `silent` and demote the touch's confirmation to `partial`, so sweeps that touch several files against one auxiliary report a higher (and honest) partial rate.
|
|
363
|
+
|
|
364
|
+
- **A silent auxiliary no longer reads as confirmed clean on batch scans either (closes [#1533](https://github.com/apmantza/pi-lens/issues/1533))** — #1493 narrowed the verdict for an unreporting scanner on the per-file lane; `clientScope: "all"` — the batch and directory scan surface — kept aggregating the same silence as `confirmation: "confirmed"`. Auxiliaries are spawned on that scope but it never enters the auxiliary grace wait, so no coverage evidence was ever derived and neither no-answer shape could be computed. A `"all"`-scope touch now derives the same evidence from post-wait state: every spawned auxiliary is recorded as `answered`, `silent`, or `deferred` through the one `auxiliaryCoverageGap` policy, so a scanner that said nothing about these bytes narrows the touch to `"partial"` and is named in `unconfirmedServerIds`. The evidence is read from a wait that already happened rather than from a second one, so the fan-out latency #1459's resync gate recovered stays recovered. `lsp_aux_wait_outcome` now covers this lane too, tagged `waitShape: "aggregate"` (versus `"aux_grace"` for the per-file producer) — `cut_off` cannot arise there because no ceiling is armed. A scanner the caller excluded by design (the workspace sweep's own opengrep exclusion) is never spawned and so is never reported as a gap. Where this changes a verdict: an auxiliary whose budget is the largest among the servers waited on (opengrep on every per-edit path) already left its touch unconfirmed by lapsing the aggregate deadline, so those results are unchanged; a faster scanner beside a slower primary — typos or ast-grep next to rust-analyzer — settles inside that deadline and now narrows a result that used to read confirmed, which costs a skipped cache seed for that file and buys an honest verdict. One further limit stated plainly: `diagnosticsVersion` is a per-client counter, so two concurrent touches sharing one auxiliary client can still cross-satisfy each other's evidence — the verdict then misses a silence rather than inventing one, and [#1544](https://github.com/apmantza/pi-lens/issues/1544)'s per-path counter closes it.
|
|
365
|
+
|
|
366
|
+
- **A failed grammar download no longer disables a language for the rest of the session (closes [#1536](https://github.com/apmantza/pi-lens/issues/1536))** — `TreeSitterClient.ensureGrammar` deduped concurrent grammar fetches by keeping the SETTLED promise, so one offline moment (a DNS hiccup, a CDN blip) memoized `false` for the life of the process; symbol search, module reports, and structural rules for that language stayed degraded even after the network came back. The in-flight entry is now evicted on settle, and a failed download gets a bounded exponential cooldown (30s, 60s, 120s, capped at 5 min, the same `transientRetryDelayMs` shape `availability-policy.ts` already uses) instead of a permanent latch. A durable CDN verdict (404/410 — the wasm genuinely does not exist) still latches, since a retry cannot change that answer; every other failure (network error, timeout, 5xx) retries. The degradation message now says "will retry" instead of "reinstall" for a retryable cause, and the user notification fires once per session per distinct retry delay, so a continuing failure streak that survives a session boundary is announced again instead of going silent.
|
|
367
|
+
|
|
368
|
+
- **A lazy install that dies on a network blip is retried instead of skipped for the session (closes #1537)** — the "we already tried this" key went in BEFORE the install ran and never came out, so a `gem install rubocop` or `rustup component add rustfmt` that failed on a transient error was never attempted again, and the tool stayed silently skipped. The storm guard stays — these spawns are up to 3 minutes and both entry points are reached per save — but it now keys off the attempt's OUTCOME: a transient failure is retried on #1497's install-class ladder (5 min, then 10 min, then the verdict is terminal for the session), while a durable one holds immediately. A missing package MANAGER is the durable case: no cooldown conjures a `gem`. The hold is released only at `session_start`, beside the other per-session availability resets.
|
|
369
|
+
- The two copies of that guard became one. `formatters.ts` carried its own `_lazyInstallAttempts` Set duplicating `lazy-installer.ts`'s, and a fix in one was not a fix. State, classification, retry ladder and spawn options now live in `lazy-installer.ts`; `formatters.ts` keeps only the part that is formatter business (a fresh binary on PATH invalidates every "not found" verdict). Because these installs are machine-global, the two seams share one hold per tool: a runner and a formatter asking for the same `gem install rubocop` are asking for the same one thing. Callers that arrive while an install is still running now join it instead of starting a second one.
|
|
370
|
+
- "We tried and the network failed" is now tellable from "this tool cannot be installed here". The lazy installers recorded nothing at all. Each attempt now produces an `InstallAttemptFact` in #1534's vocabulary; the rust-clippy runner reads it through `describeInstallAttempt` and puts it in the `availability_decision` record beside the verdict it produced, so a failed `rustup component add` reaches `latency.log` instead of looking like a silent skip. Each suppression also logs one `install`-subsystem line with the cause, the attempt count and the remaining cooldown.
|
|
371
|
+
|
|
372
|
+
- **A stalled PATH probe no longer hands your project to its second-choice formatter for the session (closes #1539)** — `detect()` returned a plain boolean, so "rubocop is not installed" and "the probe for rubocop timed out" arrived at the selection pass as the same answer. The runner-up won, the result was non-empty so #1495/#1532's poison guard never fired, and `detectionCache` is invalidated only by a config file's mtime or size: standardrb owned the repo until you restarted or edited a config. Each candidate now carries a verdict read from the PATH latch — `available`, `missing`, or `unreachable` — so a selection whose preferred candidate was never asked is provisional: it is not cached, it re-detects once the cooldown expires, and `formatter_selected` records `reason: "preferred-unreachable"` with the candidates it could not reach instead of the config-shaped `"explicit-config"`. A genuine absence still caches exactly as before, because that is a real fact about the project.
|
|
373
|
+
- The poison guard now sees every binary a detection probed, not just the formatter's own. It matched stalled probes against `command[0]`, and the extras were assumed harmless because "they run only after the primary answered". Not so: `rustfmt` genuinely absent plus a stalled `which rustup` skips the `rustup component add rustfmt` that would have fixed it, and nothing named `rustfmt` is transient, so the empty result cached for the session. The guard reads the real probe record now, which also covers the co-equal-alternative case (`pwsh` ?? `powershell`).
|
|
374
|
+
- A formatter held off by a probe cooldown is visible in `latency.log` while it is off. The latch's memo branch returned before `logAvailabilityDecision`, so one record covered arbitrarily many decisions and anyone counting `availability_decision` rows undercounted the outage. Cache-served verdicts now emit one row per cooldown window, marked `servedFromCooldown: true` so they stay separable from real probes.
|
|
375
|
+
|
|
376
|
+
- **An execution-policy-blocked PSScriptAnalyzer run no longer reports the file as clean (closes #1540)** — Both availability probes call PowerShell with `-Command`, which the execution policy does not gate, but the real analysis runs with `-File`, which it does. Under `Restricted`/`AllSigned` — the default on many corporate Windows hosts — the analysis exits nonzero with a `SecurityError` on stderr and nothing on stdout, and the runner read that empty stdout as zero diagnostics: a blocked analyzer reported as a clean file. A `-File` run is now verified directly: a nonzero exit or a crashed/signal-killed process (`status === null`) records a legible `availability_decision` (policy and interpreter included) and a `grammar-blocked` degradation-ledger entry instead of a silent pass or a silent skip, and only an actual exit-0 run is read as evidence about the file's diagnostics. Review round: a `status === null` spawn-UNKNOWN no longer latches `-File` off for the session (mirrors the nonzero-exit branch's own transient downgrade); both degradation-ledger calls use `incrementDegradationCount` so a retried block doesn't flood the ring buffer; the policy-block regex now requires the `SecurityError` + script-load pairing instead of a bare `unauthorizedaccess` match. A separate exit-0-but-empty-or-malformed-stdout gap is tracked as #1598.
|
|
377
|
+
|
|
378
|
+
- **Fish had no LSP runner in the registry, and 21 extensions had no language id, because two seams were hand-copies of the language registry (closes [#1545](https://github.com/apmantza/pi-lens/issues/1545))** — `LANGUAGE_POLICY` decides which kinds are `lspCapable`, but the lsp runner's `appliesTo` and the LSP `LANGUAGE_EXTENSIONS` table each kept their own transcription of it, so a language could be registered and still miss either one silently. Both had drifted. `appliesTo` omitted `fish`, so `RunnerRegistry.getForKind("fish", …)` returned no LSP runner and `getAvailableRunners("/p/a.fish")` led with `fish-indent`. Routing was unaffected (main dispatch resolves group runner ids directly, and `PRIMARY_DISPATCH_GROUPS.fish` names `lsp`), and `getForKind`'s only callers today are `getAvailableRunners`, which nothing in production calls, and the `@internal` `dispatchLint`, which has no production caller either — so this was a latent wrong answer in the registry rather than lost coverage, and it would have become real the moment any surface started asking the registry what runs on a file. `LANGUAGE_EXTENSIONS` was the bigger gap: powershell had no entry at all (`.ps1`, `.psm1`, `.psd1`), cxx covered seven of its twenty-four registered extensions (`.c++`, `.cp`, `.hxx`, `.inl`, `.ipp`, `.tpp`, `.txx`, `.c++m`, `.cppm`, `.cxxm`, `.ixx`, `.cu`, `.hip`, `.m`, `.mm`, `.cl`, `.clcpp` all missing), and `.json5` was absent, so `didOpen` announced each of those as `plaintext`. How much that costs depends on the server, and the honest answer is "less than it looks for most of this list": driving real servers over raw LSP, vscode-json-language-server and yaml-language-server return identical diagnostics under `plaintext` (7/7 and 3/3), and clangd derives the language from the path, so the cxx entries are very likely no-ops too. A `DocumentSelector`-based server such as PowerShell Editor Services is the case where the announced id decides whether the document is handled at all. The table is now correct either way, and correct-by-construction rather than by transcription. Both seams derive from `getLspCapableKinds()`: `appliesTo` is the policy list itself, and `LANGUAGE_EXTENSIONS` is the curated per-extension table (which stays, because one kind can span several ids, `.ts` vs `.tsx` and `.c` vs `.cpp`, and because servers want `shellscript` rather than the registry's `shell`) filled in from `KIND_EXTENSIONS` for anything the curated table does not already name. Registering a language as `lspCapable` is now the only step either seam needs. Six structural guards plus two behavioral pins in `tests/clients/lsp-capable-seam-coverage.test.ts` hold that in place: `appliesTo` non-empty (`getForKind` reads an empty one as "every kind"), every `lspCapable` kind present in it, no non-capable kind claimed there, every `lspCapable` extension present in `LANGUAGE_EXTENSIONS`, every one of them resolving to something other than `plaintext`, and the set of reachable language ids pinned by hand so a kind whose registry label no server answers to needs a deliberate review decision rather than passing silently. Each guard is proven red under the mutation it exists to catch. Found during the adversarial review of [#1520](https://github.com/apmantza/pi-lens/issues/1520), which hit both seams at once; refs [#1519](https://github.com/apmantza/pi-lens/issues/1519).
|
|
379
|
+
|
|
380
|
+
- **`biome-client-dedupe` test now isolates the in-flight probe dedupe from package-manager spawn count (refs [#1546](https://github.com/apmantza/pi-lens/issues/1546))** — `dedupes concurrent first-time callers to a single probe` armed `safeSpawnAsync` as a one-shot mock, assuming `probeBiome` calls it exactly once. Since #375, `getBiomeBinary` falls through to `findGlobalBinary` when no local or pi-lens-managed biome binary exists, and that helper legitimately probes every installed package manager's global bin dir (`where npm`, `npm config get prefix`, and so on) through the same `safeSpawnAsync` seam. On a machine without a local/managed biome binary — reproduced by moving both aside — those extra calls silently consumed the one-shot mock and `probeBiome` dereferenced the resulting `undefined`. The test now mocks `package-manager.js`'s `findGlobalBinary` to resolve `undefined` directly, the same isolation `biome-install-evidence.test.ts` already uses, so it stays scoped to `ensureAvailable`'s own in-flight dedupe (#120) instead of incidentally depending on how many package managers happen to be findable on the host.
|
|
381
|
+
|
|
382
|
+
- **A captive portal can no longer poison a grammar file permanently (closes [#1548](https://github.com/apmantza/pi-lens/issues/1548))** — the runtime grammar download trusted `res.ok` alone, so an airport-wifi or corporate-proxy login page returned as 200 OK was written to disk as `tree-sitter-<lang>.wasm`. That counted as a successful download, so no cooldown was armed and nothing was recorded, while every later resolve found the garbage file and reported the language as available — `Language.load` then failed on every parse, with no path back to a re-download until someone deleted the file by hand. The downloaded bytes are now checked against the WebAssembly preamble (`\0asm`) BEFORE anything is written; a non-wasm body is a retryable failure, so it flows through the same cooldown, notification, and degradation-ledger path a network failure does, and the record names the real shape ("returned an HTML page — a captive portal or proxy intercepted the request") instead of blaming the package manager. Grammar resolution also rejects a non-wasm file that is already on disk, so a file poisoned before this fix shipped is ignored and re-fetched instead of degrading the language forever. An ignored file is recorded once per session rather than once per process, so a session that starts with the poisoned file still on disk still reports why the language is degraded. Separately, an exception thrown anywhere in the grammar-fetch task now arms the same cooldown rather than escaping with no retry state.
|
|
383
|
+
|
|
384
|
+
- **A workspace sweep no longer speaks for a scanner that missed its deadline (refs [#1549](https://github.com/apmantza/pi-lens/issues/1549))** —
|
|
385
|
+
when an auxiliary scanner misses the sweep deadline, the result now names the
|
|
386
|
+
uncovered lanes instead of reporting the file as fully answered. A partially
|
|
387
|
+
covered snapshot is never cached and never reconciled into the widget, so a
|
|
388
|
+
later read re-asks the scanner that stayed silent.
|
|
389
|
+
|
|
390
|
+
- **One slow scanner no longer discards every good answer in a cascade sweep (refs [#1549](https://github.com/apmantza/pi-lens/issues/1549))** — a neighbour touch attaches about five servers, and its verdict was `inconclusive = notifyWriteTimedOut || diagnosticsTimedOut` with both flags TOUCH-WIDE. The diagnostics deadline is the maximum over the servers waited on, so opengrep's 3500 ms budget set the deadline for the whole touch: a clean TypeScript answer that landed in 100 ms reported as "nothing is known about this file". Measured over 6,079 sweeps in eight days, 97.6% of neighbour touches read inconclusive, against 15% for ordinary edit-time touches in the same window; a dogfood session after the [#1528](https://github.com/apmantza/pi-lens/issues/1528) notify-write flood fix still showed 37 of 37 inconclusive, which confirmed the merge rule rather than queueing as the driver. The verdict is now decided from the PRIMARY population alone, through one pure policy (`resolveTouchVerdict`): a touch whose primary answered reports its findings as usable and names the auxiliary it does not speak for, using the existing `confirmation: "partial"` plus `unconfirmedServerIds` machinery from [#1470](https://github.com/apmantza/pi-lens/issues/1470)/[#1493](https://github.com/apmantza/pi-lens/issues/1493)/[#1533](https://github.com/apmantza/pi-lens/issues/1533). Every consumer that treats confirmation as proof of coverage therefore still fails closed — no cache seeding, no widget reconcile, no clean bill of health — while the findings reach the agent instead of being thrown away. An auxiliary can no longer make a touch inconclusive whichever deadline it missed; only a primary can, and the answer is decided from EVIDENCE (a per-path publication stamp advancing, or a fresh per-file cache entry), never from how a promise settled, so a client that exposes neither accessor keeps the pre-fix verdict. The capability-aware silent-clean confirm is primary-scoped for the same reason: a clean markdown file whose marksman silence IS the answer no longer stays inconclusive because an opengrep scan beside it had not finished. Two safety consequences ride along: an auxiliary whose notify write never landed has its stale findings dropped from the merged result (they describe the previous revision, and before this they were unreachable behind the blanket verdict), and the diagnostics-timeout degradation ledger now records only the servers that actually went unheard rather than every spawned server. That drop is judged on a merge-time content-binding read unioned with [#1493](https://github.com/apmantza/pi-lens/issues/1493)'s pre-notify snapshot, because the snapshot is captured before the write and cannot see the write that lands late and then publishes for these exact bytes — [#1459](https://github.com/apmantza/pi-lens/issues/1459)'s own documented signature. One predicate decides both the drop and the coverage naming, so a scanner is never named uncovered while its findings ride along in the result. Attribution is on the record: `inconclusiveServerIds` and `inconclusiveReason` (`notify-write`, `diagnostics-wait`, or `mixed`) on the touch result, in `lsp_touch_file`, and in the cascade's own `neighbor_touch` row; `lsp_diagnostics_timeout` gains `unansweredServerIds` plus `attributedToPrimary`; and `lsp_scanner_coverage_gap` gains `auxNoAnswerServerIds` so the record that proves a scanner blackout survives the verdict change. The issue's `latency.log` retention item (one rotation sibling keeps about two days) is deferred and re-homed on the issue, and the sub-30% rate gate is measured on the next dogfood.
|
|
391
|
+
|
|
392
|
+
- **Cascade "could not compute impact" records now name the file that actually caused it (closes #1550)** — When a cascade run cannot enumerate a file's dependents, pi-lens writes one `cascade_indeterminate` line to `cascade.log`. That line labelled itself with the turn's first edited file and listed its reasons with no file attached — but the runs it summarises carry their own paths, and a run can be carried over from an earlier turn or belong to a file the turn never edited. So the log blamed the wrong file: a Markdown edit was credited with a missing graph node it could never have produced, and a plain source file was credited with the test-file-exclusion reason that only applies to tests. Each reason is now attributed to the file that produced it. A missing graph node also records what was looked up and what the graph held nearest to it — file-node count, surviving symbol nodes, known siblings in the same directory — so a cold graph, an unadmitted file, and a genuine graph inconsistency read differently in the log. Agent-facing advisories were already correct and are unchanged.
|
|
393
|
+
|
|
394
|
+
- **The availability-policy coverage gate no longer waves through a verdict that dodges the word "boolean" (refs #1552)** — Routing inheritance decided whether a unit that delegates its spawn to a routed helper may borrow that helper's coverage. It worked as a blacklist: inherit unless the unit's own memo "looks boolean". A `Map<string, "yes" | "no">` string-union verdict spells neither "boolean" nor a policy-factory name and slipped through, and a genuine boolean reached through a type alias slipped through the same way, since the word only ever appears in the alias's own declaration. The gate now works as a whitelist: a unit inherits routing only when its own memo is traceably a policy factory's handle, directly or through one hop of a module-local wrapper. An unrecognised shape defaults to unrouted instead of routed.
|
|
395
|
+
|
|
396
|
+
- **A resolved blocker no longer re-appears as "Unresolved from this turn" after pi-lens has confirmed the file clean (refs #1561)** — When a blocker on file A is caused by something in file B, fixing B re-analyzes B, not A. A's stale verdict was invalidated only by a later dispatch of A itself or by A being deleted, so it kept being injected at every turn end for the rest of the session. In one dogfood session it was re-served six times, and the agent burned three turns proving it wrong. Worse, three of those re-serves happened after the agent ran `lsp_diagnostics` on the file and pi-lens answered "confirmed clean": that confirmed result corrected the widget footer and nothing else. It now also retires the file's stale blocker, and clears the `--lens-guard` commit gate that the blocker was holding — both the in-memory latch and the persisted record, so a retired finding stops blocking the commit instead of being quoted back as the reason. Retiring is deliberately narrow. It requires a fresh, content-bound result with nothing at the blocking tier; an unconfirmed answer, a silent server, a partial answer missing an auxiliary scanner, a cache replay, or a reconcile that threw all count as unknown, never as clean. It is ordered against the dispatch that recorded the blocker, so a slow old clean cannot erase a newer finding. And it must cover the tools that actually raised the blocker: inline blockers come from every runner, not just the language server, so an eslint, actionlint, biome-check or ast-grep security finding is never retired by a language-server check that did not consult those tools, and a record whose provenance is unknown is never retired at all. Each retire is logged, so the eviction is confirmable from the runtime log. Blockers that go stale before any fresh verdict exists need a dependency-aware staleness key and remain open in #1561.
|
|
397
|
+
|
|
398
|
+
- **A truncated grammar download no longer poisons the file permanently ([#1564](https://github.com/apmantza/pi-lens/issues/1564))** — the #1548/#1560 wasm-magic check catches a captive portal's HTML, but a connection dropped mid-transfer still starts with a genuine `\0asm` preamble, so it passed. The runtime download path now verifies the full body against the pinned sha256 in `scripts/grammars.lock.json` (the same manifest the postinstall path already trusts), falling back to a Content-Length compare when no pinned hash is available. A `Language.load` failure on a file resolution just vouched for now also records a degradation naming the grammar and the loader's error, and invalidates the resolve memo so the next demand re-fetches instead of reusing the same broken file forever.
|
|
399
|
+
|
|
400
|
+
- **Tests no longer reset a duplicate copy of the module state they are guarding (closes #1565)** — Vitest resolves an import specifier literally and this repo's runtime is the compiled output, so `x.ts` and `x.js` are two module instances. A test that imported a module's reset or inspection API through `.ts` held a private copy of that module's mutable state: the `beforeEach` reset cleared the copy, the code under test kept reading the compiled original, and the assertion passed without ever exercising the behaviour it claimed to guard. Seventeen test files were reaching the same module both ways, most of them around the availability latches. Every test now imports the compiled `.js` the runtime imports, a new structural guard (`tests/config/module-instance-coverage.test.ts`) fails CI on any new `.ts` specifier for a build-compiled module, and a live regression test proves the session availability reset actually clears the compiled latch. No shipped behaviour changes; the guarded behaviour is now genuinely guarded.
|
|
401
|
+
|
|
402
|
+
- **The availability-policy coverage gate no longer inherits routing from a memo that merely mentions a policy handle's name (refs #1566)** — The #1552 whitelist checked whether a policy handle's name appeared anywhere in the memo's declared type or value. A memo typed `Awaited<ReturnType<ReturnType<typeof makeToolProbe>>>` unwraps a routed wrapper's return type down to a plain boolean, and a memo built by `emptyCache<boolean>(makeToolProbe)` merely hands the wrapper to an unrelated helper as an argument — both spell the handle's name without holding the handle, and both inherited routing they never earned. The gate now requires the handle's name to be the memo's own un-nested `ReturnType<typeof name>` or its own direct `= name(...)` call, so a name present anywhere else in the text no longer counts.
|
|
403
|
+
|
|
404
|
+
- **The ast-grep native-addon load no longer starves a concurrent caller or latches across sessions (refs [#1567](https://github.com/apmantza/pi-lens/issues/1567))** — `loadSg()` guarded the `@ast-grep/napi` load behind a plain "attempted" flag, set before the load began. The per-edit fallback runner and the session-start project scanner both call it and could race: a caller arriving while a load was already in flight read the flag as "already tried" and got back `undefined` for a load that was still pending and about to succeed — a false-negative starvation, not a duplicate load attempt. Any load failure then latched for the rest of the process, with no re-arm at session start, so a repaired install stayed invisible until the host restarted. The fix shares one in-flight promise across every caller, evicted on settle so a rejected load is never replayed (the `#1536` pattern); every failure now holds until `resetAstGrepNapiLoadState()` re-arms it at the next `session_start`. There is deliberately no in-process cooldown-then-retry for a transient failure: `@ast-grep/napi` loads via a dynamically imported `file://` URL, and Node's ESM loader permanently caches a module record that threw during evaluation, so a same-process retry would only replay the cached rejection. A classifier still distinguishes a narrow, positively-identified transient errno family from a genuine failure (an unrecognized error, including any real native-binding incompatibility, is genuine by default) so the degradation ledger — a held load now records an `ast-grep-napi-unavailable` entry, distinguishing a degraded scan from a clean one with nothing to report — says something honest about what happened.
|
|
405
|
+
|
|
406
|
+
- **A stalled ast-grep binary no longer pins the whole session to `npx` (closes #1568)** — Both ast-grep availability sweeps walk an ordered candidate list — local `node_modules/.bin`, then PATH, then a package manager's global bin, then `npx --no -- ast-grep` — and stop at the first candidate that answers. A candidate that timed out was recorded, but the record was read only when *every* candidate had failed. So one busy second at warm-up, with a healthy ast-grep on PATH, ended with `npx` selected and cached as a durable fact: a Node process started for every ast-grep invocation for the rest of the session. A win reached while a preferred candidate was merely unreachable is now provisional — it is used immediately, and re-checked once that candidate's cooldown expires, so a recovered binary wins its place back without a restart. A genuine absence still caches, unchanged. And if the re-check itself times out on a host that is still busy, the command already proved working is kept rather than discarded — a timeout says nothing about a tool that ran 30 seconds ago. The `availability_decision` entry in `latency.log` says which case it was, carrying `provisional: true`, `unreachablePreferred`, `retained`, and `retryAfterMs`.
|
|
407
|
+
|
|
408
|
+
- **A tool tier degraded by a transient stall no longer survives 24h across process restarts ([#1569](https://github.com/apmantza/pi-lens/issues/1569))** — `getToolPath`'s persistent probe cache (`~/.pi-lens/probe-cache.json`) recorded only the winning path, with no memory of whether a preferred candidate along the way had merely stalled rather than proven broken. A `--version` probe that timed out, was killed, or hit an unspawnable-process glitch (Windows `spawn UNKNOWN`, EAGAIN/EBUSY) fell through to a lower-priority tier exactly like a genuine absence, and that degraded selection was then trusted for the full 24h TTL — with no session reset touching it, since the cache outlives the process. The persisted entry now carries whether any candidate was transient at selection time, and a tainted entry ages out after the shared transient cooldown (5 min) instead of the full TTL, so a process restarting minutes later re-probes instead of inheriting the stall.
|
|
409
|
+
|
|
410
|
+
- **A failed lazy-module load or classic-tsserver repair no longer stays broken for the rest of the process (closes [#1570](https://github.com/apmantza/pi-lens/issues/1570))** — `clients/lsp-lazy.ts`, `clients/formatters-lazy.ts`, `clients/dispatch/lazy.ts`, and `clients/mcp/session.ts` each memoized a lazy `import()`/session-context promise with `x ??= ...`, which keeps a REJECTED promise cached for the process lifetime: one transient module-load failure (EMFILE, a momentary fs error) permanently poisoned that seam for every later caller. A new shared `createLazyImport` helper (`clients/lazy-import.ts`) evicts the memo on rejection so the next demand retries; no cooldown, since module loads are cheap. `clients/bootstrap.ts`'s per-client-fail-soft bootstrap promise got the same defensive eviction, though it is not expected to reject in normal operation. Separately, the classic-tsserver repair guard (`clients/lsp/server.ts`) was a one-shot per-process flag with no re-arm — a repair that failed once (an offline install, a registry hiccup) stayed unrepairable for every later session in a long-lived extension-host process. `resetLSPService({reason: "session_start"})` now re-arms it on every session start.
|
|
411
|
+
|
|
412
|
+
- **Formatter config-file cache now sees files created mid-session, for several formatters that previously missed it (refs #1572)** — the per-cwd formatter cache invalidates when a tracked config file's mtime/size changes, but `psscriptanalyzer-format`'s settings file, `google-java-format`'s marker, `cljfmt`'s bare `.cljfmt`, several `cmake-format` variants, sqlfluff's `setup.cfg`, oxfmt's `vite-plus.json`/extra `vite.config` extensions, and the Kotlin/Spotless gradle files were never in the tracked list — so adding one of them to a project after pi-lens had already looked at that directory kept returning the stale, pre-opt-in answer for the rest of the session. All are now tracked.
|
|
413
|
+
|
|
414
|
+
- **`psscriptanalyzer-format` can now be selected for `.ps1`/`.psm1`/`.psd1` files, and honors its settings file (closes #1572)** — `.ps1`'s formatter policy required explicit project config before offering the formatter, but the formatter had no config check at all: no configuration of a PowerShell project could ever select it. A project with a `PSScriptAnalyzerSettings.psd1` (or `ScriptAnalyzerSettings.psd1`) now selects `psscriptanalyzer-format` the same way a `.clang-format` or `stylua.toml` selects its formatter — and the resolved settings file is now passed to `Invoke-Formatter -Settings`, so an opted-in project's declared rules apply instead of the stock ruleset. A new coverage guard asserts every registered formatter is selectable under some configuration, so this shape can't recur silently. `tryLazyInstall`'s unreachable `golangci-lint` arm — golangci-lint already auto-installs through the shared tool-installer seam — was removed rather than wired to a second, redundant install path.
|
|
415
|
+
|
|
416
|
+
- **npm-global tool discovery now flags a stalled package-manager probe as transient (refs #1585)** — `isAvailable`'s bare boolean return could not tell a genuine "pnpm not installed" from a `where`/`which pnpm` probe that stalled, so `allAvailableGlobalBinDirs` silently dropped pnpm's global bin dir with no way to warn its caller. `findNpmGlobalToolPath`'s existing `onTransient` callback (from #1569) now fires for this case too, so `getToolPath` no longer caches a degraded npm-global selection untainted for the full 24h TTL.
|
|
417
|
+
|
|
418
|
+
- **A scanner that answered while its resync sat queued is no longer reported as a blackout (refs [#1586](https://github.com/apmantza/pi-lens/issues/1586))** — the fan-out gate allows one outstanding `didOpen` resync per auxiliary, and a touch that queues behind another is DEFERRED. Whether a deferred scanner nonetheless covers the file is a content-hash question, and the deferred door asked it of the PRE-NOTIFY snapshot: a set captured before the write went out, which by construction cannot see the publication that arrives after it. So when the write the gate deferred behind landed and the scanner published for exactly the bytes this touch carries, the touch still dropped those findings from `.diags` and still named the scanner in `unconfirmedServerIds` — a scanner that had demonstrably reported reading as one that had gone silent, which is [#1549](https://github.com/apmantza/pi-lens/issues/1549)'s underclaim one door over. Coverage is now ONE rule (the pre-notify snapshot unioned with a live content-binding read, both going through a single content-match comparator so a door cannot acquire a rule of its own by writing the comparison inline) evaluated ONCE, as the last statement before the merge. Every door that shares the merge's consequences reads that frozen answer: the merge drop, the deferred door, the merged content binding — where a dropped contributor now loses its findings and its fingerprint together, rather than the fingerprint being filtered on the raw deferral set — and the result's own `unconfirmedServerIds`, which reconciles the auxiliary wait-outcome verdict (decided earlier, when the wait ended) against the merge before anything is claimed. Freezing it is what makes the fix safe in both directions: `touchFile` awaits after the merge, and asking the live rule again when the coverage gap is named let a publication landing in that window un-name a scanner whose findings the merge had already dropped — `confirmed` over a `.diags` missing the scanner's answer, which is the overclaim direction and the one that unblocks the last-known-diagnostics cache prime and the warm mark that the coverage gap exists to hold shut. The honest case is unchanged and still fails closed: a deferred scanner with no binding for these bytes keeps its stale findings dropped and stays named, and a scanner whose breaker was open never attached at all, so there is nothing to read for it and it stays named too. `lsp_scanner_coverage_gap` and `lsp_touch_file` report the deferrals the touch is actually uncovered for rather than every deferral; the gate's own action keeps its unconditional record in `lsp_notify_resync_deferred`.
|
|
419
|
+
|
|
420
|
+
- **`TreeSitterClient.init()` no longer replays a dead `import()` on every file parse (refs [#1592](https://github.com/apmantza/pi-lens/issues/1592))** — `init()` called `loadWebTreeSitter()`, a dynamic `import()` of a fixed resolved `file://` URL, and only latched permanently on errors matching an Emscripten `Aborted`/`abort()` signature. Any other rejection — for example the module throwing during evaluation — left `initPromise` cleared, so the next `init()` call (one per file parse, via `withTreeSitterRoot()`) re-imported the same URL and replayed Node's cached rejection instead of re-attempting the load: a retry that looked like a retry and structurally could not be one. `init()` now classifies the rejection (`classifyWebTreeSitterLoadFailure`, mirroring `classifyAstGrepLoadFailure`): a RESOLUTION-shaped failure (`ERR_MODULE_NOT_FOUND`, a transient fs errno) still retries on the next call, because it can plausibly clear on its own; an EVALUATION-shaped failure latches for the SESSION and records a `web-tree-sitter-load-failed` degradation once. The latch is session-scoped, not process-lifetime — `resetLoadStateForSession()` on `TreeSitterClient`, exposed as `resetTreeSitterClientLoadState()`, is wired into `resetDispatchBaselines()` beside `resetAstGrepNapiLoadState()`, matching the `#1567`/`#1575` precedent — and stays distinct from the existing `wasmAborted` latch, which covers a corrupted heap from a runtime that DID load and deliberately stays process-lifetime. `clients/lazy-import.ts`'s docstring now states the underlying ESM-cache limit explicitly: eviction-and-retry recovers a resolution failure but replays the cached rejection for an evaluation failure, so callers must not read "retries" as "can recover from a broken compiled module mid-process." A class sweep of every dynamic-import retry/cooldown site in `clients/deps/*.ts` and its callers found no other dead-retry sites: the `@ast-grep/napi` loader already latches this way (`#1575`), and every other lazy `import()` in the codebase is either a one-shot local-module load (no retry ceremony to be dishonest about) or a network/filesystem-backed retry (grammar downloads, `Language.load` on WASM bytes) that is genuinely retryable and outside the ESM-cache class. The new session-scoped latch is registered in the `#1635` session-state registry (`tests/support/session-state-registry.ts`), replacing that file's prior blanket exemption for `tree-sitter-shared.ts`.
|
|
421
|
+
|
|
422
|
+
- **The retained-winner arm no longer re-serves an ast-grep command this same sweep just proved gone** — both the shared `isSgAvailableAsync` sweep and `SgRunner.ensureAvailable()` retain a provisional winner across a stalled sibling tier, but neither checked whether the memoized winner itself was among the candidates that ENOENTed in that same pass. A previous provisional winner (say `npx`) that goes durably missing while an unrelated tier merely stalls used to still answer available with the dead command; the sweep now tracks which candidates it proved durably absent and skips the retained arm when the memoized command is one of them, falling through to the genuine-absence path instead (#1593).
|
|
423
|
+
|
|
424
|
+
- **Basename-classified files (`Makefile`, `Dockerfile.<suffix>`) reached `didOpen` as plaintext, invisible to the #1545 extension-seam guards (refs #1594)** — `clients/file-kinds.ts`'s `SPECIAL_FILENAMES` classifies `Makefile`, `Dockerfile(.<suffix>)?`, and `CMakeLists.txt` by basename regex into `lspCapable` kinds, but `clients/lsp/language.ts`'s `getLanguageId` only resolved language ids through `LANGUAGE_EXTENSIONS`, a flat extension/literal-basename map. `Dockerfile` and `CMakeLists.txt` happened to work because two hand-kept literal keys covered their exact spelling; `Makefile` had no entry at all, and `Dockerfile.dev`/`Dockerfile.prod`-style suffixes never matched the literal `"Dockerfile"` key. `getLanguageId` now falls back to a `BASENAME_LANGUAGE_PATTERNS` list derived from the now-exported `SPECIAL_FILENAMES`, filtered to `lspCapable` kinds — the same single source of truth `detectFileKind` already uses, not a second hand-kept literal list. The two now-redundant literal keys (`"CMakeLists.txt"`, `"Dockerfile"`) were removed from `CURATED_LANGUAGE_EXTENSIONS` since the derivation covers them. `terragrunt.hcl`/`root.hcl` stay unresolved by design: `terragrunt` is `lspCapable: false`.
|
|
425
|
+
- `clients/file-kinds.ts` — `SPECIAL_FILENAMES` exported.
|
|
426
|
+
- `clients/lsp/language.ts` — `getLanguageId`, `BASENAME_LANGUAGE_PATTERNS`.
|
|
427
|
+
- `tests/clients/lsp-capable-seam-coverage.test.ts` — basename-classifier coverage guards mirroring the existing extension-classifier guards 4-6.
|
|
428
|
+
|
|
429
|
+
- **Seven more config-gated formatters were unselectable under any project configuration (refs [#1595](https://github.com/apmantza/pi-lens/issues/1595))** — same shape as [#1572](https://github.com/apmantza/pi-lens/issues/1572): commit `038cd1df` set `defaultWhenUnconfigured: false` for `csharpier`, `ormolu`, `taplo`, `terraform`, `swiftformat`, `fantomas`, and `mix` without adding a matching `hasExplicitFormatterConfig` check, so neither selection branch could ever pick them. Each now wires into `EXPLICIT_FORMATTER_CONFIG_CHECKS` against its real config-file convention — `.csharpierrc`, `.ormolu`, `taplo.toml`/`.taplo.toml`, `.swiftformat`, `.fantomasignore`/`.editorconfig`, `.formatter.exs` — or, for `terraform fmt` (which has no config file at all), the `.terraform.lock.hcl` manifest `terraform init` writes as an explicit per-project opt-in marker. `nixfmt` stays unreachable: it has no config surface and no manifest-marker equivalent, so a documented exception replaces the fix.
|
|
430
|
+
|
|
431
|
+
- **An exit-0 PSScriptAnalyzer run with empty or malformed stdout no longer reports the file as clean (refs #1598, refs #1540)** — `PS_SCRIPT` always writes the literal `[]` marker or a diagnostics array before exiting 0, but `parsePSAnalyzerOutput` treated any unparseable stdout the same as that marker, so a lost or truncated write (a host quirk, not a real clean file) reported `succeeded` with zero diagnostics identically to a genuine clean run. The parser now returns `null` for empty or malformed stdout, distinct from a real `[]`; the runner reads `null` as an unreadable run, logs an `availability_decision` (tool `psscriptanalyzer-stdout`) and a `grammar-blocked` degradation-ledger entry, and skips without latching `-File` off — one bad read is not durable evidence the interpreter is broken, mirroring the nonzero-exit branch's own guard. #1540's fix covered the `-File` nonzero-exit and `status === null` paths in the same function; this covers the exit-0-but-unreadable-stdout gap its review flagged as out of scope.
|
|
432
|
+
|
|
433
|
+
- **`BiomeClient`'s in-flight leak guard could not fail (refs #1601)** — the regression test for "the `ensureInFlight` slot doesn't leak after a failed probe" used a probe failure that latches durably, so the second `ensureAvailable()` call short-circuited on the availability memo and never reached the `ensureInFlight` check the test claimed to cover; removing the `finally`-clear it names left the test green. The suite now probes through the one path that actually reaches that check — a transient (timeout) verdict whose cooldown has expired — and is proven red against the described mutation. A second test covers the settled-slot-through-cooldown window directly: two callers racing in as the cooldown opens must share one fresh probe rather than one of them resolving off a leaked settled promise. A sweep of every other `ensureInFlight`-based client (`sg-runner.ts`, `dead-code-client.ts`, `knip-client.ts`, `dependency-checker.ts`, `security-scan-client.ts`, and the shared `toolchain-availability.ts` factory ruff and friends route through) found the same `finally`-clear on all of them; no production code changed.
|
|
434
|
+
|
|
435
|
+
- **PSScriptAnalyzer's execution-policy regex tightening now has regression tests (refs #1604)** — the #1556 review round tightened `policyDenied`'s detection from a bare `securityerror|execution polic(y|ies)|running scripts is disabled|unauthorizedaccess` alternation to requiring `securityerror` paired with one of the other three, closing a false positive where a bare `UnauthorizedAccessException` (a file-permission error, unrelated to the execution policy) or a stray mention of "execution policy" in unrelated stderr latched `-File` off as a durable policy block. That tightening shipped with no test proving the false positive it closed. Two regression tests now pin it: reverting the regex to the pre-#1556 alternation turns both red, confirming the current AND-conjunction is load-bearing, not vacuous.
|
|
436
|
+
|
|
437
|
+
- **Security scanners log a compensating `available` row when auto-install recovers a failed probe (refs #1606)** — gitleaks, trivy, and opengrep probe on PATH first and fall back to the pi-lens installer; govulncheck falls back to `go install`. When the probe failed but the install then resolved the binary, the probe's `unavailable` record stood alone in `latency.log`: nothing said the tool came back. An auditor reading the log concluded the lane was off when it was on. A second `availability_decision` row now fires on every recovery path, with `verdict: "available"`, `cause: "ok"`, `classifiedBy: "caller"`, and the binary's basename plus its install source in `evidence`.
|
|
438
|
+
|
|
439
|
+
- **agent_end no longer misreads a disabled feature as a missing cache (closes [#1607](https://github.com/apmantza/pi-lens/issues/1607))** — The `actionable_warnings_autofix` reader at `agent_end` read the actionable-warnings cache on every call, but the cache is only ever written when the `lens-actionable-warnings` flag is on. In production that flag is off, so the read always missed and logged "cache missing or expired, skipping fixes" — 428/428 times in the retained log, per a maintainer-supervised forensics pass, always blaming a stale cache instead of the real cause: the writer is disabled. The reader now checks the writer's own flag before attempting a read, so a disabled writer produces no read attempt and no misleading log line. When the flag is on and a read does miss, `CacheManager.inspectCache` (already used by `git-guard.ts` for the same purpose) distinguishes "cache absent" from "cache expired," instead of collapsing both into one ambiguous message.
|
|
440
|
+
|
|
441
|
+
- **`language: TypeScript` ast-grep rules now run on `.tsx` files** — the napi runner compared a rule's declared language against `.tsx`'s exact grammar name (`"tsx"`), so every `TypeScript`-tagged rule's language guard skipped it on React/TSX sources, leaving 120 of 263 shipped rules dark there. `.tsx`'s grammar is empirically a syntactic superset of `.ts`'s for every construct the shipped catalog's rules target, verified per rule (not assumed) by asserting each `TypeScript` rule's existing fixture-test `invalid:` snippet still matches parsed as tsx — so a `TypeScript`-tagged rule now also runs against a `.tsx` file's parsed root. A rule skipped purely for a language/file mismatch is now recorded in the same aggregated `astgrep_napi_unsupported_rules_skipped` latency telemetry the runner already emits for wholly-unsupported languages, so an empty result on a `.tsx` scan is distinguishable from "rules never ran" (#1608).
|
|
442
|
+
|
|
443
|
+
- **Several installer writes and two persisted-state read paths did not survive a hard kill cleanly (refs [#1609](https://github.com/apmantza/pi-lens/issues/1609))** — a derived sweep test (`tests/clients/atomic-write-sweep.test.ts`, `tests/support/atomic-write-scan.ts`) that enumerates every raw filesystem-write call under `clients/` at test-run time (not a hand-maintained list) found `clients/installer/index.ts` writing several downloaded tool artifacts (bare-gzip binaries, bare binaries, Maven jars and launchers, archive-install shims, the npm bootstrap `package.json`, and a GitHub "extra asset" sibling file) straight to their final path instead of through the shared atomic tmp+rename seam (`clients/atomic-write.ts`, #762) — a process killed mid-install could leave a corrupt binary or launcher behind. All nine sites now go through `writeFileAtomicAsync`, which gained an optional `mode` option so the executable bit still travels with the staged file across the rename. The sweep itself judges each write call by its own TARGET argument and by each file's own `node:fs`/`node:fs/promises` import bindings — not by a whole matched line or a whole-file "imports the seam somewhere" check — so a scratch-named DATA argument or a differently-named `fs` import can't hide an unrelated violation. The `session_start` orphan-staging sweep (`clients/runtime-session.ts`) now also covers the installer's `bin/` and `tools/` directories, which it omitted entirely; `sweepOwnStagingFiles` doesn't recurse, so an orphaned staging file left there by a kill mid-install was never reaped and repeated kills would have accumulated full-size orphan binaries. Fault injection also found two read-side gaps: `clients/instance-registry.ts` degraded a corrupt/torn `instances.json` to empty exactly like a genuinely missing file, with no way to tell a real torn-write regression from a normal clean start — a non-`ENOENT` read failure (or a valid-but-wrong-shape file, which a torn tail can produce) now records one `instance-registry-corrupt` degradation via `recordDegradationOnce` (deduped per session, since the read path is called far more often than just at session start), while a missing file stays silent. And `deserializeProbeCache`'s locked write-side merge threw on a torn `probe-cache.json` instead of degrading like its sibling deserializers, so a torn cache file could never be repaired by a later successful flush — it now degrades to `{}` and logs the recovery, matching `diagnostic-dispositions.ts` and `actionable-warnings.ts`.
|
|
444
|
+
|
|
445
|
+
- **~16 CLI runners now log recovery when auto-install fixes a failed tool probe (closes #1612)** — `resolveAvailableOrInstallUnshared`, the shared seam behind golangci-lint, ruff, shellcheck, pyright, knip, jscpd, and about a dozen other runners, wrote a latched `unavailable` `availability_decision` row when the PATH probe missed, then fell through to the pi-lens installer. If the installer resolved the binary, nothing recorded that recovery — the durable log kept saying the tool was off after it came back on. This is the same defect shape #1606/PR #1610 fixed in `security-scan-client.ts`'s `ensureViaInstaller`, on a seam with a much larger blast radius. A second row now fires on that recovery path, with `verdict: "available"` and an `evidence.source` tag DERIVED from the tool registry's own install strategy (`managed-dir`, `pip-user`, `github-release`, `archive-dist`, or `maven-jar` — never hand-mapped). The row fires once per correction, and its `install`/`resolved` evidence is read from the installer's own attempt record rather than assumed from a truthy result, so a cache-resolved recovery reads `install: "not-attempted", resolved: "cache"` instead of dishonestly claiming a fresh install on every dispatch.
|
|
446
|
+
|
|
447
|
+
- **Marking a secrets/security finding false-positive or won't-fix now sticks, on every reporting lane (refs [#1617](https://github.com/apmantza/pi-lens/issues/1617))** — `applyDispositions` (the dispatch pipeline's agent/user mark filter) was only ever wired into the per-edit dispatch path; gitleaks, trivy, and govulncheck reported straight from their own scan caches with zero disposition wiring, so an agent-marked false-positive re-reported on every turn_end gate check and every `lens_diagnostics mode=full` scan. A new `applyDispositionsMultiFile` (`diagnostic-dispositions.ts`) extends the SAME anchor derivation dispatch already uses to lanes whose findings span many files in one report, grouping by each finding's own file and reading its current content once; a file that can't be read fails OPEN (still reported) for a false-positive's content-hashed anchor, while a weak-anchored suppress mark — which never looks at content — still applies. Wired into `lens_diagnostics mode=full`'s single `record()` choke point (covers knip, jscpd, madge, gitleaks, govulncheck, opengrep, trivy, dead-code in one place) and into turn_end's gitleaks secrets gate plus its govulncheck/trivy advisory sections, using each lane's existing `ProjectDiagnostic` adapter so a mark made against what `lens_diagnostics` showed the agent is honored everywhere. A disposition-dropped finding now surfaces a "suppressed by disposition: N" trace in both surfaces instead of silently vanishing (the #1616 suppressed-bucket rule). Trivy's *secret* findings (as opposed to its CVE findings, which are covered) have no existing `lens_diagnostics`-surfaced identity to anchor a mark against and are tracked as a follow-up rather than shipped with an unreviewed anchor shape.
|
|
448
|
+
|
|
449
|
+
- **Idle-reset timer no longer destroys the LSP service mid-sweep (closes [#1618](https://github.com/apmantza/pi-lens/issues/1618))** — `handleTurnEnd` arms a detached idle-reset timer on a file-less turn, but `lens_diagnostics mode=full` grants its own workspace sweep a 300s wall-clock ceiling; a maintainer-supervised forensics pass found the timer firing mid-sweep and mislabeling ~81 service-destroyed files as budget exhaustion. `clients/lsp/workspace-sweep-hold.ts` adds a token-based hold that `runWorkspaceDiagnostics` acquires for its whole call (try/finally, so an overlapping sweep or a throw still releases correctly); the idle-reset timer now defers instead of firing while any sweep holds it, and re-arms a fresh delay once the last sweep releases. The delay on EVERY idle-reset path (base, subagent, and cross-process-budget-shortened) is now derived from the sweep's own wall-clock ceiling plus a safety margin, so the constants can't drift back into this relationship — an accepted, deliberate cost of a longer default idle residency (up from 240s to ~360s) for the derivation's coherence. A hold also clears at `session_start` and force-releases past its own max age (with a distinct log record), so a leaked hold can't permanently disable idle reset. `LSPWorkspaceDiagnosticResult` gains a discriminated `unconfirmedReason` (`budget` / `inconclusive` / `coverage_gap` / `service_destroyed` / `error` / `binding_mismatch`) so a destroyed service, a stale content binding, a real timeout, and a thrown error each render distinctly instead of collapsing into "within budget"; `touchFile`'s destroyed early-return now emits a cheap, local `lsp_touch_file` record instead of leaving zero trace; and `scripts/analyze-pi-lens-logs.mjs` (the forensics tool that found #1618) now reads the per-reason tally instead of blaming budget exhaustion for every unconfirmed file.
|
|
450
|
+
|
|
451
|
+
- **LSP teardown no longer hangs on a wedged server, leaking the process (refs #1620, #1459)** — `clientShutdown` bounded the `shutdown` request but awaited the `exit` notification with no timeout. A notification write on a pipe that is not draining neither resolves nor rejects, so the await never returned. Everything after it was skipped: the connection was never disposed, no `lsp_client_shutdown` record was written, the child was never deregistered from the instance registry, and the process was never killed. The wedged-write breaker calls this teardown precisely when a server's stdin has already been proven wedged, so every demoted server leaked — three surviving `ast-grep` processes on one host, plus older orphans, with coverage degrading as the leak compounded. The `exit` notification now has its own ceiling (`PI_LENS_LSP_EXIT_NOTIFY_TIMEOUT_MS`, 1s, matching the request budget, so a healthy server still exits gracefully), and disposal, the record, the deregistration, and the process-tree kill moved into a `finally` block so they run no matter how either write behaves. The `lsp_client_shutdown` record gains `exitNotifyTimedOut` and a rolled-up `shutdownOutcome` of `graceful`, `forced`, or `fast`, so a forced teardown is countable from the log instead of showing up as a missing record.
|
|
452
|
+
|
|
453
|
+
- **LSP teardown: live-wedged proof, precise failure attribution, and safe idempotent shutdown (refs #1620)** — closes out the residuals from #1624's bounded `clientShutdown` fix. Adds a real-child-process fixture (`FAKE_LSP_WEDGE_STDIN_AFTER_INIT`) proving the bound holds against a genuinely wedged OS pipe, not just a mocked connection — the prior unit-level test alone did not exercise real pipe backpressure. `lsp_client_shutdown` now distinguishes a real timeout, a genuine rejection, and a SWALLOWED stream error (`safeSendRequest`/`safeSendNotification` resolve instead of rejecting on EPIPE/disposed-connection, which previously reported a nothing-was-delivered teardown as `shutdownOutcome: "graceful"`) via `shutdownRequestUndelivered`/`exitNotifyUndelivered`, alongside the existing `*TimedOut` pair. `clientShutdown` is idempotent — two callers racing to shut down the same client (8 call sites can race the same state) share one teardown instead of each running the RPC handshake and inflating any `shutdownOutcome: "forced"` count — but ONLY when the in-flight teardown's options are at least as aggressive as the new call's; a `fast`+`processExiting` session-exit call no longer inherits a weaker in-flight graceful teardown's wait (or its `taskkill` spawn, which `processExiting` exists specifically to forbid while the event loop is closing). A rejected teardown also no longer latches a permanently-dead promise — it clears so the next call actually retries instead of silently giving up on the leak.
|
|
454
|
+
|
|
455
|
+
- **Rename propagation no longer hangs on a wedged LSP server (refs #1621, #1620)** — `LSPService.renameFile` awaited `client.closeDocument` and `client.didRenameFiles` with no timeout. A notification write on a pipe that is not draining neither resolves nor rejects, so one wedged server stalled its `Promise.all` and blocked the rename for every healthy client alongside it. Both notifies now carry their own budget (`PI_LENS_LSP_RENAME_NOTIFY_TIMEOUT_MS`, 1500ms). Rename propagation is best-effort advice to servers, not a correctness gate, so a timed-out `didRenameFiles` notify is recorded in the existing failure ledger and the rename still completes; a timed-out `didClose` still aborts and resynchronizes the rename, matching the existing failure path. Each ledger entry now carries a `disposition` of `timedOut` or `rejected`, so an empty failure list still means clean and a stall is never indistinguishable from a genuine rejection.
|
|
456
|
+
|
|
457
|
+
- **Cached secret findings no longer replay pre-edit line numbers (refs #1622)** — the turn-end secrets blocker cited a gitleaks line for the full 30-minute cache TTL even after the agent edited that file, because the delivery gate probed only whether the cited path still existed. Existence is not authority: the TTL says the cache is young and the stat says the file is there, and neither says the line number is still true. The gate now returns a three-way freshness verdict per cited path, from one memoized `statSync` per delivery. A deleted path still drops. A path whose mtime is newer than the store's `scannedAt` is **demoted, never dropped** — the finding is still surfaced, out of the blocker tier and without the line number the edit invalidated, because dropping it would let any edit, malicious or accidental, mute a real credential. An unmodified path delivers unchanged at full severity. Trivy's secrets lane gets the identical gate, and govulncheck drops the cached call-site line for an edited file while keeping the CVE. An unparseable or absent `scannedAt` disables the staleness arm rather than demoting a whole store on a clock anomaly. Each demotion writes one bounded `finding_stale_line_demote` record per store per delivery, mirroring `finding_dead_path_drop`, so the replay window is readable from `latency.log` instead of only inferable. What a deleted file means is now per-store: for secrets it drops, because a credential in a deleted file cannot be rotated; for govulncheck it demotes, because the CVE is pinned by `go.mod` and one deleted call site does not un-pin it. Demoted secrets keep their rule id and source (`src/config.ts — generic-api-key [gitleaks]`) so the agent can still triage them, and they render in their own tier with an imperative preamble rather than under the "no action required this turn" advisory label. A turn whose only finding is a demoted secret reports as `stale_secrets_pending` rather than `clean`.
|
|
458
|
+
- `clients/advisory-provenance.ts` — three-way freshness verdict, per-delivery stat memo, the `gateFindingsByPathFreshness` delivery seam, and the per-store `onMissing` policy.
|
|
459
|
+
- `clients/runtime-turn.ts` — gitleaks, trivy secrets, and govulncheck read through the gate; demoted secrets get their own tier, preamble, rule/source detail, and turn-result telemetry; the govulncheck header renders off the post-gate list so it can never print with zero rows.
|
|
460
|
+
- `clients/latency-logger.ts` — `finding_stale_line_demote` excluded from `lastPhase` attribution, like its drop-record sibling.
|
|
461
|
+
|
|
462
|
+
- **Fix "age unknown old" grammar in lens_diagnostics cache-age notes (refs #1623)** — the render call sites that report a lane served from cache in `lens_diagnostics mode=full` (test-runner findings, the cheap project scan under `refreshRunners=cached`) appended the literal word "old" after `formatCacheAge` unconditionally. When the underlying cache metadata had a missing or corrupt timestamp, `formatCacheAge` already degraded to "age unknown" instead of fabricating a NaN age (#1623 fix-round F4) — but the caller still glued "old" onto it, rendering the ungrammatical "test-runner (age unknown old)". A new `formatCacheAgeOld` helper (`clients/project-diagnostics/extractors.ts`) is the single place that decides whether "old" belongs on the string; both render sites now go through it instead of hand-rolling the suffix.
|
|
463
|
+
|
|
464
|
+
- **lens_diagnostics now names every lane that did not run, instead of rendering silence as clean (refs [#1623](https://github.com/apmantza/pi-lens/issues/1623))** — `mode=full` without `refreshRunners` never ran the heavyweight analyzers (gitleaks, knip, trivy, govulncheck, opengrep, jscpd, madge, dead-code) or the cheap in-process project scan (tree-sitter/fact-rules/ast-grep), but said nothing about it — a result with no gitleaks section read exactly like "gitleaks ran and found nothing," per a maintainer-supervised dogfood forensics pass. Each lane now renders a "not run (\<reason\>)" marker. Each gate captures its own specific reason at the exact point it decides, reusing the SAME availability taxonomy (`AvailabilityOutcome`/`AvailabilityCause`) dispatch-side messages already use — so a durable absence ("gitleaks binary unavailable"), a transient probe under its own retry cooldown ("retry cooldown (15s), not a missing install"), and a project-trust install denial for govulncheck are three different, honest sentences instead of one confident "unavailable" guess. That taxonomy is a single source of truth (`fetchFreshProjectDiagnostics`'s `coldReasons`, `clients/project-diagnostics/fresh-fetch.ts`) the render layer reads rather than re-deriving. A second dogfood pass reported the mirror problem for cache-served lanes (test-runner's cache-only findings, and the cheap project scan under `refreshRunners=cached`, including when nothing has been cached yet) being misread as a fresh run — all now render their status explicitly ("cached, 18m old — not re-run", or "not run (no cached scan; refresh to populate)"), guarding a corrupt/missing cache timestamp against rendering a fabricated "NaNh old".
|
|
465
|
+
|
|
466
|
+
- **A blocking secrets finding can no longer be silenced by a loosely-matched mark (refs [#1617](https://github.com/apmantza/pi-lens/issues/1617))** — the disposition filter added for the secrets/security lanes let a WEAK-anchored suppress or defer (matched on file/tool/rule/message, no line content) drop a `"blocking"`-tier finding. Two distinct secrets sharing one gitleaks rule — two different AWS keys, say — collapsed onto the same weak anchor, so marking one false-positive silenced both. A blocking finding now drops only via the STRICT, content-bound false-positive anchor; a weak suppress or defer never touches it, in the dispatch filter, the cache-only instant filter, and the session-wide defer set alike. The suppressed-count trace (`lens_diagnostics mode=full` and turn_end) now also breaks its total down per analyzer ("gitleaks 2, knip 1"), and a per-project defer can no longer bleed into an unrelated project sharing the same relative file path.
|
|
467
|
+
|
|
468
|
+
- **Trivy secret findings now honor dispositions (closes #1628)** — Trivy's own secret scan (`TrivyResult.secrets`) had no `ProjectDiagnostic` identity for an agent to anchor a `lens_diagnostic_mark` call against, so a false-positive mark could never suppress it — the same bug #1617/#1625 fixed for gitleaks, govulncheck, and Trivy's CVE lane, left open for Trivy's secret lane. A new `trivySecretFindingToProjectDiagnostic` adapter (rule id `trivy-secret:<ruleId>`, namespaced apart from the CVE lane's `trivy:<vulnerabilityId>`) now surfaces these findings in `lens_diagnostics mode=full` and routes them through `applyDispositionsMultiFile`/`turn_end`'s disposition filter, matching the existing gitleaks/govulncheck/Trivy-CVE wiring.
|
|
469
|
+
|
|
470
|
+
- **`instance-reaper-prune-concurrency` no longer times out flakily under CI contention (closes #1629)** — root-caused, not just re-budgeted. Solo, the 40-iteration `#1217` concurrency test measured 1.7-3.3s against vitest's 5000ms default, but 8 runs alongside 5 contended sibling suites blew that budget 3/8 times (5.1-6.4s) — genuine test-budget starvation, not a `pruneDeadInstances` regression (the write path is unchanged fs I/O; #1738's registry-write serialization did not touch it). The describe block now declares an explicit 30s timeout (matching this repo's existing `HEAVY_IO_TIMEOUT_MS` convention), with the measurement in a comment. A second, independent defect surfaced under the old 5s budget: a timed-out test's `for` loop keeps running in the background (vitest's timeout races a timer against the test promise; it does not cancel in-flight work), and its next `pruneDeadInstances` call re-reads the now-repointed `PI_LENS_HOME` and writes its zombie staging file into the NEXT test's temp directory — failing that test's "leaves no staging files behind" assertion with the PRIOR test's litter. `afterEach` now drains the last-known in-flight prune promise before the next test's `beforeEach` repoints the env var, closing that window. Both the timeout comment and the drain are backed by controlled before/after measurement (N=8 contended local runs pre-fix: 3 failures; N=4 contended runs post-fix: 0 failures) and a mutation probe (forcing a real timeout at a 700ms budget: with the drain, only the timed-out test fails, cleanly; with the drain removed, the sibling test also fails on leftover `.tmp-` litter, 3/4 runs).
|
|
471
|
+
|
|
472
|
+
- **knip no longer reports an export as unused after a new test imports it (refs #1630)** — pi-lens runs knip with its own `--cache-location`, and knip's glob cache only revalidates the directories that contributed a matched file. A directory that matched nothing when the cache was written is never recorded, so a consumer added to it stays invisible to every later cached run: knip keeps reporting the export as unused, and pi-lens renders that stale verdict as fresh. This is what made a dogfood session see an "unused export" warning that a direct `knip` run in the same project did not reproduce. pi-lens now deletes knip's glob cache before each run, which forces a fresh file walk while the module and plugin caches — which carry nearly all of the speed — survive. Measured on this repo with knip 6.4.1: uncached 3.3s, fully cached 1.3s, glob cache dropped 1.4s. Upstream knip fixed the glob cache in 6.28.0; the prune is unconditional because the managed install floats and older versions are still common.
|
|
473
|
+
|
|
474
|
+
- **Cached cross-file blockers no longer replay for the whole session after the cause is fixed in a dependency (closes [#1631](https://github.com/apmantza/pi-lens/issues/1631))** — a blocker recorded on a file F is a verdict about F *and everything F imports*, but every existing invalidation path keyed on F alone. When the cause was fixed in a dependency G — especially out-of-band, where G is never dispatched — no event fired and F's stale verdict re-served at every turn end and on every `lens_diagnostics mode=all` for the rest of the session (the #1561 dependency-axis remainder, observed live four times). A turn-boundary freshness sweep now runs before a cached blocking finding is re-served: it stats F and its forward imports (resolved through the parse layer — no reverse-dependency index, so the tests-free-index blocker that holds #1561's full invalidation does not apply) against the verdict's timestamp. On drift the finding is DEMOTED, not dropped (#1419): the turn-end re-serve moves it out of the authoritative blocker channel into a `[stale — re-run to confirm]` advisory, and the widget store that feeds `mode=all` marks it stale so it renders as a non-blocking `[stale]` entry instead of a 🔴. The gate lives at the cache layer on both stores — `RuntimeCoordinator` inline blockers and `widget-state` — and never re-pulls LSP verdicts on its own, avoiding the document-resync hazard of re-querying an LSP whose in-memory dependency document is itself stale. Each turn logs a bounded `blocker_freshness_sweep` phase (and `mode=all` a `blocker_freshness_widget_gate` phase) naming the revalidated/kept/truncatedImports counts so a future replay is reconstructible from `latency.log` alone. The full dependency-axis invalidation (actively re-verifying consumers when a dependency is dispatched clean) remains deferred to #1561.
|
|
475
|
+
|
|
476
|
+
- **Dependency-drift blocker freshness gate: review-round hardening (refs #1631)** — the read-time freshness gate from PR #1633 landed with issues found across two review rounds. The `+1ms` drift tolerance (borrowed from same-process single-writer reconciles) false-demoted a blocker recorded immediately after its own file write on Windows, where mtime measurably leads `Date.now()` by up to ~11.4ms; the tolerance is now `+50ms` at every comparison site, including a third site (`reconcileStaleWidgetFiles`'s own-file gate) found while red-proving a flaky-looking "control" test — the same host-clock skew, not cross-test state. Demotion is now narrowed to language-server-sourced findings (`sources`/`tool === "lsp"`): an ast-grep secret or a CVE finding doesn't stop being true because a file it imports changed, so it stays fully blocking through a dependency edit. A dependency-drift demotion no longer survives `exportWidgetState`/`importWidgetState` — the same #1348 shape as a failed-formatter entry, one field over — so a real blocking finding can't resume as permanently non-blocking after a session restore with the dependency pinned back into the past (`WIDGET_STATE_VERSION` bumped to 3). Round 2 found the fix itself restored `diagnosticCounts` verbatim from the snapshot instead of recomputing it from the migrated (stale-stripped) entries — a resumed record could report `isBlocking() === true` on an entry while its own `diagnosticCounts.blocking` still said 0, inverting the one predicate every consumer trusts; counts are now derived the same way every other write site derives them. Forward-import resolution is now memoized per turn (keyed on path+mtime, reset on `RuntimeCoordinator.turnIndex` advancing, not process-lifetime), covering both the turn-end sweep and the `mode=all` gate that were independently re-parsing the same files. Each `blocker_freshness_sweep`/`blocker_freshness_widget_gate` latency record now names how many entries had their import list truncated at the drift-check cap. The footer/red-dot render no longer silently drops a demoted finding — it now renders with a dimmed marker and the stale-marker text instead of vanishing from the list `isBlocking` was also gating the render loop against. `STALE_LINE_MARKER` moved out of the turn orchestrator into its own leaf module (`clients/stale-marker.ts`) after round 2 found the low-level widget store importing it from `runtime-turn.ts` introduced a new module cycle. The `retired` count in `BlockerFreshnessCounts`, always zero because the gate is architecturally demote-not-drop, is removed rather than kept as dead API surface (and the two remaining doc sites that still promised it are corrected); the module docs name the dynamic `import()`/`require()` resolution boundary as an honest gap.
|
|
477
|
+
- `clients/blocker-freshness.ts` — `MTIME_DRIFT_TOLERANCE_MS = 50` (exported), `sources`-based LSP-only demotion gate, per-turn import-resolution memo, `ForwardImportMtimes.truncated`, `BlockerFreshnessCounts.truncatedImports`, `retired` removed.
|
|
478
|
+
- `clients/widget-state.ts` — `reconcileStaleWidgetDependencyBlockers` narrowed to `tool === "lsp"` and returns `{ demoted, truncatedImports }`; `reconcileStaleWidgetFiles`'s own two tolerance checks widened to the shared constant; `migrateEntryStamps` strips `stale` on import; `importWidgetState` recomputes `diagnosticCounts` from the migrated entries instead of restoring the snapshot's counts verbatim; `WIDGET_STATE_VERSION` bumped to 3; `renderWidget`'s blocker-detail loop renders demoted entries instead of dropping them; `STALE_LINE_MARKER` now imported from `clients/stale-marker.ts`.
|
|
479
|
+
- `clients/stale-marker.ts` (new) — `STALE_LINE_MARKER`, moved out of `runtime-turn.ts` to break a widget-state → runtime-turn module cycle (`madge --circular clients/widget-state.ts`: 45 → 44 cycles, the one involving `runtime-turn.ts` gone).
|
|
480
|
+
- `clients/runtime-turn.ts` — imports `STALE_LINE_MARKER` from the new leaf module instead of defining it (resolving the merge conflict with master's #1627); `retired` dropped from, `truncatedImports` added to, the turn-end latency record.
|
|
481
|
+
- `tools/lens-diagnostics.ts` — imports `STALE_LINE_MARKER` from the leaf module instead of a hardcoded literal; passes the runtime's `turnIndex` into the `mode=all` gate and logs `truncatedImports`.
|
|
482
|
+
- `AGENTS.md`, `.changelog/fix-1631-blocker-freshness-gate.md` — `retired` replaced with `truncatedImports` in both doc sites that still promised the removed count.
|
|
483
|
+
|
|
484
|
+
- **Every agent-facing finding surface now either routes through the shared freshness/disposition gate stack or explicitly states its cache age — no more silent third state (refs [#1634](https://github.com/apmantza/pi-lens/issues/1634))** — #1461/#1561/#1622/#1631 each converged the SAME freshness fix onto one store at a time (secrets, govulncheck, cached cross-file blockers), but nothing enumerated every surface that renders findings to the agent, so a new store could still bypass both the gate and any honesty about staleness. The trivy CRITICAL-dependency-CVE blocker, non-critical CVE advisory, and license-risk advisory in `clients/runtime-turn.ts` were exactly that gap: package-pinned findings with no cited file:line to freshness-gate, reading a session_start cache that can be arbitrarily old, rendered with zero age signal — a 🔴 STOP blocker from hours ago presented as current. `clients/finding-delivery-gate.ts` is the new single documentation home: a `DELIVERY_SURFACES` registry enumerates every render seam (turn-end blocker/advisory tiers, `lens_diagnostics` mode=full/all, the widget footer, agent nudges, the persisted project-diagnostics snapshot) as either `gated` (names the real gate call) or `labeled` (names a reason and an age source), and `assertNoDeliveryBypass()` rejects any third shape. The three trivy reports now carry a `formatCacheAgeLabel()` suffix (`"trivy, scanned 45m ago"`, degrading to `"scan age unknown"` on a missing/unparseable timestamp rather than fabricating a number) instead of no signal at all. `tests/clients/finding-delivery-gate.test.ts` ground-truths every registered surface's declared gate/label call against its actual source file, so a gate call removed from source without updating the registry — or a labeled surface silently losing its age string — fails the suite.
|
|
485
|
+
|
|
486
|
+
- **markdownlint and 14 other CLI runners now log recovery when the install-fallback helpers fix a failed probe (closes #1636)** — `resolveToolCommandWithInstallFallback` and `resolveCommandArgsWithInstallFallback`, the seam behind actionlint, biome-check, hadolint, htmlhint, ktlint, markdownlint, mypy, oxlint, rubocop, sqlfluff, stylelint, swiftlint, taplo, vale, and yamllint, recovered a tool through the pi-lens installer after a checker had already latched it `unavailable`, but wrote no compensating row. The durable log kept saying markdownlint's lane was off while it ran and succeeded (live evidence from a dogfood session). This is the third site of the #1606 defect shape: #1610 fixed `ensureViaInstaller`, #1612/#1615 fixed `resolveAvailableOrInstall`. This fix reuses the exact `emitCompensatingAvailableRow` constructor #1615 landed rather than forking a third copy, so both seams share the once-per-correction memo and the same evidence derivation. Carried over from #1615's review: `resolved` now distinguishes `"cache"` (a session/probe cache hit), `"path"` (a plain PATH or managed-dir discovery, no install involved), and `"declined"` (policy refused the install and handed back whatever discovery found anyway) — collapsing all three into `"cache"` misread a project-trust decline as a resolved-and-trusted answer. `getInstallAttempt` is now a hard import (a removed export throws instead of silently reading as "not attempted").
|
|
487
|
+
- `clients/dispatch/runners/utils/runner-helpers.ts` — `emitCompensatingAvailableRow` (shared row constructor), `resolvedTagForAttempt`, `verifyOrInstallCommand` wiring.
|
|
488
|
+
- `clients/dispatch/runners/utils/availability-policy.ts` — `ProbeEvidence.resolved` widened to `"cache" | "path" | "declined"`.
|
|
489
|
+
- `clients/installer/index.ts` — `getLastEnsureResolutionSource`, the new signal that lets the compensating row tell a plain PATH/managed-dir discovery apart from a cache hit.
|
|
490
|
+
|
|
491
|
+
- **`createVenvFinder` now checks the managed tools dir before falling back to PATH (closes #1638)** — the resolver behind `createAvailabilityChecker` (golangci-lint, ruff, shellcheck, pyright, shfmt, spotbugs, terragrunt, tflint, trivy, helm, jscpd, knip, ktfmt, madge, and others) checked venv paths, then fell straight to a bare-name PATH probe. A tool installed only into `~/.pi-lens/tools/node_modules/.bin/<tool>` (where `ensureTool` puts npm-strategy installs) never resolved there, so every dispatch re-probed PATH, missed, spawned a doomed `--version` process, and only then fell through to the installer's own cache a few lines later — one wasted spawn per dispatch, for the life of the session, on top of the availability noise that made #1615's compensating row re-fire on every call. `createVenvFinder` now checks `findManagedNodeToolBinary` — the same managed-dir lookup already used elsewhere in this file — between the venv paths and the bare PATH fallback.
|
|
492
|
+
- `clients/dispatch/runners/utils/runner-helpers.ts` — `createVenvFinder`.
|
|
493
|
+
|
|
494
|
+
- **TypeScript diagnostic-sequence records now report a real settle duration, a real version, and a distinct source for every settle shape (closes #1639)** — `lsp_typescript_diagnostic_sequence` had the same three bugs on BOTH its producers. The push-path producer, `logSequence` (the `textDocument/publishDiagnostics` handler), is the one that actually generated the issue's cited evidence: it put time-since-`didOpen` in the top-level `durationMs` field on every settled record instead of the settle operation's own time (147/239 records showed >60s "durations" for settles that took milliseconds), hardcoded `version: docVersion ?? null`, and logged its raw per-publication receipt with no `settleSource` at all — the exact "unsettled-then-settled" pair (~61ms apart, near-identical document age) the issue described as a double-emit is this function's own designed shape (a receipt record immediately followed by its eventual settle record), not the pull path. The pull-path producer, `logTypeScriptPullSettle`, had the identical durationMs/version bugs and a real (if rarer) double-settle: `ensureWarmForSweep`'s warm-up-only readiness probe and a sweep's real content-collecting touch both run a genuine pull round trip for the same file — both fixed here too. `durationMs` now measures the settle itself (the debounce wait for a `quiet-window` settle, 0 for an immediate `first-push` settle or a `pull`/`pull-warmup` request's own round trip); document age keeps its own honest name, `metadata.elapsedSinceDidOpenMs`. `version` never reports `null`: push settles report the real server-reported version or `"push-unversioned"`; pull settles report the real tracked value from `diagnosticsVersionsByPath` or `"pull-unversioned"`. Every settle shape now carries an explicit `settleSource` — `"first-push"`, `"quiet-window"`, `"publication"` (the raw per-publication receipt, not itself a settle), `"pull"`, or `"pull-warmup"` — so no two legitimate observations read as indistinguishable duplicates.
|
|
495
|
+
- `clients/lsp/client.ts` — `logSequence` (push path) and `logTypeScriptPullSettle` (pull path) both take the settle's own `durationMs` and an explicit `settleSource`; the push handler's per-publication receipt is now tagged `"publication"` instead of logging with no source at all; both fall back to an honest `-unversioned` marker instead of `null`.
|
|
496
|
+
- `clients/lsp/index.ts` — `touchFile`'s per-server wait tags `pullSettleSource: "pull-warmup"` when the touch's `source` is `"lsp_sweep_warmup"`.
|
|
497
|
+
- Checked `scripts/analyze-pi-lens-logs.mjs` for consumers of the old `durationMs`/`version: null` semantics on this phase: none exist today, so nothing else needed updating.
|
|
498
|
+
|
|
499
|
+
- **TypeScript diagnostics on files outside every tsconfig no longer block** — a file that matches no project's `include` is still checked by tsserver, but in a synthetic *inferred* project with default compiler options the project's own `tsc --noEmit` never uses. Those diagnostics now render as warnings labelled `not in any tsconfig project — checked with inferred settings; add <dir>/** to a tsconfig for authoritative checking` instead of unlabelled 🔴 blockers. They are demoted, never suppressed: the live report that prompted this found the batch was a mix of phantom errors and genuine ones the project's own gate cannot see. Detection asks tsserver directly (`projectInfo` → `configFileName`); when the probe cannot answer, nothing is demoted.
|
|
500
|
+
|
|
501
|
+
- **Diagnostics no longer cite lines past the file's current EOF (refs #1641)** — a live session got errors at lines 407-410 of a file that was 402 lines on disk and unmodified: the LSP's in-memory document had drifted longer than disk with no write ever happening, so the mtime-based freshness gates (#1622/#1627) had nothing to catch. The fix is a structural, orthogonal check: a diagnostic whose cited line exceeds the file's CURRENT on-disk line count cannot be describing current content, regardless of any timestamp. The gate demotes the entry (`stale: true`), never drops it — the underlying issue may still be real, just at a coordinate this gate can no longer vouch for — and writes one bounded `diagnostic_past_eof` record naming the file, cited line, actual line count, and serving surface. `lens_diagnostics` (mode=all and mode=full, via `formatAllMode` — the single point both converge through) triggers a best-effort document resync on a demotion, so the desync heals instead of re-serving the same stale verdict on the next call; the TUI widget's higher-frequency render path demotes without resyncing, to avoid storming the LSP with didOpen calls from a loop that fires far more often than an agent's edit cadence. A new `lsp_document_send` record logs each didOpen/didChange's document version and content length at SEND time, so a later past-EOF citation can be paired with the send that produced it and the divergence moment reconstructed. The line-count check counts newlines in bounded chunks (never a full-file read) and memoizes the result in a shared, mtime-invalidated, size-bounded cache — see the review-round fragment below for the full cost story.
|
|
502
|
+
- `clients/diagnostic-line-freshness.ts` — new: `demotePastEofDiagnostics` (the gate), `getCachedLineCount`/`createLineCountCache` (mtime-invalidated line count), `resyncDocumentOnPastEof` (best-effort didOpen resync via `LSPService.openFile`).
|
|
503
|
+
- `clients/widget-state.ts` — `WidgetDiagnostic.stale`; `isBlocking`/`countDiagnostics` exclude demoted entries from the blocking/error/warning tallies; the gate runs in `getFileDiagnosticSummaries`, `getFileDiagnostics`, and the TUI's blocking-diagnostics render loop.
|
|
504
|
+
- `tools/lens-diagnostics.ts` — `formatAllMode` gates the final merged summary set (covering both the cached mode=all path and mode=full's freshly-merged LSP results, since a "fresh" sweep's server can still be citing its own stale in-memory document); demoted lines render `PAST_EOF_STALE_MARKER` instead of a line number, and a file whose only findings are demoted stays visible with a "stale — re-run to confirm" note rather than reporting clean.
|
|
505
|
+
- `clients/lsp/client.ts` — `recordSentContent` now logs `lsp_document_send` (version + content length) on every didOpen/didChange.
|
|
506
|
+
|
|
507
|
+
Turn-end's "unresolved from this turn" inline blockers are NOT covered — that store only carries a pre-formatted summary string (`{ filePath, summary }`, `runtime-coordinator.ts`), with no structured line field for this gate to check, and #1633's dependency-freshness gate is already the active work on that exact structure. Composing a line-count check into it belongs with that PR, not duplicated here.
|
|
508
|
+
|
|
509
|
+
- **Past-EOF diagnostic gate review round (refs #1641)** — six findings from adversarial review. The line-count convention was `wc -l` style, but `WidgetDiagnostic.line` is LSP `range.start.line + 1` addressing: a document with N newlines has N+1 addressable lines (the position after a trailing `\n`, or the single line of an empty document, is real, and servers anchor EOF diagnostics there — e.g. TS1005 `'}' expected`). A 2-real-line file with a trailing newline was demoting its OWN valid EOF error at line 3. Fixed the count to `newlineCount + 1` uniformly. The line-count cache was scoped to one render pass and re-created every call, which deduped nothing (each file appears once per pass) — every serving call paid a full synchronous file read, measured at 35.68ms/call across 40 files and 30.78ms per TUI frame on a 6MB file. Replaced the full read with a bounded chunked byte scan (raw file descriptor, no UTF-8 decode, no whole-file string) behind a 4MB size gate (matching `captureReadContentBinding`'s precedent), and made the cache shared and FIFO-bounded across calls instead of single-use — it still cannot go stale, because every read re-stats the file first and only trusts the memo when the mtime still matches. The store also used to latch: once a diagnostic was marked `stale`, the gate never re-checked it, so a transient shrink (a formatter pass, a checkout, a partial write) that later restored stayed permanently demoted. The gate now re-derives the verdict from current inputs on every call — the rising edge logs and resyncs, the healing direction is silent, and a restored line un-demotes on its own next read. The `lsp_document_send` telemetry record's line count used `content.split("\n")`, which both disagreed by one with the gate's pre-fix convention and allocated one substring per line (13.3ms/200k allocations on a 7MB send); it now counts newlines directly, matching the gate exactly. The gate now runs AFTER dispositions and rule policy in `lens_diagnostics`, so a policy-dropped or marked-suppressed finding never triggers a resync or a telemetry record for something that isn't actually being served. `PAST_EOF_STALE_MARKER` now composes with the shared `STALE_LINE_MARKER` base (extracted out of `runtime-turn.ts` into `clients/stale-marker.ts`) instead of duplicating the string.
|
|
510
|
+
- `clients/diagnostic-line-freshness.ts` — `newlineCount + 1` line convention; chunked byte-level newline scan (`countNewlinesChunked`) replacing the full-file read; 4MB size gate; shared, FIFO-bounded, mtime-invalidated cache (`sharedLineCountCache`) as the default, with `createLineCountCache()` still available for test isolation; `demotePastEofDiagnostics` re-derives `stale` on every call instead of short-circuiting on an already-demoted entry, logging/resyncing only on the rising edge.
|
|
511
|
+
- `clients/lsp/client.ts` — `recordSentContent`'s `contentLineCount` counts newlines directly instead of `content.split("\n")`.
|
|
512
|
+
- `clients/widget-state.ts` — `applyPastEofGate` always persists the freshly re-derived array back into the record (not only when something newly demoted), so a healed entry's `stale: false` actually reaches the store.
|
|
513
|
+
- `tools/lens-diagnostics.ts` — the past-EOF gate moved to run after `applyWeakDispositions`/`applyRulePolicy`.
|
|
514
|
+
- `clients/stale-marker.ts` — new: `STALE_LINE_MARKER`, extracted from `runtime-turn.ts`.
|
|
515
|
+
|
|
516
|
+
- **Verify round: line-count memo keyed on size too (refs #1641)** — moving the line-count cache to module scope (the review round's F2 fix) introduced a HIGH finding: the memo was keyed on `mtimeMs` alone, and this host's mtime resolution (~1ms) is coarse enough that two writes in the same tick collide — truncate-then-write, a formatter write-back, a checkout, or pi-lens's own auto-format immediately followed by the agent's write. Measured live at 207/300 shrink/restore cycles serving the wrong line count, including a first-read-of-cycle returning a stale count for an 11-line file — a false-demotion source in the same class as the review round's own F1 line-convention bug. `LineCountCacheEntry` now also stores `size`, and a cache hit requires both `mtimeMs` and `size` to match; a shrink/restore cycle always changes size, so this closes the collision with no added I/O (size is already on the same `fs.Stats` the mtime came from).
|
|
517
|
+
- `clients/diagnostic-line-freshness.ts` — `LineCountCacheEntry.size`; `getCachedLineCount` compares both fields for a hit.
|
|
518
|
+
|
|
519
|
+
- **Turn-end inline blockers now gate on past-EOF lines too (closes #1641)** — the "Unresolved from this turn" blocker re-served at `turn_end` (`RuntimeCoordinator`'s `_pendingInlineBlockers`) could still cite a line beyond the file's current on-disk line count, re-asserted at full blocking authority. #1664 wired the past-EOF gate into every `WidgetDiagnostic` surface (widget-state, `lens_diagnostics`, the TUI render loop) but deferred this one store to #1633, which merged without it. The record now carries the cited lines structurally (captured at write time from the same diagnostics the summary text was rendered from, not re-parsed from that prose later), and a new turn-end sweep (`clients/blocker-past-eof.ts`) demotes a record citing an out-of-bounds line to the `[stale — re-run to confirm]` advisory instead of the authoritative blocker channel — the same demote-not-drop precedent, re-derived fresh every turn end so a transient shrink-then-restore un-demotes on its own. It composes with #1631's dependency-drift gate on the same store via a shared `staleReason` field: each gate only heals demotions it made itself.
|
|
520
|
+
|
|
521
|
+
- **A gitignored worktree edit could dirty the parent checkout (refs #1642)** — `tool_result` resolved a relative path against the project root instead of the cwd/worktree the call actually ran under, so a worktree edit could collapse onto a same-relative-path file in the parent checkout, which the deferred-format staleness fallback then formatted. `tool_call` now records the resolution basis (its own cwd) by tool-call identity (correlated via `toolCallId`/`callId`/`requestId`/`id`, whichever the host populates); `tool_result` resolves its own authoritative post-execution path against that basis instead of guessing. A relative path with no recorded basis fails closed (logged as `path_attribution_missing`) rather than falling back to a guess; a genuine call-time/execution-time divergence is logged (`path_attribution_refused`) as a diagnostic without gating the outcome. A blocked call's recorded basis is cleared immediately so it never leaks into the correlation cache. The deferred-format staleness fallback now also requires the queued record's true origin cwd (distinct from the workspace-root `turnStateCwd` used for bookkeeping) to match the claiming session's before treating an aged-out record as a recoverable orphan; a mismatched-origin record is left queued and logged, never formatted or deleted.
|
|
522
|
+
|
|
523
|
+
- **The #1605 lifecycle-smoke lane's phase 5 now genuinely exercises an absent `gh`, and `safeSpawnAsync` decides a never-started spawn from the `close` event's own shape (refs #1651)** — phase 5 pinned outcome `"missing"` for a genuinely-absent `gh` binary, and passed on Windows but got `"non-installable"` on Linux CI. A debug pass added a raw `spawnSync("gh", ["auth", "token"])` probe right where the lane's own removal happens, and it answered `status: 1, stderr: "no oauth token found for github.com"` — a genuine, correctly-completed `gh` run. GitHub-hosted `ubuntu-latest` runners carry a *second* `gh` on PATH that `which gh` doesn't report first; the lane's PATH exclusion only stripped the one `which` did report, leaving the other one reachable the moment the fixture was deleted. Phase 5 was never testing a missing binary on Linux at all — it was asking the classifier to explain a real rejected-auth answer while asserting the missing-tool verdict. `classifyGhTokenFailure` was correct both times; the harness's setup was not. `findAllOnPath` (`which -a` on POSIX; Windows' `where` already lists every match) now finds every `gh` on PATH, and `pathWithGhMasked` swaps in a shadow directory — every OTHER file in that directory linked, `gh`/`gh.exe`/`gh.cmd` omitted — instead of excluding the whole directory: an earlier version of this fix excluded the whole directory and broke phases 1/2/4 instead, because GitHub-hosted `ubuntu-latest` keeps `getconf` in the same `/usr/bin` the second `gh` lives in, and pi-lens's own resource sampler shells out to it. Separately, this investigation also surfaced (and fixed) a genuine, narrower `safeSpawnAsync` hazard: Node's docs leave the `error`/`close` event order unspecified for a child that never launched, and the real shape for that case is a negative code (`-2` for ENOENT on Linux, `-4058` on Windows), with `error` sometimes landing a full tick later via `setImmediate`/`process.nextTick`. The `close` handler now decides straight from `code === null || code < 0` — a shape no completed process ever produces — independent of whether or when `error` fires, reusing `error`'s Error when it's already available and synthesizing an ENOENT-shaped one otherwise, with a single `resolved` flag so whichever handler decides first wins outright (including the inverse: a healthy `close(0, null)` is never downgraded by a late, unrelated `error`, e.g. a post-exit `kill()` failing with `EPERM`). `classifyGhTokenFailure` also gets a belt for the same reason: it no longer trusts a bare `!res.error` as proof `gh` ran and answered — a `null`/negative `status` alone (OS-independent, read off Node's own report) now routes through the missing/transient handling regardless of whether `error` happened to be attached. That spawn-race fix did not turn out to be what made phase 5 fail (the harness bug did), but it closes a real correctness gap this investigation surfaced.
|
|
524
|
+
- `scripts/smoke-availability-lifecycle.mjs` — `findAllOnPath`, `pathWithGhMasked`, `maskedGhDir`, `shadowFile` (replace the single-match `findOnPath`/`pathWithoutDirOf`).
|
|
525
|
+
- `clients/safe-spawn.ts` — the `close`/`error` handlers inside `safeSpawnAsync`.
|
|
526
|
+
- `clients/zizmor-config.ts` — `classifyGhTokenFailure`'s `neverAnswered` guard.
|
|
527
|
+
- `tests/clients/safe-spawn-close-before-error-race.test.ts` — regression tests for the `close(-2, null)`/`close(null, null)` shapes (with and without a delayed or absent `error`) and the healthy-run non-inversion case.
|
|
528
|
+
|
|
529
|
+
- **Package-manager availability latches re-arm at `session_start` (closes #1653)** — `resolveNodePackageManager` keeps one `AvailabilityLatch` per pnpm/yarn/bun/npm in a module-local map, so a genuine "missing" verdict from one session stayed latched into the next: install pnpm mid-day, start a fresh session, pi-lens still reported it missing until a process restart. Same module-local shape as psscriptanalyzer's latches (#1490) and zizmor's `gh auth token` cache (#1535) — `resetDispatchAvailabilityState`'s generation counter never reached it because nothing called the module's own reset hook. `handleSessionStart` now calls `_resetPackageManagerCache()` in its per-session reset block, beside `resetZizmorTokenAvailability()` and `resetPsScriptAnalyzerAvailability()`.
|
|
530
|
+
- `clients/runtime-session.ts` — `handleSessionStart`'s per-session reset block.
|
|
531
|
+
- `clients/package-manager.ts` — `_resetPackageManagerCache`'s doc comment now records it as production wiring, not just a test hook.
|
|
532
|
+
- Review round: making the reset a real production path exposed a latent race in `isAvailable`'s in-flight probe map — its `.finally` deleted the map entry by key rather than by identity, so a pre-reset probe that settled after a new session's own probe for the same manager was already in flight could evict that newer entry and cause a duplicate spawn. Fixed with the same identity guard `resolveMadge` in `dependency-checker.ts` already uses for the equivalent race.
|
|
533
|
+
|
|
534
|
+
- **Deferred format/autofix drain no longer runs mid-run on auto-retry or overflow-compaction (refs #1654)** — pi's `agent_end` fires on every completion of `_runAgentPrompt`, including a run about to auto-retry or resume after overflow-compaction; pi computes `willRetry` only after emitting `agent_end` and never exposes it to extensions. Running the #1387 deferred-format/autofix drain there could format files the agent was still actively working on between retries, shifting lines under queued work and staling in-flight content bindings (the #1642 harm family). The drain now runs at `agent_settled` — pi's documented once-per-run signal ("no automatic retry, compaction, or queued continuation will run") — with a best-effort, time-boxed safety net at `session_shutdown` so a run that ends without ever settling does not strand queued work forever.
|
|
535
|
+
|
|
536
|
+
- **Hardened pi-lens against five pi host-contract facts it mishandled (refs #1655)** — pi's `emitToolCall` is the one extension emit path with no per-handler `try`/`catch`, and its caller rethrows as "Extension failed, blocking execution", so any unguarded throw inside pi-lens's `tool_call` handler blocked the user's tool call outright; the handler now absorbs every throw, records one `tool-call-handler-throw` degradation per tool name, and lets the tool proceed. Telemetry identity no longer comes from `tool_result`/`session_start` event fields that pi never sets — it is read from `ctx.model`, so the reported model and provider stop being `"unknown"` for the whole session, and the seven vacuous fields plus the unreachable branch they fed are gone. A tool_call's edit batch width is snapshotted at handler entry instead of re-read from the live, mutable `input` object hundreds of lines later. pi-lens now resolves a tool path the way pi does, in both of pi's stages: the base resolution (unicode-space folding, `@` mention-prefix stripping, `~` expansion, `file://` conversion, Git-Bash drive paths) and then the fallback ladder (narrow no-break space before AM/PM, NFD, U+2019, NFD plus curly quote). A path pi opens — a macOS screenshot name, an `@`-prefixed mention, a `file://` URL, a non-breaking space — is no longer invisible to the read guard, the LSP warm, and the dispatch, and a path that still does not resolve records a `path-variant-unresolved` degradation instead of returning in silence. A blocked tool call also stays blocked when its own bookkeeping fails: the read guard's refusal is no longer discarded by a throw in the telemetry that follows it. The invariants behind the tool cwd basis and bash-side path attribution are pinned by tests against pi's own build.
|
|
537
|
+
|
|
538
|
+
- **`safeSpawnAsync`'s post-exit pipe-idle wait no longer pays out its full 100ms grace window on every call (refs #1656)** — the wait now finishes as soon as Node's `close` event fires (the stronger, already-available signal that stdio is fully released), instead of always idling out the timer. A normal spawn, where nothing holds its pipes open, now settles in a few ms again; a daemonized descendant that never emits `close` is unaffected and still bounded by the existing grace/cap timers.
|
|
539
|
+
|
|
540
|
+
- **`safeSpawnAsync`'s post-exit pipe-idle wait no longer lets a late, unrelated error steal an already-decided healthy verdict (refs #1656)** — the wait added to fix the daemonized-descendant hang left the outcome undecided across the whole (bounded, up to 2s) idle-grace window, widening a race a prior fix had closed down to a single microtask: a post-exit `kill()` failing with EPERM 10ms after a clean `close(0, null)` could flip a successful run to a spawn failure. The verdict is now latched immediately once the exit/close event decides it, before the idle wait runs, so a late `error` can never downgrade it.
|
|
541
|
+
|
|
542
|
+
- **`safeSpawnAsync` no longer hangs forever on a daemonized descendant that inherits its stdout/stderr pipe (refs #1656)** — the async spawn path waited for Node's `close` event, which only fires once every file descriptor referencing the child's stdio has been released. A Windows-orphaned grandchild (no job object) can hold that inherited pipe open indefinitely, so `close` never fires even though the process we spawned is long dead — the caller hangs, and a hung prober silently loses its result rather than failing loudly. Adopting pi's `waitForChildProcess` construction: finalize off `exit` instead, then wait for stdout/stderr to fall idle (no data for 100ms, re-armed on every chunk, capped at 2s so a pathological never-quiet descendant can't extend the caller's overall budget). A quiet inherited handle now releases after one grace window; an actively-streaming child is still captured in full.
|
|
543
|
+
|
|
544
|
+
- **A compensating `available` row that corrected nothing no longer silences the real one** — the once-per-correction memo now burns only when a latched `unavailable` row actually stood before it. Runners that reach the install seam with no probe of their own (biome-check, oxlint) emitted a row that pre-empted the next genuine latch-then-recover for the same tool and directory, leaving the durable log saying the tool was off while it ran. Managed-tool resolution also runs the installer's own `verifyToolBinary` check instead of a bare `existsSync`, so a broken shim no longer shadows a working PATH binary; a settled verdict is memoized per shim per session, a verification that never got to run keeps the optimistic answer under a bounded cooldown, and concurrent first touches share one probe (#1657).
|
|
545
|
+
|
|
546
|
+
- **Pull diagnostics no longer miss whole categories on multi-source servers** (#1667) — a server that registers `textDocument/diagnostic` once per diagnostic source (Roslyn: syntax, semantic, analyzers; vtsls) had every `registerOptions.identifier` discarded, so pi-lens issued one bare pull and never asked for the other sources. The client now pulls every registered source in parallel plus the bare request, answers on the first source with findings for the file, and merges slower sources into the cache in the background. Result-id inheritance (`kind: "unchanged"`) is tracked per source, a source retired by `client/unregisterCapability` no longer leaves its findings in the cache, and workspace-pull support is read from the registration's `workspaceDiagnostics` flag instead of guessed from the method name.
|
|
547
|
+
|
|
548
|
+
- **LSP servers now learn about files a bash command deleted, even though pi-lens never sends `workspace/didChangeWatchedFiles` type 3 anywhere (refs #1668)** — the only enqueue site for that notification (`handleNotifyOpen`, via the #271 `WatchedFilesQueue`) fires on a document's first open and can only emit type 1 (Created) or 2 (Changed). A turn that ran `rm`/`git rm`/`mv` on a file with no notification at all left gopls' package cache, rust-analyzer's vfs, and clangd's index stale, with diagnostics for the deleted module persisting forever. `bash-file-access.ts` gained `extractDeletedPathsFromCommand`, which proposes the explicit targets of `rm`, `git rm`, and the source side of `mv` — never a directory sweep or a `git clean`, since those don't name a file to confirm. `ReadGuard` gained `hasKnownPath`/`forgetPath` so the check only fires for a path pi-lens already has a read or write record for (never an unrelated `rm`), and only after confirming the path is actually gone from disk. A confirmed delete now reaches every already-active LSP client for that file's servers via `LSPService.notifyExternalFileChange`, routed through the same #271 debounced queue a burst of deletes still coalesces into one notification per server — this never spawns a server just to tell it about a delete. Bash-created/modified files that were never opened already got their type-1/2 notification through the existing first-open queue path once the dispatch pipeline's LSP runner touches them; that leg needed no new code, only regression coverage. A git-status delta at turn boundaries (to catch changes outside pi-lens' own write/edit/bash surface — external editors, other processes) was judged and scoped out of this PR as a separate, costlier mechanism. Re-opening an already-open document on a bash-authored change (type 2 on reopen) stays out of scope per the issue, pending a measured cost on gopls/rust-analyzer.
|
|
549
|
+
- `clients/bash-file-access.ts` — `extractDeletedPathsFromCommand`.
|
|
550
|
+
- `clients/read-guard.ts` — `ReadGuard.hasKnownPath`, `ReadGuard.forgetPath`.
|
|
551
|
+
- `clients/lsp/client.ts` — `handleNotifyExternalChange`, `notify.watchedFileChange`.
|
|
552
|
+
- `clients/lsp/index.ts` — `LSPService.notifyExternalFileChange`, `notifyExternalFileChange` cross-layer seam.
|
|
553
|
+
- `clients/runtime-tool-result.ts` — bash delete detection wired into the tool_result handler.
|
|
554
|
+
|
|
555
|
+
- **LSP client answers `workspace/diagnostic/refresh` and honors Incremental text sync (refs #1669)** — a server telling us its diagnostics are stale (`workspace/diagnostic/refresh`) previously got `MethodNotFound`; the client now replies `null`, drops every on-disk workspace-diagnostics sweep cache the process has swept under (not just the wrong per-server root marker), clears the same per-document pull state a normal resync already clears for every open document, and proactively re-pulls each open document under pull mode. A `workspace/diagnostic` sweep's `unchanged` report against a resultId basis the client no longer holds now falls back to a real per-file pull instead of silently reporting the file clean. Separately, the client always sent whole-document `didChange` events regardless of the server's negotiated `textDocumentSync.change` kind; an Incremental-only server now receives a single ranged edit spanning its entire previous document (counting CRLF/lone-CR line endings correctly), sent only once the notification is confirmed to have actually reached the transport. `Full`/`None` servers are unaffected.
|
|
556
|
+
|
|
557
|
+
- **Monorepo Cargo and Maven projects no longer spawn one heavy LSP server per crate or module (refs [#1671](https://github.com/apmantza/pi-lens/issues/1671))** — rust-analyzer's existing workspace hoist walked up to an ancestor `Cargo.toml`'s `[workspace]` table without ever checking the session ceiling, so a hit above the session cwd could pick a root outside the project boundary; the walk is now bounded the same way `enforceLspRootCeiling` already bounds crate roots. jdtls had no hoisting at all: every Maven module spawned its own server even inside a declared parent build. It now walks up through a chain of `pom.xml` files, hoisting to a parent only when that parent's `<modules>` actually declares the child as a member — an undeclared sibling directory stays independently rooted, and the walk still respects the session ceiling. `NearestRoot`'s doc comment now says plainly that `excludePatterns` skip a directory and keep walking, rather than aborting resolution.
|
|
558
|
+
|
|
559
|
+
- **Path-attribution follow-ups: bounded orphan degradation, documented id-fallback assumption, pinned reclaim test (refs #1678, #1642, #1648)** — A deferred-format record abandoned by its origin worktree used to log a full raw event on every subsequent `agent_end` forever. The raw, detailed event (file identity, origin, age) now fires only on the RISING edge — the first time a given record is observed as an orphan — and every later repeat of the same record is counted only by `incrementDegradationCount`, so the ledger holds one bounded entry with a running count instead of an ever-growing stream of log lines. `resolveToolCallCorrelationId`'s widest fallback (`event.id`) now documents the assumption it makes — a host that reuses one id per message, not per call, would cross two parallel tool calls in a turn — and notes that real pi's own tool events carry no top-level `id` field at all, making this rung dead code against today's host. Added regression tests pinning the rising-edge gating (exactly one raw event across 3 agent_ends on the same orphan) and the already-correct "mismatch-flush leaves queued, later match-flush reclaims and formats" reclaim path.
|
|
560
|
+
|
|
561
|
+
- **Bound the playground rule verifier's child-process wait (closes #1679)** — `scripts/playground-verify-rule.mjs` spawned its CDP/Chrome helper scripts and waited on the child's `close` event with no timeout. A wedged or daemonized child hung the script forever. It now routes through the shared `safeSpawnAsync` (bounded timeout, tree-kill), matching the rest of the codebase's spawn discipline.
|
|
562
|
+
|
|
563
|
+
- **The secrets and govulncheck gates' disposition filters are now proven, not just present (closes #1694)** — `filterFindingsByDisposition` was applied to both the live and stale arms of the gitleaks/trivy-secrets freshness gate since #1625/#1628, and to govulncheck's combined live+stale array, but none of those three had a regression test proving it. Mutating `trivySecretsStaleFiltered.kept`/`gitleaksStaleFiltered.kept` back to the raw pre-filter `.stale` array, or `govFiltered.kept` back to the raw `[...govGate.live, ...govGate.stale]` array, left the whole suite green. New tests in `tests/clients/runtime-turn-secrets-disposition.test.ts` mark a finding false-positive in each of the three lanes and prove it stops reappearing (the gitleaks/trivy-secrets cases use a stale finding — a scan whose cited file was edited afterward, routed into the 🔑 ACTION NEEDED tier; the govulncheck case mirrors the review round's own mutant), each going red against its own lane's mutant independently. `docs/dispositions.md` now documents the dual-scanner double-mark: gitleaks and trivy anchor the same credential under different `tool`/`rule` identities, so clearing a corroborated finding takes two `lens_diagnostic_mark` calls — expected defense-in-depth, previously undocumented. `trivySecretsToProjectDiagnostics` (no consumer outside its own file) is no longer exported.
|
|
564
|
+
|
|
565
|
+
- **Give `findingPathFreshness` the same mtime-skew tolerance its sibling freshness gates carry (closes #1708)** — `findingPathFreshness` (`clients/advisory-provenance.ts`) compared a finding's cited-file mtime to the scan timestamp with zero tolerance. On Windows, a file's mtime can lead the immediately following `Date.now()` read by up to ~11.4ms, the same host skew `blocker-freshness.ts`'s `MTIME_DRIFT_TOLERANCE_MS` already covers. Without it, a file written and scanned within that window demoted a real secrets STOP blocker to an ACTION NEEDED tier, flaking `runtime-turn-secrets-disposition.test.ts`. `findingPathFreshness` now reuses the same `MTIME_DRIFT_TOLERANCE_MS` constant instead of a second hand-tuned number.
|
|
566
|
+
|
|
567
|
+
- **Give `reconcileProjectDiagnosticsSnapshot` the shared mtime-skew tolerance, not a bare +1ms (closes #1711)** — `reconcileProjectDiagnosticsSnapshot` (`clients/project-diagnostics/cache.ts`) compared a diagnostic's file mtime to the scan timestamp with only a +1ms slack. PR #1710 measured the real write-then-scan skew at up to ~11.4ms on Windows and gave `findingPathFreshness` and `isEntryFresh` the shared `MTIME_DRIFT_TOLERANCE_MS` (50ms) for it, but deferred this consumer. It is the worse sibling: its stale arm DROPS diagnostics outright rather than demoting them, so a same-tick write silently lost a finding from the persisted cache. It now reuses `MTIME_DRIFT_TOLERANCE_MS` from `blocker-freshness.ts`, and the stale-boundary doc comment at `clients/advisory-provenance.ts:264` (and two other spots that had drifted the same way) now cite the tolerance instead of a bare `mtime > scannedAt`.
|
|
568
|
+
|
|
569
|
+
- **Pull-diagnostic timeouts are observable again (refs [#1713](https://github.com/apmantza/pi-lens/issues/1713))** — the per-request `withTimeout` around `textDocument/diagnostic` and `workspace/diagnostic` pulls rejected on timeout, and the rejection skipped every settle emit, so a timed-out pull left no trace beyond a 10-entry `pullFailureHistory` ring buffer. `pullDiagnosticSource` and `clientRequestWorkspaceDiagnostics` now write an `lsp_pull_diagnostic_timeout` latency.log record on every timeout (file, identifier, effective budget, whether a `previousResultId` was echoed, elapsed), and attach a telemetry-only continuation to the abandoned request promise. If the server answers anyway after the caller gave up, that continuation records `lsp_pull_late_answer_discarded` and increments the `lsp-pull-late-answer` degradation-ledger kind (subject = the same file+identifier key the pull path already uses), so a chatty late-answering server's discards are counted, not spammed, and the identity survives aggregation. A late REJECTION is not an "answer" and is not recorded. No behavior change: the timeout path still abandons at budget exactly as before; the instrument only observes. Per-pull overhead is one extra `Date.now()` call (~60ns) on the success path — everything else runs only inside the timeout branch.
|
|
570
|
+
|
|
571
|
+
- **Full-scan sweeps no longer out-run an auxiliary scanner (refs #1714)** — A
|
|
572
|
+
`lens_diagnostics mode=full` sweep used to hand a single-threaded scanner more
|
|
573
|
+
documents than it could read, which stalled ast-grep and forced a kill. Each
|
|
574
|
+
auxiliary now holds a bounded number of unacknowledged `didOpen` notifies; past
|
|
575
|
+
that, the next notify waits for a request round-trip proving the server drained
|
|
576
|
+
its input. A file the throttle holds back is reported as uncovered rather than
|
|
577
|
+
dropped, so it stays in the sweep's coverage gap. Tunable per server class via
|
|
578
|
+
`LSPServerInfo.notifyInflightLimit` (default 8, ast-grep 4) and globally via
|
|
579
|
+
`PI_LENS_LSP_AUX_NOTIFY_INFLIGHT`.
|
|
580
|
+
|
|
581
|
+
- **`navRequest` timeouts are observable again (refs [#1716](https://github.com/apmantza/pi-lens/issues/1716))** — the per-request `withTimeout` around every hover/definition/references/signatureHelp/documentSymbol/workspace-symbol/call-hierarchy request rejected on timeout, and the rejection skipped every settle emit, so a timed-out nav request left no trace at all. `navRequest` is the highest-volume LSP call site (one call per turn, often several), so unlike the `lsp_pull_diagnostic_timeout` fix ([#1713](https://github.com/apmantza/pi-lens/issues/1713)), which logs every timeout, this fix bounds the detailed record to the rising edge — the first timeout, and separately the first late answer, per (method, file) each session — while the degradation-ledger counters (`lsp-nav-request-timeout`, `lsp-nav-late-answer`) still count every occurrence exactly, so a storming server floods neither latency.log nor memory. No behavior change: the timeout path still resolves `undefined` at budget exactly as before; the instrument only observes. Per-request overhead is one extra `Date.now()` call (~125ns) on the success path.
|
|
582
|
+
|
|
583
|
+
- **The ast-grep self-scan now actually runs (closes [#1718](https://github.com/apmantza/pi-lens/issues/1718))** — `scripts/run-astgrep-pi-lens.mjs` hardcoded a single author's now-nonexistent machine paths (`C:/Users/R3LiC/Desktop/pi-lens[-rules2]`) as both the scan target and the rules source, so the dogfooded #1158 defect-shape rules (`no-raw-json-store-write`, `no-win32-isabsolute-for-qualification`, `no-bare-host-path-in-win32-branch`, now tagged `metadata.category: pi-lens-self-scan` in their YAML) never scanned pi-lens's own tree — not in CI, not on any contributor's machine. It printed an error internally but exited 0, so the failure was invisible. The script now derives its repo root from its own file location and reads the rule set from the shipped `rules/ast-grep-rules/.sgconfig.yml`, filtered to the tagged subset (scripts/lib/astgrep-self-scan.mjs). It runs in the `test` CI job (`npm run astgrep:self-scan`, ~3.5s over 1074 files in `clients/` + `tests/` — cheap enough for every PR) and fails on any finding outside the committed `rules/ast-grep-rules/self-scan-baseline.json` baseline, regenerable via `npm run astgrep:self-scan:update-baseline` after triage. Triage on this pass: production code (`clients/`) is clean; `no-raw-json-store-write` fired 200× in `tests/` on throwaway test-fixture JSON writes — not durable production state, so `tests/**` joins the rule's existing `ignores:` (build scripts, the atomic-write seam itself) rather than being silently baselined. The baseline ships empty. `tests/scripts/astgrep-self-scan.test.ts` is the registered-or-fail guard: it asserts a nonzero scanned-file count (an empty result must read as errored, not clean — AGENTS.md defect-shape 10) and runs the real scan against a synthetic violation outside pi-lens's own tree, so a future regression that silently kills the scan again fails there instead of shipping unnoticed a second time.
|
|
584
|
+
|
|
585
|
+
- A cascade's neighbor re-check no longer notifies auxiliary scanners
|
|
586
|
+
(ast-grep, opengrep, typos) for every changed neighbor. A neighbor's
|
|
587
|
+
content did not change — only its import target did — so an auxiliary
|
|
588
|
+
scanner's file-local verdict for it cannot have changed, and the cascade's
|
|
589
|
+
own merge already discarded any aux re-derivation by construction. The
|
|
590
|
+
touch now uses `clientScope: "primary"` (language server only), matching
|
|
591
|
+
the sibling tier-aware touch. Measured on a representative slow-aux
|
|
592
|
+
fixture: 0 aux notifies per neighbor (was 1 per configured auxiliary) and
|
|
593
|
+
a ~850ms per-neighbor conclusive-latency drop (900ms to 50ms) when an aux
|
|
594
|
+
scanner is slow to answer. (#1720)
|
|
595
|
+
|
|
596
|
+
- **Knip now runs the project's own knip (refs #1721)** — When a project
|
|
597
|
+
installs knip, the dead-code scan spawns that copy instead of pi-lens's
|
|
598
|
+
managed one. A pinned-stale managed knip previously reported unused exports
|
|
599
|
+
the project's own `npx knip` did not, and acting on those flags deletes live
|
|
600
|
+
code. A project that installs knip also no longer needs pi-lens's managed
|
|
601
|
+
copy at all. Each scan records the binary, its version, and the config it ran
|
|
602
|
+
with in `~/.pi-lens/sessionstart.log`.
|
|
603
|
+
|
|
604
|
+
- **`loop_block` now names a synchronous block still in progress** — every
|
|
605
|
+
dispatch runner brackets its run with a start/finish marker
|
|
606
|
+
(`clients/latency-logger.ts`), and an event-loop block attributes to
|
|
607
|
+
whichever bracket — still running, or recently closed — overlaps the
|
|
608
|
+
block's own time window the most, instead of only the previous, unrelated
|
|
609
|
+
phase that had already finished (#1723).
|
|
610
|
+
|
|
611
|
+
- **`loop_block` now records every block over the floor, not only the session's biggest (refs [#1723](https://github.com/apmantza/pi-lens/issues/1723))** — `turn_end` gated the `latency.log` write itself on `shouldLogWorstBlock` (a new-session-maximum test), so any block that did not beat the running high-water was silently dropped. In a session where an early large block set the bar, every later block — including the exact ones that could starve an LSP pull (#1549/#1713) — went unrecorded, making the loop-block-vs-pull-timeout correlation undecidable. `turn_end` now gates on a new floor-only `shouldLogLoopBlock` (still ≥60ms) and keeps `shouldLogWorstBlock` only to compute the `worstSoFar` metadata flag and to decide whether to advance the session high-water. Volume stays bounded by call cadence, not by the gate: the check runs at most once per turn (the histogram window resets every turn), so the cap is one `loop_block` record per turn. Attribution also widens from a single `lastPhase` pointer to a bounded `recentPhases` ring (last 3 of a 5-entry cap) via `getRecentLoggedPhases`, because the phase actually causing a synchronous block is often still in flight — and so unlogged — when the block fires, leaving the single last-*completed*-phase pointer aimed at unrelated prior work. No change to block *detection* (`isSuspendSuspectedBlock`, the histogram, or the per-turn window) — only to what gets persisted.
|
|
612
|
+
|
|
613
|
+
- **A forced LSP shutdown could leave the dead pid registered in `instances.json` for the rest of the session (refs #1724)** — `recordLspChild` and `removeLspChild` each did their own unserialized read-modify-write of the registry file. A client-ceiling eviction's forced shutdown (`removeLspChild`) landing at roughly the same moment as the replacement client's spawn (`recordLspChild`) let the later write revert the earlier one: the replacement's write was built from a read taken before the removal landed, so it silently resurrected the just-deregistered pid alongside the new one. Dogfooding caught a 40-minute phantom this way — a force-shut `ast-grep` process stayed listed as alive across several heartbeats until session-end teardown finally cleared the whole entry. `recordLspChild` and `removeLspChild` now share one mutation queue, so a same-process record and remove can never interleave their read-modify-write against each other. `removeLspChild` also takes an optional spawn marker and refuses to drop a pid whose recorded marker no longer matches — defense against the pid being recycled onto an unrelated child in the (now much narrower) window before the deregistering write lands.
|
|
614
|
+
|
|
615
|
+
- **Sweep the remaining author-machine paths baked into scripts/ and package.json (closes [#1728](https://github.com/apmantza/pi-lens/issues/1728))** — the class sweep from #1718 found three more hardcoded `C:/Users/R3LiC/...` literals. `scripts/run-ts-rules-pi-lens.mjs` now derives its scan root from its own file location (`import.meta.url`) instead of a dead absolute path, so it actually scans the checkout it runs from. `scripts/run-all-ts-rules-posthog.mjs` scans an EXTERNAL checkout rather than pi-lens's own tree, so there is no repo-relative default to derive; it now takes the target via `--posthog-dir`/`POSTHOG_DIR` and exits with a usage message instead of silently no-oping when unset. `package.json`'s `harness:*-poc` scripts dropped their hardcoded `--pi-bin "C:\Users\R3LiC\..."` flag entirely — `scripts/run-harness.mjs`'s own `$APPDATA`-derived PATH resolution already finds `pi.cmd`/`pi` for any contributor, so the literal was both wrong for everyone else and redundant. `tests/scripts/no-hardcoded-machine-paths.test.ts` is the registered-or-fail guard: it greps every script under `scripts/` plus `package.json` for a `C:\Users\<name>` / `C:/Users/<name>` literal, with a reviewed allowlist (currently one entry, tracking `scripts/run-astgrep-pi-lens.mjs` until #1718's own fix in PR #1729 lands) that must itself still contain the literal it excuses or the test fails.
|
|
616
|
+
|
|
617
|
+
- **Managed tools now refresh instead of drifting stale (refs #1730)** —
|
|
618
|
+
pi-lens installed each managed npm tool once and never re-resolved it, so the
|
|
619
|
+
copy it ran could be many minor versions behind what its own recorded range
|
|
620
|
+
permits. A managed knip 28 minors behind reported 62 unused exports on a tree
|
|
621
|
+
the project's own knip reported clean, and acting on flags like those deletes
|
|
622
|
+
live code. pi-lens now re-resolves one managed tool per session, at most once
|
|
623
|
+
a week per tool, on a background timer that never blocks startup. Every
|
|
624
|
+
refresh records the version it moved from and to in
|
|
625
|
+
`~/.pi-lens/sessionstart.log`, and verifies the updated binary still runs
|
|
626
|
+
before trusting it. A refresh that fails records one degradation and clears
|
|
627
|
+
the cached tool path so the next probe re-checks the tree, rather than
|
|
628
|
+
blocking the tool. Set `PI_LENS_DISABLE_TOOL_REFRESH=1` to turn it off.
|
|
629
|
+
|
|
630
|
+
- **taplo, biome, stylua, and vulture now prefer the project's own binary and config over pi-lens's managed copy (refs #1731)** — The #1721 sweep found
|
|
631
|
+
the same defect shape in six more runners. taplo's project-local check was
|
|
632
|
+
unreachable whenever pi-lens's managed shim answered first, and even its
|
|
633
|
+
fallback missed an npm-installed taplo on Windows (wrong extension). biome's
|
|
634
|
+
client path returned the first session's auto-installed binary for every
|
|
635
|
+
later project, no matter what that project shipped. stylua had no
|
|
636
|
+
project-local check at all — PATH only. vulture never looked at the
|
|
637
|
+
project's own `.venv`. jscpd and vulture also always passed
|
|
638
|
+
`--min-lines`/`--min-tokens`/`--exclude` (jscpd) and
|
|
639
|
+
`--min-confidence`/`--exclude` (vulture), silently overriding a project's
|
|
640
|
+
own `.jscpd.json`/`[tool.vulture]` thresholds. biome's `--config-path` did
|
|
641
|
+
the same to a project's own `biome.json`, blocking its nested-config
|
|
642
|
+
resolution in monorepos. sqlfluff now spawns with the file's own `cwd`
|
|
643
|
+
instead of the extension host's, so it resolves `.sqlfluff` against the
|
|
644
|
+
right project.
|
|
645
|
+
|
|
646
|
+
- **Runner exit codes now gate clean vs. errored (refs #1736)** — Knip, jscpd,
|
|
647
|
+
and vulture no longer read an empty result on a nonzero exit as "clean."
|
|
648
|
+
A broken shim, a crash, or a config-load error now reports as errored and
|
|
649
|
+
records a bounded degradation entry naming the runner and its exit status,
|
|
650
|
+
instead of silently showing zero issues.
|
|
651
|
+
|
|
652
|
+
- **Managed tools from GitHub releases, pip, gem, archives and Maven now refresh too (refs #1747)** —
|
|
653
|
+
#1730 unfroze the npm-installed tools. The other 39 managed tools stayed on
|
|
654
|
+
whatever version pi-lens resolved the day it first installed them, because
|
|
655
|
+
the installer only runs when a tool is absent. A stale ast-grep, ruff,
|
|
656
|
+
gitleaks or terraform-ls produces exactly the wrong verdicts #1730
|
|
657
|
+
documented for knip. All five remaining strategies now share the same
|
|
658
|
+
weekly per-tool cadence, the same one-refresh-per-session budget and the
|
|
659
|
+
same degradation record as the npm path. GitHub tools re-resolve
|
|
660
|
+
`releases/latest` with a stored `ETag`, so an unchanged release downloads
|
|
661
|
+
nothing; archive and Maven tools compare the version pinned in pi-lens's
|
|
662
|
+
own registry and touch the network only when it moved; pip and gem tools
|
|
663
|
+
re-run their install command in upgrade form. A refreshed binary that
|
|
664
|
+
cannot report a version is treated as a failed refresh, not a success. The
|
|
665
|
+
refresh honors the same install kill-switch, project-trust gate and install
|
|
666
|
+
lock as a normal install. On an EXISTING machine, GitHub tools have no
|
|
667
|
+
recorded release tag yet, so each one's first refresh downloads its current
|
|
668
|
+
release once before the ETag comparison starts paying off — roughly 27
|
|
669
|
+
sessions of one-time downloads at the one-per-session budget, spread over
|
|
670
|
+
weeks, not a burst. Set `PI_LENS_DISABLE_TOOL_REFRESH=1` to turn the whole
|
|
671
|
+
mechanism off.
|
|
672
|
+
|
|
673
|
+
- **ruff `S` security rules now run by default; the IaC misconfig lane now actually scans (refs #1757)** —
|
|
674
|
+
the bundled `config/ruff/core.toml` enables `S` (Bandit-equivalent), with
|
|
675
|
+
the sub-rules that over-fired on real code (`S101`, `S311`, `S603`,
|
|
676
|
+
`S607`) excluded and the exclusion reasons recorded in the config; a
|
|
677
|
+
project-local ruff config still overrides the bundled one unchanged.
|
|
678
|
+
Separately, two `trivy config` call sites — the per-edit IaC lane
|
|
679
|
+
(`clients/dispatch/runners/trivy-config.ts`) and the Helm
|
|
680
|
+
rendered-manifest pass (`clients/dispatch/runners/helm-render.ts`) — were
|
|
681
|
+
passing `--no-progress`, a flag that subcommand rejects; every real
|
|
682
|
+
invocation exited 1 with trivy's usage text on stdout instead of scanning
|
|
683
|
+
anything. `trivy-config.ts`'s empty-output-only guard missed this
|
|
684
|
+
entirely and reported a clean scan; `helm-render.ts` already checked exit
|
|
685
|
+
status unconditionally, so it failed loud instead, but still never
|
|
686
|
+
scanned a single rendered manifest. Both call sites are fixed, the
|
|
687
|
+
trivy-config runner now treats any nonzero exit as an error regardless of
|
|
688
|
+
stdout content (with a bounded degradation-ledger record so this can't go
|
|
689
|
+
unnoticed again), and the lane now also covers CloudFormation templates
|
|
690
|
+
(yaml and json).
|
|
691
|
+
|
|
692
|
+
- **Tree-sitter grammar wasms now refresh when the pinned version moves (refs #1760)**
|
|
693
|
+
— a grammar wasm downloaded once was never replaced when this repo
|
|
694
|
+
bumped the version it pins, because the cached file's name carries no
|
|
695
|
+
version. A future fix to a broken grammar build (the class #255 and #427
|
|
696
|
+
already hit) would never reach a machine that had already downloaded the
|
|
697
|
+
broken one. pi-lens now compares each cached grammar's sha256 against the
|
|
698
|
+
currently pinned manifest (`scripts/grammars.lock.json`) before trusting it,
|
|
699
|
+
memoized per file so steady state costs no extra hashing and never touches
|
|
700
|
+
the network. A mismatch is treated as a missing file: the existing lazy
|
|
701
|
+
fetch re-downloads it, verifying and atomically swapping in the new build,
|
|
702
|
+
so a failed re-download never destroys the working cached copy. This also
|
|
703
|
+
catches on-disk corruption, which nothing detected before.
|
|
704
|
+
|
|
705
|
+
- **LSP resync timeout no longer blames a server still spawning (closes #1766)** — When the pre-dispatch resync's wait budget expired while a language server's first spawn was still in progress, the log wrongly read "server slow/wedged" — a verdict about a running server, not a cold start. The record now emits `reason: "spawn-in-flight"` and says the server is still cold-spawning when that is the case, and keeps the "slow/wedged" wording only when no spawn is in flight.
|
|
706
|
+
|
|
707
|
+
- **`runtime-session.test.ts` no longer flakes under CI contention (closes #1767)** — root-caused, not just re-budgeted. Every test in this file drives `handleSessionStart`, which does real synchronous fs work plus fire-and-forget background scans (`scheduleStartupScans`) that reach their mocks only after a real `setImmediate` tick and real per-file fs reads. Solo runs measured 8-13s for all 14 tests (well inside `vi.waitFor`'s 1000ms default and vitest's 5000ms `testTimeout` default), but 24 contended runs (this file run 6x in parallel) failed 8 times (33%) — some on individual `vi.waitFor` calls timing out, others on the whole test blowing the 5000ms default while `handleSessionStart` itself ran long. Confirmed deterministically too: `--testTimeout=300` failed 12 of 12 pre-fix runs, 0 of 14 post-fix. This confirms the reporter's own A/B finding that `clients/runtime-session.ts` is not the cause. Every `vi.waitFor` call in the file now uses an explicit 30s budget (`HEAVY_IO_TIMEOUT_MS`, matching `tests/clients/ast-grep-rule-precedence-followups.test.ts`'s existing convention), and the enclosing `describe`/`it` blocks use a 45s budget (`TEST_BUDGET_MS`) — kept deliberately larger so a genuine failure still surfaces `vi.waitFor`'s specific message instead of racing it against the outer test-timeout's generic one. Post-fix, 18 of 19 completed contended runs passed with zero genuine test failures (the 19th run hit an unrelated `npx` infrastructure error, not a test assertion). Test-only change; `clients/runtime-session.ts` is untouched.
|
|
708
|
+
|
|
709
|
+
- **Genuine pull timeouts now count in the degradation ledger (#1771)** — `lsp_pull_diagnostic_timeout` previously wrote a detailed latency.log record but tallied nothing, so a storming server was invisible in aggregate. It now increments `lsp-pull-diagnostic-timeout` with a subject that preserves server and file identity.
|
|
710
|
+
|
|
711
|
+
- **`index-loop-block-wiring` case (a) no longer times out flakily under load (closes #1772)** — pre-existing test-budget starvation, not a regression. Case (a) does a cold `await import("../index.js")` after `vi.resetModules()`; cases (b) and (c) also reset modules but reuse the already-warm transform cache from case (a)'s run, so only case (a) pays the full cost. That cost measured up to 9.2s solo on a busy box, already over vitest's 5000ms default (timeout, not an assertion failure). Two independent controlled A/B comparisons (#1743 builder, #1761 reviewer) showed `origin/master` fails at the same or higher rate as feature branches, and CI stays green on both heads, confirming this is environmental, not a code defect. The describe block now declares an explicit 30s timeout, matching the `HEAVY_IO_TIMEOUT_MS` convention (`tests/clients/ast-grep-rule-precedence-followups.test.ts`) and #1764's `instance-reaper-prune-concurrency` fix, instead of hoisting the import: the per-test isolation actually comes from the `beforeEach` mock clears, not the re-import, but hoisting would still order-couple case (c)'s `worstSoFar` assertions to whatever high-water state case (b) leaves behind, so the import stays per-test. A repo-wide sweep for the same shape (cold `import("../index.js")` + `resetModules`, no timeout budget) found four untouched siblings, one reproduced red in the same session — filed as #1778.
|
|
712
|
+
|
|
713
|
+
- **Diagnostic pulls no longer dispatch once the caller's budget is exhausted (#1773)** — `textDocument/diagnostic` and `workspace/diagnostic` pulls used to send an unwinnable 1ms request when the remaining budget was 0 or negative, then log it as a genuine timeout. Below a 5ms usable floor the pull is skipped outright and recorded as `lsp_pull_skipped_budget_exhausted`, so `lsp_pull_diagnostic_timeout` only ever means a pull was really attempted.
|
|
714
|
+
|
|
715
|
+
- **A pull timeout's abandoned request now traces server rejections, not just late answers (#1774)** — when an abandoned pull rejects (for example a permanent server error after the caller gave up) instead of answering or staying silent, it now emits a bounded `lsp_pull_late_rejection` record with the error code and elapsed time. The rejection is still swallowed; only the observability changes.
|
|
716
|
+
|
|
717
|
+
- **Two more `index-*-wiring` test files budget their cold import (closes #1778)** — `tests/index-memory-sample-wiring.test.ts` and `tests/index-smells-rollup-wiring.test.ts` share #1772's flake shape: a cold `await import("../index.js")` after `vi.resetModules()` per test, racing vitest's 5000ms default `testTimeout`. Both describe blocks now declare an explicit 30s timeout, matching #1779's `index-loop-block-wiring` fix and this repo's `HEAVY_IO_TIMEOUT_MS` convention, instead of hoisting the import — hoisting would defeat the per-test module-state isolation these wiring guards check. `tests/index-integration.test.ts` and `tests/index-lsp-idle-reset.test.ts` (the other two files in #1778's sweep) already carry an equivalent per-test 45s timeout and needed no change.
|
|
718
|
+
|
|
719
|
+
- **`session_start` self-heals cachedExports after a starved sequence read (closes #1785)** — under heavy load, `handleSessionStart`'s bounded change-log sequence read (#1162's 250ms budget) can time out even when the underlying log is empty, purely from CPU scheduling starvation. The freshness gate correctly refuses to trust a snapshot it can't validate yet, so `cachedExports`/`projectRulesScan` stay empty — but the deferred read that continues in the background only reseeded `runtime.projectSeq`, never re-checked snapshot freshness, so a project snapshot that really was current stayed unhydrated for the rest of the session even after the real answer arrived. Quick mode (the interactive hot path) now retroactively hydrates from the snapshot once the deferred read confirms it was fresh — a deterministic completion signal, not a sleep. Full mode is deliberately left alone: its cold path actively rescans instead of accepting an empty state, so retroactive hydration there risks overwriting fresher, actively-computed data. A `snapshot-sequence-read-timeout` degradation-ledger record now carries the project root and the on-disk snapshot's age/size whenever the budget is blown, so a real recurrence is diagnosable from the ledger alone. Characterization also corrects the issue's original hypothesis: the suspected `saveProjectSnapshot` worker-persist race does not reproduce — the in-process authoritative-snapshot cache (`clients/project-snapshot.ts`) already guarantees read-your-own-write consistency within a process.
|
|
720
|
+
|
|
721
|
+
Review round found the first version of this fix could itself be defeated by quick mode's own cold-start warmup (2s later by default): the warmup saves a snapshot built from the still-cold live runtime, and a re-load at deferred-hydration time could read that emptied snapshot instead of the good one. The retroactive-hydration closure now captures the on-disk snapshot BEFORE the warmup timer is even armed and hydrates only from that captured value, immune to anything the warmup writes afterward. The hydration itself is now additive per field (`hydrateRuntimeFromProjectSnapshotIfIdle`): it never nulls a live `wordIndex` from a snapshot missing one, and never clears a live `cachedExports`/`projectRulesScan` — each field only hydrates from the snapshot while its own runtime copy is still idle.
|
|
722
|
+
|
|
723
|
+
A second review round found that capture itself was too expensive to run unconditionally on the interactive hot path: it called the full `loadProjectSnapshot` (gunzip + parse the entire body, dominated by `wordIndex`'s postings graph) on EVERY quick-mode session start, bypassing the #947 skip-stale meta gate and briefly holding a second copy of the postings graph in memory (#1370). A new narrow loader, `loadProjectSnapshotExportsAndRules`, excises the heavy fields (`wordIndex`, `files`, `symbols`, `reverseDeps`) from the raw JSON text before `JSON.parse` ever sees them, instead of parsing everything and discarding the unwanted parts afterward.
|
|
724
|
+
|
|
725
|
+
A third review round replaced even that narrow capture: it was still 210-289ms of synchronous read+gunzip+scan on every quick-mode session start, to guard a timeout that rarely fires. Quick mode's own cold-start warmup already loads the on-disk snapshot for its own purposes (reusing a cached `startupScan` verdict), strictly before any save the warmup itself might make — so the fix now has the warmup publish that already-loaded read (narrowed to exports+rules) for the retroactive-hydration path to reuse, at zero marginal cost, instead of paying for a second, separate read. This also closed a leftover retention bug the narrow loader's own test hook had reintroduced: an earlier observability hook held the full narrowed body text at module scope, unconditionally — the exact retention class the narrow loader exists to avoid. It's now a bounded `{length, containsHeavyKey}` digest instead.
|
|
726
|
+
|
|
727
|
+
- **`workspace/symbol` no longer goes to LSP servers that never advertised it (refs #1789)** — without a `filePath`, `LSPService.workspaceSymbol` picked "the first active client" from the full spawned-client map, auxiliary scanners (ast-grep, opengrep, zizmor, ...) included, with no capability check of its own; a workspace whose only spawned client was an auxiliary sent every workspace-wide symbol query to it and ate a wasted round trip each time (2026-08-20 plegma dogfood: ast-grep). The gate now reads `client.getOperationSupport().workspaceSymbol` — the same source `lsp-document-symbols.ts`'s documentSymbol gate already reads, populated from the initialize result's `capabilities` — right at the point that resolves and calls the target client, for both the `filePath` and no-`filePath` branches. Caller-visible shape is unchanged: an unsupported server returned `[]` before (from a thrown/absorbed error) and returns `[]` now (from the short-circuit). A repo-wide sweep of every `navRequest`/`safeSendRequest` call site in `clients/lsp/client.ts` found one sibling with the same "chokepoint, not caller, decides the target and never checks" shape — `LSPService.incomingCalls`/`outgoingCalls` resolve their target server from the call-hierarchy item's own file, which can cross servers in a polyglot workspace — left open as a follow-up rather than folded into this contained fix.
|
|
728
|
+
|
|
729
|
+
- **`blocker_freshness_sweep`'s population excludes cache-served diagnostic rows (closes [#1790](https://github.com/apmantza/pi-lens/issues/1790))** — the turn-end freshness gate (`clients/blocker-freshness.ts`, `sweepInlineBlockerFreshness`) built its population solely from `RuntimeCoordinator`'s inline-blocker map, a live-dispatch-only store. A workspace-diagnostics cache hit never touches that map — it writes straight into `widget-state.ts`'s `files` store (`reconcileScanDiagnostics`, called from the cache-serve branch of `tools/lsp-diagnostics.ts`). During the 2026-08-20 dogfood `blocker_freshness_sweep` logged `total:1 kept:1` while five stale cache-served blocking rows still rendered in the widget — the sweep's population never saw them, so its drift check never ran over them either. `sweepInlineBlockerFreshness` now accepts `options.additionalEntries`: widget-store rows fed in by `runtime-turn.ts` from the new `widget-state.ts` export `getWidgetBlockingFilesForSweep` (one entry per file with a currently-blocking, LSP-sourced diagnostic, baselined on the earliest `observedAt`), deduped by file path against the inline-blocker map so a file present in both stores is drift-checked once, not twice. Review round 1 found the dedup DROPPING the widget row on a duplicate path instead of also demoting it: a live dispatch writes both stores for the same verdict (`runtime-tool-result.ts`'s inline write, `pipeline.ts`'s widget write), and `markInlineBlockerStale` only ever touches `RuntimeCoordinator`'s map — so the widget's own `isBlocking` for the file stayed true even after the inline entry demoted, the exact ghost this issue exists to kill, just moved one store over. A duplicated path now CHAINS the widget demote onto the inline entry's demote instead: one drift check, both stores written. Review round 2 found the chain itself still ghost-prone: it fired even when the inline entry it chained onto was INELIGIBLE for the drift check (already latched `stale` from a prior turn's one-way demotion, an unstamped legacy record, or a non-LSP/mixed-sources entry like an unrelated ast-grep finding on the same file) — the sweep's own gate short-circuits before ever calling `demote()` on those, so the chained widget demote silently never fired, either as a permanent forever-ghost (the stale latch never re-arms) or reappearing the instant an unrelated inline blocker happened to share the file. A duplicate now only chains when the inline entry is itself eligible (`isEligibleForDriftCheck`, the same three gates factored out so this decision can't drift from the real ones); an ineligible duplicate gives the widget row its own separate population entry instead, so its own eligibility and its own baseline decide its own fate. The dedup key is `normalizeEphemeralMapKey` (cheap, no syscalls), not `normalizeMapKey` (measured ~313µs per deleted path via its `realpathSync`/walk-up fallback — a real cost on a turn-end hot path, and the wrong key besides: the widget store this population is deduped against keys on `normalizeEphemeralMapKey` itself). A widget-origin row that drifts is demoted through the new `markWidgetFileBlockersStale` — the same store and the same demote-not-drop write `reconcileStaleWidgetDependencyBlockers` already performs, just triggered by the sweep's own drift check instead of an explicit `lens_diagnostics mode=all` call; a comment at that write site pins the single-file-baseline assumption it currently relies on (safe today because every LSP-diagnostics writer replaces a file's errors wholesale with one `observedAt` per write) so a future mixed-stamp merge writer can't make it over-demote silently. The source defect — the cache serving genuinely stale rows in the first place — was fixed separately in [#1786](https://github.com/apmantza/pi-lens/pull/1786) (cache expiry + clean-answer eviction, closes [#1782](https://github.com/apmantza/pi-lens/issues/1782)); this is the defense-in-depth remainder, so a future cache-path defect cannot render invisible to the sweep again. Bounded cost: one extra pass over `widget-state.ts`'s per-turn file count (already bounded by `MAX_INACTIVE_FILE_RECORDS`), filtered to files with a live blocking diagnostic; the dedup key itself is now a pure string operation with no stat/realpath cost, and the per-file drift check is still bounded by the existing `MAX_DRIFT_CHECK_IMPORTS` cap and per-turn import-resolution memo.
|
|
730
|
+
|
|
731
|
+
- **Clean workspace-diagnostics cache entries no longer replay across a dependency edit on a cold session** — A clean entry recorded while a
|
|
732
|
+
reverse-dependency index was available now refuses to serve on a later
|
|
733
|
+
session that has none, instead of falling back to mtime-only checking. The
|
|
734
|
+
cache used to trust any clean entry's own mtime alone whenever this
|
|
735
|
+
session's dependency graph wasn't built yet, so a dependency change (or a
|
|
736
|
+
config change that flipped a file from clean to failing) went unnoticed for
|
|
737
|
+
as long as the file's own bytes stayed untouched. Refused entries count
|
|
738
|
+
toward the existing `lsp_workspace_diagnostics_cache_expiry` record in
|
|
739
|
+
`latency.log` (`depIndexColdRefusals`). An entry recorded on an equally cold
|
|
740
|
+
session keeps today's mtime-only behavior — it never claimed more than that
|
|
741
|
+
check can verify.
|
|
742
|
+
|
|
743
|
+
- **`lens_diagnostics` no longer double-counts error findings (closes #1799)** — The compact
|
|
744
|
+
header and the mode=all/full summary printed the same error-severity findings
|
|
745
|
+
twice, once as "blocking" and again as "errors", making three real problems
|
|
746
|
+
read as six. `semantic === "blocking"` and `severity === "error"` always
|
|
747
|
+
describe the same set of findings, so both surfaces now report blocking and
|
|
748
|
+
warnings only. Also removed the always-zero `byTier.error` field from
|
|
749
|
+
`clients/actionable-warnings.ts`, since error-severity diagnostics never
|
|
750
|
+
reach the actionable-warnings path.
|
|
751
|
+
|
|
752
|
+
- **`callHierarchy/incomingCalls` and `outgoingCalls` no longer go to servers that never advertised `callHierarchyProvider` (closes #1803)** — `LSPService.incomingCalls`/`outgoingCalls` resolved their target server from the call-hierarchy item's own file and called straight through, with no check of `getOperationSupport().callHierarchy`. Both now read `client.getOperationSupport().callHierarchy` — the same source `detectOperationSupport` populates from the initialize result's `callHierarchyProvider` (clients/lsp/client.ts:5253) — right before dispatch, mirroring the #1789 gate on `workspaceSymbol`. Because `lsp_navigation`'s shared capability pre-check only runs for path-based operations (`needsFilePath`, tools/lsp-navigation.ts:1099-1104 excludes call-hierarchy traversal), the service-only fix would have silently turned a live, user-visible unsupported-server error into a false "no callers found" clean empty — the review round caught this and added a matching pre-check in the tool layer's `incomingCalls`/`outgoingCalls` cases, so an unsupported server now reports the same discriminated `emptyReason: "unsupported"` shape every other capability-gated operation already uses, while a supporting server with zero callers still reports a clean empty. A pattern sweep of every `getClientForFile`-resolved navigation method in `clients/lsp/index.ts` found eleven more siblings with the same service-layer gap (`definition`, `typeDefinition`, `declaration`, `references`, `hover`, `signatureHelp`, `documentSymbol`, `codeAction`, `rename`, `implementation`, `prepareCallHierarchy`), all already gated at the `lsp_navigation` tool layer — filed as #1826 as defense-in-depth, not a live bug.
|
|
753
|
+
|
|
754
|
+
- **Biome findings can now reach actionable warnings** — biome's real
|
|
755
|
+
`lint --reporter=json` output (probed live against the shipped 2.5.x
|
|
756
|
+
binary) carries no `tags` field and never did, so the old
|
|
757
|
+
`d.tags?.includes("fixable")` check always missed and every biome
|
|
758
|
+
diagnostic's `fixable`/`autoFixAvailable`/`fixKind` were permanently
|
|
759
|
+
false. The parser now resolves each rule's real fix tier via
|
|
760
|
+
`biome explain <rule>` (the one place biome states fixability as
|
|
761
|
+
structured text, sourced from the running binary's own rule registry),
|
|
762
|
+
cached for the process lifetime since fix tier is a property of the rule,
|
|
763
|
+
not the occurrence. Also aligned the diagnostic location field to biome's
|
|
764
|
+
real `location.path` (the parser and its fixtures assumed
|
|
765
|
+
`location.source`, a field that never shipped) (#1810).
|
|
766
|
+
|
|
767
|
+
- **`workspaceSymbol`'s no-`filePath` branch now picks a client that actually supports it (refs #1812)** — without a `filePath`, `LSPService.workspaceSymbol` stopped at `state.clients`' first entry by insertion order, even after the #1789 capability gate. In a polyglot workspace where an auxiliary scanner (ast-grep, opengrep, zizmor, ...) spawned before a supporting primary server, every workspace-wide symbol query silently returned `[]` with zero requests, and the supporting primary never got a look-in. A new `selectWorkspaceScopeClient` helper scans the spawned-client map for the first LIVE client matching a predicate, preferring a primary (non-`"auxiliary"`) server over an auxiliary one — mirroring the `role !== "auxiliary"` preference `getClientForFile` already applies for `filePath`-scoped requests, and the `isAlive()` filter `getCapabilitySnapshots`'s own no-`filePath` branch already applies. Only when NO live spawned client supports the query does it fall back to `[]`. Swept and fixed the same "clients[0] by insertion order" shape at the other four no-`filePath` chokepoints in `clients/lsp/index.ts` — `getAdvertisedCommands`, `executeCommand`, `getOperationSupport`, and `getWorkspaceDiagnosticsSupport` — onto the same helper; `getCapabilitySnapshots`'s no-`filePath` branch was already correct (it iterates every client, not just the first). A follow-up review round found the multi-primary case where `getOperationSupport`'s own no-`filePath` probe (no predicate) can disagree with `workspaceSymbol`'s predicate-driven selection — filed as #1846, not fixed here.
|
|
768
|
+
|
|
769
|
+
- **A warm session's dependency index no longer vacuously clears files it never scanned** — `isEntryFresh`'s per-file dependency check used to treat
|
|
770
|
+
"this file is absent from the reverse-deps index" the same as "the index
|
|
771
|
+
confirmed this file has zero imports": both returned an empty import list,
|
|
772
|
+
so the freshness loop iterated zero times and reported clean. A session
|
|
773
|
+
with a real index covering the rest of the project could still fail-open,
|
|
774
|
+
with no signal at all, for the one file outside that index's own coverage.
|
|
775
|
+
`getImports` now returns `undefined` for an uncovered file instead of
|
|
776
|
+
silently coercing it to `[]`, and a clean entry stamped with dependency
|
|
777
|
+
knowledge is refused and evicted for that file the same way #1793 already
|
|
778
|
+
refuses one on a session with no index at all. Refusals from both cases
|
|
779
|
+
share the existing `depIndexColdRefusals` counter in `latency.log`. The
|
|
780
|
+
reverse-deps index itself carried the same masking one layer down —
|
|
781
|
+
`clients/reverse-deps.ts` used to stamp an empty import list for a file it
|
|
782
|
+
never scanned (known only as another file's import target, or recorded in
|
|
783
|
+
the snapshot with no imports field at all); it now leaves that file's
|
|
784
|
+
entry absent, matching the distinction `getImports` relies on.
|
|
785
|
+
|
|
786
|
+
- **Actionable-warning and code-quality-warning ids now canonicalize the file path like disposition anchors already did** —
|
|
787
|
+
`createActionableWarningId` and `createCodeQualityWarningId` hashed a RAW
|
|
788
|
+
`relativeFile`, so a drive-letter-case or slash-variant file path produced
|
|
789
|
+
a different id than `diagnostic-dispositions.ts` would compute for the
|
|
790
|
+
same file (the #533 orphaned-record class, swept from 1 of 3 copies to
|
|
791
|
+
all 3). Both now share `clients/finding-identity.ts`'s canonicalizing
|
|
792
|
+
`relativeFile` and 12-char hash length; a warning already suppressed in
|
|
793
|
+
`actionable-warning-state.json` under the pre-fix, 10-char id is still
|
|
794
|
+
honored and migrated forward onto the new id (#1816).
|
|
795
|
+
|
|
796
|
+
- **Session-state conformance coverage catches a new latch landing inside an already-registered file (refs [#1817](https://github.com/apmantza/pi-lens/issues/1817))** — `tests/clients/session-state-conformance.test.ts`'s coverage sweep audited FILES: once a module was registered or exempted, any NEW module-level `Map`/`Set`/`PathKeyedMap` added inside it was invisible, because the sweep never re-checked what was already inside a file it had already cleared. `#1801`'s review round 1 found this the hard way — `staleGrammarVersionAt`, a process-lifetime latch on the `TreeSitterClient` singleton, sat unregistered and unreset for a whole review round while the sweep stayed 55/55 green. `tests/support/sweep-kit.ts` gains `auditSymbolCounts()`, built on the existing `auditRegistry()` machinery: each flagged file's LIVE detected-symbol count is folded into its registry id (`file@N`), so a file whose count changes presents an id the pin does not name and reds as an ordinary unaccounted item — no parallel registry, no new exemption semantics. `tests/support/session-state-registry.ts` pins the current count for all 72 files the sweep flags today (`SESSION_STATE_SYMBOL_COUNTS`), and the conformance suite diffs it against a live scan on every run. Proven red-first by seeding an uncleared module-level `Map` into the already-registered `tree-sitter-shared.ts` (the `#1801` F1 shape exactly): the new pin test failed with `tree-sitter-shared.ts@1` unaccounted; reverting the seed restored green. Chose the cheaper of the issue's two options (a pinned per-file count) over full symbol-to-reset attribution — the registry's `state` field is free text that does not reliably map onto the scanner's detected identifier names across its ~30 entries, so attributing every symbol to a reset or a reason would be a real migration, not a rider on this fix.
|
|
797
|
+
|
|
798
|
+
- **ast-grep-napi LSP supersede gate B tests now pin the gate, not the fallback (closes #1830)** — the two "skips ..." tests under `LSP supersede gate (#239 Phase 2)` mocked `@ast-grep/napi` with a bare `parse: vi.fn()`, so a neutered gate B still returned `"skipped"` by falling through to the load/parse skip path. `expect(result.status).toBe("skipped")` couldn't tell the two skip reasons apart, so the tests passed whether gate B fired or not. A `mockWorkingSgLoad()` helper now makes the NAPI fallback path succeed, so only gate B can still produce `"skipped"`. Neutering the gate's `.includes("ast-grep")` check now turns both tests red with `"succeeded"`, confirming the assertion is load-bearing.
|
|
799
|
+
|
|
800
|
+
- **`require-safety-comment-for-as-unknown-as`'s `SAFETY:` valve no longer exempts every later cast in the same block (closes #1834)** — the backward comment scan now stops at the first non-comment sibling instead of walking unbounded to the top of the block.
|
|
801
|
+
|
|
802
|
+
- **Workspace-wide capability checks now describe every live client, not just the first (closes #1846)** — `LSPService.getOperationSupport()` called without a `filePath` answers for the workspace, but it returned a single client's capabilities: whichever `selectWorkspaceScopeClient()` picked with no predicate. In a multi-primary workspace — a `json` server spawned before `typescript`, for example — the first client's missing `workspaceSymbolProvider` reported the operation unsupported even though the later client advertised it. `tools/lsp-navigation.ts`'s workspaceSymbol gate read that snapshot and threw `__UNSUPPORTED__`, so `LSPService.workspaceSymbol` — which #1812 taught to find the supporting client — was never reached, and the probe disagreed with the call it was gating. Each capability now resolves through `selectWorkspaceScopeClient` with a per-capability predicate, so the snapshot is ORed across exactly the clients that routing would consider. Aggregation inherits that helper's `isAlive()` filter rather than re-deriving one: a dead client cannot contribute a capability nobody can answer. The `filePath`-scoped branch is unchanged, a single-client workspace gets the same snapshot as before, and no active client still returns `null`.
|
|
803
|
+
|
|
804
|
+
- **`require-safety-comment-for-as-unknown-as`'s `SAFETY:` valve now accepts a comment above an exported, class-field, or enum-member cast (closes #1847)** — the rule's `inside:` kind list was missing `export_statement`, `public_field_definition`, and `enum_assignment`, so a documented `export const`, class-field, or enum-member cast had no way to satisfy the ERROR-tier blocking rule.
|
|
805
|
+
|
|
806
|
+
- **Resource sampling distinguishes failed process-table queries from empty samples (closes #1863)** — failed or timed-out Windows and POSIX resource queries now remain unknown, with bounded query-specific degradation telemetry, instead of being treated as clean zero-sample results.
|
|
807
|
+
|
|
808
|
+
- **Ignore zero-byte freshness reads (refs #1865)** — Keep live diagnostics visible during truncate-then-write windows and record the observed file size for past-EOF demotions.
|
|
809
|
+
|
|
810
|
+
- **Reuse knip results for unchanged project content** — Turn-end and startup
|
|
811
|
+
scans now reuse the last successful knip result when the runtime project
|
|
812
|
+
sequence has not changed. Telemetry distinguishes cached results from
|
|
813
|
+
executed scans, and a new session re-arms execution.
|
|
814
|
+
|
|
815
|
+
- **Identify TypeScript diagnostic sequence records** — Every
|
|
816
|
+
`lsp_typescript_diagnostic_sequence` row now names its server and whether it
|
|
817
|
+
records a publication or a settled answer, instead of leaving the minimum
|
|
818
|
+
outcome identity implicit.
|
|
819
|
+
|
|
820
|
+
- **Accept SAFETY comments on object-literal members (refs #1870)** — `require-safety-comment-for-as-unknown-as` recognizes the TypeScript `pair` node while keeping unrelated sibling comments scoped to their own member.
|
|
821
|
+
|
|
822
|
+
- **javac fallback skips classpath-less compiles inside Maven/Gradle projects (closes #1877)** — when the Java LSP runner misses its wait budget, dispatch falls back to the `javac` runner, which compiles the single file with no `-classpath`/`-sourcepath`. On build-tool projects every non-JDK import becomes a blocking `package does not exist` false positive that gates agent edits and stays cached in session diagnostics after jdtls recovers. The runner now walks up from the edited file with the shared `hasJavaBuildDescriptor` seam (the same gate SpotBugs uses) and returns `skipped` when a `pom.xml`, `build.gradle(.kts)`, or `settings.gradle(.kts)` is present; the dispatcher's coverage notice reports the LSP gap honestly. Standalone `.java` files with no descriptor keep the javac fallback.
|
|
823
|
+
|
|
824
|
+
- **Tally verified relative-path attribution guesses instead of logging one record per read (refs [#1884](https://github.com/apmantza/pi-lens/issues/1884))** — existing workspace-root guesses now increment one session counter and emit one `path_attribution_verified_rollup` latency row at shutdown; missing, ambiguous, or non-existent guesses retain the full `path_attribution_missing` record with its raw and guessed paths.
|
|
825
|
+
|
|
826
|
+
- **Keep context-free compiler findings non-blocking ([closes #1885](https://github.com/apmantza/pi-lens/issues/1885))** —
|
|
827
|
+
standalone javac, C/C++ syntax checks, Zig single-file builds, and direct
|
|
828
|
+
elixirc runs still report useful compiler errors, but no longer stop an edit
|
|
829
|
+
when missing project classpaths, build flags, modules, or dependencies could
|
|
830
|
+
have caused the finding. A recovered Java LSP clean result also replaces any
|
|
831
|
+
javac findings already held in session diagnostics.
|
|
832
|
+
|
|
833
|
+
- **Unverified path guesses remain visible ([refs #1886](https://github.com/apmantza/pi-lens/issues/1886))** —
|
|
834
|
+
a same-named workspace file no longer verifies a tool result when the
|
|
835
|
+
execution target and cwd are unavailable for comparison. The full
|
|
836
|
+
attribution record is retained, lifecycle rollups are tested through
|
|
837
|
+
`session_start` and primary or secondary `session_shutdown`, and the
|
|
838
|
+
session tally remains explicitly best-effort because a crash loses its
|
|
839
|
+
memory-only count.
|
|
840
|
+
|
|
841
|
+
- **The widget count now includes project self-scan findings** —
|
|
842
|
+
`lens_diagnostics mode=full` projects its correlated cross-lane result into
|
|
843
|
+
widget state, so `ast-grep-napi` findings remain counted when the ast-grep LSP
|
|
844
|
+
lane is broken or unconfirmed (#1888).
|
|
845
|
+
|
|
846
|
+
- **Formatter PATH availability re-arms at session start (closes [#1895](https://github.com/apmantza/pi-lens/issues/1895))** —
|
|
847
|
+
formatter `which` latches now clear with the primary session reset, so a
|
|
848
|
+
formatter binary installed or removed between sessions is detected on the
|
|
849
|
+
next session instead of keeping the previous session's verdict.
|
|
850
|
+
|
|
851
|
+
- **Coverage-marker dedupe key sorts with an explicit code-unit comparator.** SonarCloud S2871 flagged the bare `.sort()` in the silent-scanner set; the key now sorts locale-independently so dedupe identity cannot vary by environment.
|
|
852
|
+
|
|
853
|
+
- **Stale workspace-diagnostics cache entries now expire** — A cached entry that
|
|
854
|
+
asserts findings and predates the current session start, or that has aged past
|
|
855
|
+
a four-hour in-session ceiling, is dropped at serve time and the file goes back
|
|
856
|
+
through a fresh check. A cache hit used to skip that check, so an entry no
|
|
857
|
+
newer pull re-answered replayed its diagnostics forever, across sessions,
|
|
858
|
+
rendered as current blocking errors. Clean entries keep serving: their only
|
|
859
|
+
claim is "nothing changed", which the existing mtime, dependency, and
|
|
860
|
+
content-hash gates check. Each sweep that expires entries writes one
|
|
861
|
+
`lsp_workspace_diagnostics_cache_expiry` record to `latency.log` with the
|
|
862
|
+
count and the oldest age.
|
|
863
|
+
|
|
864
|
+
- **A clean re-answer now clears a cached diagnostics entry** — A
|
|
865
|
+
`workspace/diagnostic` pull returns a project-wide report, but pi-lens read
|
|
866
|
+
only the part covering the files it had just asked about. An explicit
|
|
867
|
+
zero-diagnostic answer for a file served from cache was discarded, so a server
|
|
868
|
+
that re-checked a file and found it clean could not dislodge its stale
|
|
869
|
+
blockers by any means available to a user. Those answers now flow through the
|
|
870
|
+
sweep's ordinary result list, which overwrites the cache entry and clears the
|
|
871
|
+
widget rows.
|
|
872
|
+
|
|
873
|
+
- **Surface silent LSP scanners to the agent (refs #1867)** — Per-edit
|
|
874
|
+
diagnostics now render a compact coverage marker when one or more auxiliary
|
|
875
|
+
scanners did not answer. An empty partial result is labeled incomplete instead
|
|
876
|
+
of looking like a clean pass, and repeated notices retain the existing
|
|
877
|
+
per-file session dedupe.
|
|
878
|
+
|
|
879
|
+
- **Heal the LSP view after an untracked disk edit** — An edit made outside
|
|
880
|
+
the tracked write/edit path, such as a bash-tool bulk edit, sent no
|
|
881
|
+
`didChange`, so the language server kept publishing pre-edit diagnostics
|
|
882
|
+
indefinitely. pi-lens now records what content actually landed on each
|
|
883
|
+
server and, on a bounded cadence, compares disk size and mtime against that
|
|
884
|
+
record. A document whose bytes really changed is re-pushed within 10 seconds
|
|
885
|
+
of the next LSP activity, to every server holding it — the primary language
|
|
886
|
+
server and the auxiliary scanners alike. The sweep stats at most 64 documents
|
|
887
|
+
per pass
|
|
888
|
+
(1.7ms measured), reads only the ones whose stat diverged, and issues at
|
|
889
|
+
most 4 resyncs per pass so a bulk edit heals in paced rounds.
|
|
890
|
+
|
|
891
|
+
- **`no-non-null-assertion` narrows two more idiom shapes** — a `!` guarded
|
|
892
|
+
by an identical truthy ternary condition, or re-asserted on a
|
|
893
|
+
`.filter()`-checked property immediately chained into `.map()`, no longer
|
|
894
|
+
flags. Both exclusions are scoped to the nearest enclosing ternary or
|
|
895
|
+
arrow function and require the same metavariable binding, so a
|
|
896
|
+
differently-bound guard still flags. Pi-lens's own residual drops from 36
|
|
897
|
+
to 27; the rule stays at `warning` pending the tier decision tracked in
|
|
898
|
+
#1818.
|
|
899
|
+
|
|
900
|
+
- **`no-unknown-returns` drops its callback/type-alias arm** — a
|
|
901
|
+
`function_type` (`(x: T) => R`) is a type-position annotation, and a
|
|
902
|
+
callback contract that returns `unknown` is legitimate, unlike a real
|
|
903
|
+
function whose value-position return skipped parsing at a boundary.
|
|
904
|
+
Dropping that arm takes pi-lens's own `clients/` + `index.ts` from 21 hits
|
|
905
|
+
to 9 (#1824).
|
|
906
|
+
|
|
907
|
+
- **Orphan backstop: off the session-start critical path, with verified kills ([closes #1857](https://github.com/apmantza/pi-lens/issues/1857))** —
|
|
908
|
+
The registry-independent orphan sweep scanned the OS process table inline at
|
|
909
|
+
session start. One dogfood session measured that scan at 9344ms, overlapping
|
|
910
|
+
and starving language-profile warmup. The sweep now runs on an unref'd timer
|
|
911
|
+
30 seconds after session start, under a machine-wide 30-minute cooldown, with
|
|
912
|
+
a 5-second hard timeout on the enumeration child. Session start pays nothing.
|
|
913
|
+
|
|
914
|
+
Three correctness defects went with it. Kill outcomes were counted as
|
|
915
|
+
successes: `killPidTree` returned no signal, so a permanently unkillable
|
|
916
|
+
process reported as reaped and paid the full sweep again every session. Kills
|
|
917
|
+
are now verified by a post-kill liveness poll, and a survivor is recorded by
|
|
918
|
+
identity (`<binary>#<pid>`) through the degradation ledger. The sweep's only
|
|
919
|
+
record used to be a `{scanned, killed}` count on the reaped path, so "ran and
|
|
920
|
+
found nothing", "never ran", and "threw" were the same absence; every sweep
|
|
921
|
+
now logs one of `clean`, `reaped`, `error`, `cooldown`, or `disabled`. And a
|
|
922
|
+
name-matching process spawned seconds ago but not yet registered was
|
|
923
|
+
kill-eligible the moment its launcher shim exited — a window measured at
|
|
924
|
+
890ms in the same session — so a process must now be at least 60 seconds old
|
|
925
|
+
before the backstop will touch it.
|
|
926
|
+
|
|
927
|
+
The registry-driven reaper carried the same attempt-counted-as-kill defect
|
|
928
|
+
and gets the same verified accounting.
|
|
929
|
+
|
|
930
|
+
Review round: the sweep runs under the repo's cross-process directory lock,
|
|
931
|
+
so two sessions starting together cannot both pay for it. A candidate spared
|
|
932
|
+
by the spawn grace arms exactly one follow-up sweep, instead of waiting for
|
|
933
|
+
a next session that a long-lived host may never start. The scan timeout
|
|
934
|
+
terminates the scanner child through the same tree-kill-and-verify path an
|
|
935
|
+
orphan gets, rather than firing one unverified signal and walking away. And
|
|
936
|
+
a sweep whose kills all failed reports `unverified`, not `reaped` — the same
|
|
937
|
+
accounting defect this work exists to fix, reproduced one level up at the
|
|
938
|
+
outcome surface.
|
|
939
|
+
|
|
940
|
+
- **Preserve pyright's information severity and fix its diagnostic positions in dispatch (closes #1802)** — `pyright` collapsed every `"information"`-tier diagnostic to `warning`, even though pyright's own `--outputjson` output declares `error`/`warning`/`information`. Pyright findings now carry the same four-tier `Diagnostic.severity` biome-check adopted in #1791: `information` maps to `info`. Only `error` still blocks. The parser also read a top-level `start` field that real pyright output never sets — every diagnostic landed at line 0/column 0 — and now reads the documented `range.start.line`/`character` (zero-based, converted to pi-lens's one-based `line`/`column`). `rust-clippy`'s mapping got its own fix: rustc/clippy's `compiler-message.level` serializes six values, not two, and a top-level `"note"`/`"help"` message can carry a real primary span. `note` now maps to `hint`, `help` to `info`, and an internal-compiler-error (`"error: internal compiler error"`) is classified as blocking instead of silently falling through to `warning`.
|
|
941
|
+
|
|
942
|
+
- **Eight ast-grep rules stop flagging correct code** — the 2026-08-20 severity audit found
|
|
943
|
+
broken escape valves, patterns that crossed function boundaries, and
|
|
944
|
+
content-shape gaps that fired on parameterized queries and static markup.
|
|
945
|
+
A follow-up adversarial review then caught three of the fixes
|
|
946
|
+
over-correcting (an escape valve exempting more than the false-positive
|
|
947
|
+
case, or a narrowed match losing a detection class); those are fixed too.
|
|
948
|
+
Per rule: `no-dupe-class-members`'s static-modifier check missed `static
|
|
949
|
+
async` methods (false positive) and, in the first fix, over-exempted
|
|
950
|
+
static members entirely, losing genuine `static foo(){} static foo(){}`
|
|
951
|
+
duplicates (false negative) — both fixed, staticness is now compared
|
|
952
|
+
BETWEEN the pair, not blanket-exempted. `no-return-value-in-generator`
|
|
953
|
+
conflated yield/return across nested functions. `no-compile-call` now
|
|
954
|
+
suppresses literal and same-scope `ast.parse`-derived sources, scoped to
|
|
955
|
+
the nearest enclosing function (a first pass climbed past nested
|
|
956
|
+
functions) and with the single-argument call form restored.
|
|
957
|
+
`no-server-bind-wildcard`'s first fix anchored to `.run`/`.listen`/
|
|
958
|
+
`.serve` call targets, which missed `uvicorn.Config(...)`, bare
|
|
959
|
+
`run(host=...)`, and other server-startup shapes — reverted to the
|
|
960
|
+
original broad match with a narrow exclusion for the actual false
|
|
961
|
+
positive (a pydantic `.model_construct()`/`.construct()` call).
|
|
962
|
+
`no-sql-in-code`/`-js` now require string concatenation or template
|
|
963
|
+
substitution, mirroring the already-correct Python sibling, and exclude
|
|
964
|
+
literal-plus-literal concatenation (compile-time-constant text, nothing
|
|
965
|
+
to inject); the residual corpus hits match the declared
|
|
966
|
+
concatenation/substitution shape, though a few interpolate module-level
|
|
967
|
+
constants rather than user input. `no-inner-html`/`-js` now exempt
|
|
968
|
+
literal/empty-template RHS. `redundant-unsafe-function` now exempts a
|
|
969
|
+
preceding `/// # Safety` doc comment, bounded to the comment block
|
|
970
|
+
immediately above the function (an unbounded first pass let one
|
|
971
|
+
`# Safety` comment anywhere earlier in the file exempt every later
|
|
972
|
+
`unsafe fn`). `no-await-expression-member`/`-js` no longer recommend a
|
|
973
|
+
rewrite that reads a property off the unresolved promise (prose only,
|
|
974
|
+
no rule change; kept rather than retired — the corrected hint is not a
|
|
975
|
+
no-op, though retirement stays available as a separate call).
|
|
976
|
+
Refs #1806.
|
|
977
|
+
|
|
978
|
+
- **Seven linters no longer report a clean file after a failed run** —
|
|
979
|
+
`markdownlint`, `mypy`, `sqlfluff`, `stylelint`, `swiftlint`, `vale`, and
|
|
980
|
+
`yamllint` read their tool's exit status zero times, so a rejected flag, an
|
|
981
|
+
unreadable config, a crash, or a `SIGKILL` parsed to zero diagnostics and was
|
|
982
|
+
reported as a clean file. All seven now route the spawn through a shared
|
|
983
|
+
`classifyRunOutcome` primitive, return `skipped`, and record one bounded
|
|
984
|
+
`runner-empty-result` row naming the tool, its exit status, and the signal.
|
|
985
|
+
A shared `formatToolFailure` gives every runner one wording and one
|
|
986
|
+
truncation, and `SpawnResult` now carries the signal as a field instead of
|
|
987
|
+
burying it in an error message. Two bugs the same survey found are fixed
|
|
988
|
+
alongside: the degradation ledger appended its repeat count before a second
|
|
989
|
+
truncation at the same bound, so a 200-character reason lost the count; and
|
|
990
|
+
the quarantine file lock required a finite `createdAt` before it would test
|
|
991
|
+
PID liveness, so an owner record without one was never reclaimed and every
|
|
992
|
+
caller burned its full wait.
|
|
993
|
+
|
|
994
|
+
- **Project snapshot misses now name the rejecting gate (refs #1858)** — session-start diagnostics distinguish an absent body, stale meta gate, invalid body, and sequence-stale snapshot instead of reporting every null load as `missing`.
|
|
995
|
+
|
|
996
|
+
- **Tree-sitter cache now spans a full-project scan** — the parse-tree cache
|
|
997
|
+
grows to fit a scan's file count (capped at 500 entries, or
|
|
998
|
+
`PI_LENS_TREE_SITTER_CACHE_SCAN_CAP`) instead of staying fixed at 50, so a
|
|
999
|
+
repeat `lens_diagnostics mode=full` scan reuses trees from the first pass
|
|
1000
|
+
instead of re-parsing every file (#1715).
|
|
1001
|
+
|
|
1002
|
+
- **Two error-tier ast-grep rules now run in the CI self-scan** —
|
|
1003
|
+
`no-unsafe-dictionary-any` and `no-bare-object-param` shipped at `error`
|
|
1004
|
+
without being wired into the `pi-lens-self-scan` category, so pi-lens's
|
|
1005
|
+
own tree was never actually audited by them. Both confirm clean (0
|
|
1006
|
+
findings) on the full tree (#1825).
|
|
1007
|
+
|
|
1008
|
+
### Security
|
|
1009
|
+
|
|
1010
|
+
- **A cascade sweep no longer blacks out the security scanner, and a blackout no longer reads as clean (closes [#1459](https://github.com/apmantza/pi-lens/issues/1459))** — A `clientScope: "all"` sweep used to fan a full-text `didOpen` resync at opengrep for every neighbour at once. Its stdin stopped draining, three per-server notify-write deadlines expired, and the #743 breaker opened for 15 s; every touch inside that window skipped the scanner and still resolved `confirmation: "confirmed"`. An auxiliary now gets at most one outstanding resync, as a bounded QUEUE rather than a drop: a healthy scanner accepts each write in milliseconds, so every neighbour of a sweep is still scanned, and only a scanner that cannot accept a write inside the budget makes a waiter give up and report itself uncovered. The slot is claimed synchronously, so waiters cannot all wake and write at once, and the queue wait is bounded by the caller's own budget rather than a flat write budget. A write that lands late but inside the wedge window retracts the timeout it was charged for; a write nothing accepts for the whole wedge window is demoted by its own timer, without waiting for a later touch to notice. A deferred auxiliary records a distinct `deferred` outcome in `lsp_aux_wait_outcome` rather than occupying the `silent` row, which stays reserved for a scanner that had the content and published nothing (#1493). #1493's content-hash exemption outranks a deferral: a scanner whose stored publication is bound to exactly these bytes stays covered, and its findings still reach the result. A deferred scanner is neither waited on nor read from, so it cannot burn its budget on a version that can never advance or contribute the previous content's findings. A scanner that never attached (breaker open) or never received the content (deferred resync) now lands in `unconfirmedServerIds`, narrows the touch to `"partial"`, and reaches the agent through `CascadeNeighborResult.unconfirmedServerIds` and an explicit "scanners did not cover" advisory line — so no consumer, and no agent, can read the gap as a clean bill of health. New `latency.log` records: `lsp_scanner_coverage_gap`, `lsp_notify_resync_deferred`, `lsp_notify_write_late_landed`.
|
|
1011
|
+
|
|
1012
|
+
- **A stalled `gh auth token` probe no longer disables zizmor's online audits for the session (closes #1535)** — A timed-out or unspawnable `gh auth token` lookup used to be memoized as "no token" with no TTL, silently turning off zizmor's GitHub-aware audits (`known-vulnerable-actions`, `unpinned-uses`, `impostor-commit`) for the rest of the session while the scan kept reporting success. The lookup now routes through the shared availability policy: only a genuine answer (`gh` ran and returned an exit code — including a clean run with no token — or is proven absent) is cached — a timeout, host stall, or unspawnable probe expires on a bounded cooldown and is retried, with every offline spawn (including one served from a still-cooling cache) recorded as a degradation instead of silent. The verdict also resets at the start of every session, so running `gh auth login` and starting fresh is picked up immediately, not just after a cooldown. Caveat: this only decides the token a *new* zizmor process launches with — an already-running (warm) zizmor keeps the token it started with until it next respawns, so recovery from a transient failure takes effect on the next spawn, not immediately.
|
|
1013
|
+
|
|
1014
|
+
- **Directory-scanning security lanes no longer scan pi-ecosystem scratch trees, and gitleaks recognizes placeholder secrets (closes #1562)** — `gitleaks`, `trivy fs`, and `opengrep` each hand their target directory to an external binary that does its own tree walk, bypassing pi-lens's own scratch/cache-tree exclusion (the same `.pi/`, `.claude/`, `node_modules/`, … list `knip`/`jscpd` already honor). In practice this meant a gitignored web-research cache (`.pi/greedysearch-sources/`) could serve a cached doc example's `YOUR_API_KEY` placeholder to the agent as a leaked secret — a false positive that cost a full turn to debunk. gitleaks excludes a NARROW pi-ecosystem/agent/cache-only tier (a review-round probe caught the first cut over-excluding real leak surfaces like `dist/`, `vendor/`, `.vscode/`, `build/` — those stay fully scanned), via a generated gitleaks config plus a TS-side backstop that demotes (never silently drops) a scratch-tree hit to an info-level, non-blocking finding with a `tracked`/`ignored`/`scratch`/`untracked` observability field. trivy/opengrep exclude the broader existing walker-parity list via their own native mechanism (`--skip-dirs`, `--exclude`). gitleaks also gets a placeholder-secret allowlist (`YOUR_API_KEY`, `<your-key>`, `xxx`, …) and its generated exclusion regexes match both `/` and `\` path separators. An untracked `.env` with a real-shaped secret is still reported — the fix does not become a blanket "respect `.gitignore`". `dead-code-client.ts`'s `VULTURE_EXCLUDES` list, found independently drifted from the shared exclusion list during this sweep, is now derived from it too.
|
|
1015
|
+
|
|
19
1016
|
## [4.0.1] - 2026-08-16
|
|
20
1017
|
|
|
21
1018
|
### Added
|
|
@@ -46,6 +1043,8 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
46
1043
|
|
|
47
1044
|
### Fixed
|
|
48
1045
|
|
|
1046
|
+
- **Timed-out pull diagnostics stop aging the LSP server (refs [#1889](https://github.com/apmantza/pi-lens/issues/1889))** — Document and workspace diagnostic pulls now send `$/cancelRequest` when their local deadline expires. Repeated native-TS7 touches no longer leave abandoned requests running and building a server-side backlog; an aging regression test holds outstanding pull work to one across repeated timeouts.
|
|
1047
|
+
|
|
49
1048
|
- **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
1049
|
|
|
51
1050
|
- **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.
|