@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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-boundary freshness gate for cached inline blockers (#1631).
|
|
3
|
+
*
|
|
4
|
+
* An inline blocker recorded for a file F is a verdict about F *and everything F
|
|
5
|
+
* imports*. The existing invalidation paths all key on F alone — a later dispatch
|
|
6
|
+
* of the SAME path returning no blockers, the path ceasing to exist (#1245), and a
|
|
7
|
+
* fresh confirmed-clean verdict (#1561/#1573). When only a DEPENDENCY of F changes,
|
|
8
|
+
* none of those events fire, so F's stale verdict re-served at every turn end for
|
|
9
|
+
* the rest of the session. The live instances in #1631 are exactly that shape: the
|
|
10
|
+
* missing export was added to the dependency (one via a bash script, so the
|
|
11
|
+
* dependency was never dispatched at all — even the same-path event was unreachable
|
|
12
|
+
* by construction).
|
|
13
|
+
*
|
|
14
|
+
* The gate here is a READ-time freshness sweep, not the #1561 dependency-axis
|
|
15
|
+
* invalidation (which stays blocked on a maintainer design decision). Before a cached
|
|
16
|
+
* blocking finding is re-served at turn end, stat the file and its forward imports.
|
|
17
|
+
* The file's own import list comes from the parse layer — no reverse-dependency index
|
|
18
|
+
* is needed, sidestepping the tests-free-index blocker that holds #1561's remainder.
|
|
19
|
+
*
|
|
20
|
+
* Drifted entries are DEMOTED, not dropped (#1419 precedent, as applied by sibling
|
|
21
|
+
* issue #1622): a surviving file whose content drifted gets re-served with a
|
|
22
|
+
* `[stale — re-run to confirm]` marker and out of the authoritative blocker channel,
|
|
23
|
+
* rather than being silently deleted or re-asserted at full authority. The gate never
|
|
24
|
+
* re-pulls LSP verdicts on its own — a drifted entry is marked stale and left for an
|
|
25
|
+
* explicit re-run or the next dispatch to resolve. That keeps the sweep bounded and
|
|
26
|
+
* free of the document-resync hazard the issue calls out (re-querying an LSP whose
|
|
27
|
+
* in-memory dependency document is itself stale would regenerate the stale verdict).
|
|
28
|
+
*
|
|
29
|
+
* Scope (#1631 review F4): an inline blocker is not always LSP-only —
|
|
30
|
+
* `dispatcher.ts` builds it from every `semantic === "blocking"` diagnostic across
|
|
31
|
+
* all runners, so an eslint, biome-check, or ast-grep security-rule (hardcoded
|
|
32
|
+
* secret, CVE) finding can land here too. Only a language-server verdict is
|
|
33
|
+
* actually invalidated by an IMPORT changing; an ast-grep secret match doesn't stop
|
|
34
|
+
* being true because a file it imports was edited. The sweep therefore demotes an
|
|
35
|
+
* entry only when its recorded `sources` (`InlineBlockerRecord.sources`) are ALL
|
|
36
|
+
* `"lsp"` — a mixed or non-LSP-sourced blocker is left fully authoritative, the same
|
|
37
|
+
* fail-closed shape `retireInlineBlockerOnConfirmedClean` already uses for
|
|
38
|
+
* provenance it can't yet reason about.
|
|
39
|
+
*
|
|
40
|
+
* Resolution boundary (#1631 review F8): `extractForwardImportPaths` parses static
|
|
41
|
+
* import/require syntax via tree-sitter. It does not, and cannot, resolve a
|
|
42
|
+
* dependency reached only through a dynamic `import()` or `require()` behind a
|
|
43
|
+
* runtime condition — that edge is invisible to this gate, so a blocker whose only
|
|
44
|
+
* stale dependency arrives that way survives a replay. This is an honest boundary,
|
|
45
|
+
* not a silently-closed one.
|
|
46
|
+
*/
|
|
47
|
+
import * as fs from "node:fs";
|
|
48
|
+
import { normalizeEphemeralMapKey } from "./path-utils.js";
|
|
49
|
+
import { resolveImportToFiles } from "./review-graph/import-resolvers.js";
|
|
50
|
+
import { getSharedTreeSitterClient, resolveTreeSitterLanguage, } from "./tree-sitter-shared.js";
|
|
51
|
+
import { TreeSitterSymbolExtractor } from "./tree-sitter-symbol-extractor.js";
|
|
52
|
+
/**
|
|
53
|
+
* Upper bound on the number of forward imports stat-checked for a single blocker.
|
|
54
|
+
* Blocking entries are rare, but one file can import many modules; the sweep must
|
|
55
|
+
* stay bounded so it cannot inflate a turn end.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_DRIFT_CHECK_IMPORTS = 128;
|
|
58
|
+
// Per-language extractor cache, mirroring review-graph/builder.ts and
|
|
59
|
+
// module-report.ts. Memoize failures too: a grammar that fails to load once is not
|
|
60
|
+
// re-probed for every blocker of that language within the process.
|
|
61
|
+
const extractorCache = new Map();
|
|
62
|
+
function getExtractor(languageId) {
|
|
63
|
+
let cached = extractorCache.get(languageId);
|
|
64
|
+
if (!cached) {
|
|
65
|
+
cached = (async () => {
|
|
66
|
+
const client = getSharedTreeSitterClient();
|
|
67
|
+
if (!client)
|
|
68
|
+
return null;
|
|
69
|
+
const extractor = new TreeSitterSymbolExtractor(languageId, client);
|
|
70
|
+
const ok = await extractor.init();
|
|
71
|
+
return ok ? extractor : null;
|
|
72
|
+
})().catch(() => null);
|
|
73
|
+
extractorCache.set(languageId, cached);
|
|
74
|
+
}
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* #1631 review F6: memoized forward-import RESOLUTION (the parse step, not the
|
|
79
|
+
* per-dependency mtime stats — those must always be read fresh so drift is never
|
|
80
|
+
* missed). Reviewer measurement: ~450ms/turn at 8 blockers × 300 imports
|
|
81
|
+
* unmemoized, ~435ms per `mode=all` call, because both the turn-end sweep and the
|
|
82
|
+
* `mode=all` gate independently re-parse the SAME consumer files' import lists.
|
|
83
|
+
*
|
|
84
|
+
* Keyed on (path, own mtime) — a cache hit is safe by construction: if the file's
|
|
85
|
+
* mtime hasn't moved, its parsed import LIST cannot have changed (the imports
|
|
86
|
+
* themselves may still have drifted; that's what the mtime stats after this cache
|
|
87
|
+
* check are for). Scoped PER TURN, not process-lifetime (the latch screen this
|
|
88
|
+
* class of state must pass): `noteImportResolutionMemoTurn` clears it whenever the
|
|
89
|
+
* `RuntimeCoordinator`'s `turnIndex` advances, so a resolver injected for one test
|
|
90
|
+
* or a project whose import graph is edited mid-session is never served a result
|
|
91
|
+
* from a stale earlier turn. When no turn index is available (mode=all called
|
|
92
|
+
* outside a tracked turn, or a test with no runtime), the cache is left alone —
|
|
93
|
+
* still correct because of the mtime key, just not turn-bounded in that case.
|
|
94
|
+
*/
|
|
95
|
+
const importResolutionMemo = new Map();
|
|
96
|
+
let importResolutionMemoTurnIndex;
|
|
97
|
+
function noteImportResolutionMemoTurn(turnIndex) {
|
|
98
|
+
if (turnIndex === undefined)
|
|
99
|
+
return;
|
|
100
|
+
if (importResolutionMemoTurnIndex !== turnIndex) {
|
|
101
|
+
importResolutionMemo.clear();
|
|
102
|
+
importResolutionMemoTurnIndex = turnIndex;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function resolveForwardImportsMemoized(cwd, filePath, resolveForwardImports) {
|
|
106
|
+
const sig = await statSignature(filePath);
|
|
107
|
+
if (sig === undefined) {
|
|
108
|
+
// Can't key reliably (deleted/unreadable) — resolve uncached rather than
|
|
109
|
+
// risk serving a memo entry for content that may no longer exist.
|
|
110
|
+
try {
|
|
111
|
+
return await resolveForwardImports(cwd, filePath);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const cached = importResolutionMemo.get(filePath);
|
|
118
|
+
// mtime alone collides for writes within the same timestamp tick (~1ms);
|
|
119
|
+
// size disambiguates the shrink-then-restore case (#1641's measurement).
|
|
120
|
+
if (cached && cached.mtimeMs === sig.mtimeMs && cached.size === sig.size)
|
|
121
|
+
return cached.imports;
|
|
122
|
+
let imports;
|
|
123
|
+
try {
|
|
124
|
+
imports = await resolveForwardImports(cwd, filePath);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
imports = [];
|
|
128
|
+
}
|
|
129
|
+
importResolutionMemo.set(filePath, { mtimeMs: sig.mtimeMs, size: sig.size, imports });
|
|
130
|
+
return imports;
|
|
131
|
+
}
|
|
132
|
+
/** Test-only: clear the extractor and import-resolution memos. */
|
|
133
|
+
export function _resetBlockerFreshnessForTests() {
|
|
134
|
+
extractorCache.clear();
|
|
135
|
+
importResolutionMemo.clear();
|
|
136
|
+
importResolutionMemoTurnIndex = undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Production forward-import resolver. Parses `filePath` with its tree-sitter grammar
|
|
140
|
+
* and resolves every extracted import source to in-project files. Degrades to an
|
|
141
|
+
* empty list (never throws) when the grammar, client, or file is unavailable — the
|
|
142
|
+
* sweep then falls back to checking only the blocker file's own drift.
|
|
143
|
+
*/
|
|
144
|
+
export async function extractForwardImportPaths(cwd, filePath) {
|
|
145
|
+
const languageId = resolveTreeSitterLanguage(filePath);
|
|
146
|
+
if (!languageId)
|
|
147
|
+
return [];
|
|
148
|
+
const client = getSharedTreeSitterClient();
|
|
149
|
+
if (!client)
|
|
150
|
+
return [];
|
|
151
|
+
const extractor = await getExtractor(languageId);
|
|
152
|
+
if (!extractor)
|
|
153
|
+
return [];
|
|
154
|
+
let content;
|
|
155
|
+
try {
|
|
156
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const outcome = await client.withParsedTree(filePath, languageId, content, (tree) => extractor.extract(tree, filePath, content).imports);
|
|
162
|
+
if (!outcome.parsed)
|
|
163
|
+
return [];
|
|
164
|
+
const resolved = new Set();
|
|
165
|
+
for (const ref of outcome.value) {
|
|
166
|
+
for (const target of resolveImportToFiles(cwd, filePath, languageId, ref.source)) {
|
|
167
|
+
resolved.add(target);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [...resolved];
|
|
171
|
+
}
|
|
172
|
+
async function statSignature(filePath) {
|
|
173
|
+
try {
|
|
174
|
+
const stat = await fs.promises.stat(filePath);
|
|
175
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function statMtimeMs(filePath) {
|
|
182
|
+
try {
|
|
183
|
+
const stat = await fs.promises.stat(filePath);
|
|
184
|
+
return stat.mtimeMs;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Resolve `filePath`'s forward imports and stat each, returning the imports that
|
|
192
|
+
* exist together with their mtime. Shared by the inline-blocker sweep and the
|
|
193
|
+
* widget-store dependency reconcile so both gates weigh drift identically. A path
|
|
194
|
+
* that cannot be stat'ed (deleted/unreadable) is omitted — a deleted dependency is
|
|
195
|
+
* not a content drift this gate reports.
|
|
196
|
+
*/
|
|
197
|
+
export async function collectForwardImportMtimes(cwd, filePath, resolveForwardImports = extractForwardImportPaths, turnIndex) {
|
|
198
|
+
noteImportResolutionMemoTurn(turnIndex);
|
|
199
|
+
let imports = [];
|
|
200
|
+
try {
|
|
201
|
+
imports = await resolveForwardImportsMemoized(cwd, filePath, resolveForwardImports);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
imports = [];
|
|
205
|
+
}
|
|
206
|
+
const truncated = imports.length > MAX_DRIFT_CHECK_IMPORTS;
|
|
207
|
+
const out = [];
|
|
208
|
+
for (const dep of imports.slice(0, MAX_DRIFT_CHECK_IMPORTS)) {
|
|
209
|
+
const mtimeMs = await statMtimeMs(dep);
|
|
210
|
+
if (mtimeMs !== undefined)
|
|
211
|
+
out.push({ path: dep, mtimeMs });
|
|
212
|
+
}
|
|
213
|
+
return { mtimes: out, truncated };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Detect drift for one blocker entry. Returns the set of paths (the file itself plus
|
|
217
|
+
* any forward import) whose mtime is strictly newer than the verdict baseline. A
|
|
218
|
+
* missing/unstat-able path contributes nothing here — a DELETED blocker file is
|
|
219
|
+
* already dropped by `reconcileInlineBlockers`, and a deleted dependency is not a
|
|
220
|
+
* content drift this gate is responsible for.
|
|
221
|
+
*
|
|
222
|
+
* +50ms tolerance. The verdict baseline is `Date.now()`, but on Windows a file's
|
|
223
|
+
* mtime can LEAD `Date.now()` by a measurable margin — not just sub-millisecond
|
|
224
|
+
* rounding. Reviewer measurement across 200 writes on Windows: 184/200 mtimes read
|
|
225
|
+
* ahead of the immediately-following `Date.now()`, by up to ~11.4ms, the same host
|
|
226
|
+
* skew #1491/#1498 hit for install-probe timestamps. A +1ms tolerance (the
|
|
227
|
+
* convention borrowed from `reconcileStaleWidgetFiles` /
|
|
228
|
+
* `reconcileProjectDiagnosticsSnapshot`, both same-process single-writer paths
|
|
229
|
+
* without this skew) demoted a blocker recorded immediately after its own file
|
|
230
|
+
* write with zero real drift — 42 false demotions in 50 runs, and two of this
|
|
231
|
+
* gate's own tests went host-dependent (red 3/6 runs on Windows, green 6/6 at
|
|
232
|
+
* +50ms). +50ms comfortably clears the measured skew while staying far below the
|
|
233
|
+
* gap between genuinely distinct edits.
|
|
234
|
+
*/
|
|
235
|
+
export const MTIME_DRIFT_TOLERANCE_MS = 50;
|
|
236
|
+
async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, turnIndex) {
|
|
237
|
+
const drifted = [];
|
|
238
|
+
const ownMtimeMs = await statMtimeMs(filePath);
|
|
239
|
+
if (ownMtimeMs !== undefined && ownMtimeMs > recordedAtMs + MTIME_DRIFT_TOLERANCE_MS) {
|
|
240
|
+
drifted.push(filePath);
|
|
241
|
+
}
|
|
242
|
+
const { mtimes, truncated } = await collectForwardImportMtimes(cwd, filePath, resolveForwardImports, turnIndex);
|
|
243
|
+
for (const { path, mtimeMs } of mtimes) {
|
|
244
|
+
if (mtimeMs > recordedAtMs + MTIME_DRIFT_TOLERANCE_MS)
|
|
245
|
+
drifted.push(path);
|
|
246
|
+
}
|
|
247
|
+
return { drifted, truncated };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Whether a population entry is eligible for the drift check at all — the same
|
|
251
|
+
* three gates the main sweep loop applies (not already stale, has a timestamp
|
|
252
|
+
* baseline, all-`"lsp"` sources). Factored out so the #1790 review F5 dedup
|
|
253
|
+
* decision below (chain vs. separate row) asks the IDENTICAL question the main
|
|
254
|
+
* loop will ask, rather than a second hand-written approximation of it that
|
|
255
|
+
* could drift from the real gates.
|
|
256
|
+
*/
|
|
257
|
+
function isEligibleForDriftCheck(entry) {
|
|
258
|
+
if (entry.stale)
|
|
259
|
+
return false;
|
|
260
|
+
if (entry.recordedAtMs === undefined)
|
|
261
|
+
return false;
|
|
262
|
+
return (entry.sources !== undefined &&
|
|
263
|
+
entry.sources.length > 0 &&
|
|
264
|
+
entry.sources.every((source) => source === "lsp"));
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Freshness sweep over every blocking row the widget currently serves — the cached
|
|
268
|
+
* inline blockers (`RuntimeCoordinator`) AND, since #1790, any widget-store row
|
|
269
|
+
* reached only through a cache-served replay (`options.additionalEntries`, injected
|
|
270
|
+
* by the call site to avoid an import cycle with `widget-state.ts`). Called at turn
|
|
271
|
+
* end before the blockers are re-served. Entries whose own file or forward imports
|
|
272
|
+
* drifted since the verdict are demoted via `demote`; the turn-end renderer then
|
|
273
|
+
* serves them out of the advisory channel with a `[stale — re-run to confirm]`
|
|
274
|
+
* marker instead of as an authoritative blocker.
|
|
275
|
+
*
|
|
276
|
+
* #1790: a file present in BOTH stores — the common case, since a live dispatch
|
|
277
|
+
* writes an inline blocker (`runtime-tool-result.ts`) AND a widget-store record
|
|
278
|
+
* (`pipeline.ts`) for the SAME verdict — is drift-checked ONCE, via its
|
|
279
|
+
* inline-blocker entry, so the sweep's one drift check never runs twice over the
|
|
280
|
+
* same file WHEN that inline entry is itself eligible for the check. But
|
|
281
|
+
* `markInlineBlockerStale` only ever touches `RuntimeCoordinator`'s map, never
|
|
282
|
+
* the widget store; on drift for a duplicated path, the widget demote is
|
|
283
|
+
* CHAINED onto the inline entry's `demote` (both stores write) rather than the
|
|
284
|
+
* widget row being silently dropped — the reviewer's F1 probe caught an earlier
|
|
285
|
+
* revision that discarded it: `revalidated:1` while the widget's own
|
|
286
|
+
* `isBlocking` for the file still read true, the exact ghost #1790 exists to
|
|
287
|
+
* kill.
|
|
288
|
+
*
|
|
289
|
+
* #1790 review F5: chaining is conditional on the inline entry actually being
|
|
290
|
+
* ELIGIBLE for the drift check (`isEligibleForDriftCheck` — not already stale,
|
|
291
|
+
* timestamped, all-LSP sources). An ineligible inline entry never reaches
|
|
292
|
+
* `demote()` in the loop below, so chaining onto one is ALSO a silent drop —
|
|
293
|
+
* just one store removed from F1's. The widget row's eligibility belongs to the
|
|
294
|
+
* WIDGET STORE, not to whatever inline entry happens to share its file path; an
|
|
295
|
+
* ineligible duplicate therefore gets its own separate population row instead
|
|
296
|
+
* of being chained, so a file can legitimately count twice (once per store) when
|
|
297
|
+
* the two stores disagree on eligibility.
|
|
298
|
+
*
|
|
299
|
+
* Never throws: any internal failure leaves the entry untouched (existing re-serve
|
|
300
|
+
* behavior) rather than failing the turn end.
|
|
301
|
+
*/
|
|
302
|
+
export async function sweepInlineBlockerFreshness(runtime, cwd, options) {
|
|
303
|
+
const counts = {
|
|
304
|
+
total: 0,
|
|
305
|
+
kept: 0,
|
|
306
|
+
revalidated: 0,
|
|
307
|
+
alreadyStale: 0,
|
|
308
|
+
truncatedImports: 0,
|
|
309
|
+
};
|
|
310
|
+
const resolveForwardImports = options?.resolveForwardImports ?? extractForwardImportPaths;
|
|
311
|
+
let turnIndex;
|
|
312
|
+
try {
|
|
313
|
+
turnIndex = runtime.turnIndex;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
turnIndex = undefined;
|
|
317
|
+
}
|
|
318
|
+
let inlineEntries;
|
|
319
|
+
try {
|
|
320
|
+
inlineEntries = runtime.getInlineBlockersSnapshot();
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return counts;
|
|
324
|
+
}
|
|
325
|
+
const population = inlineEntries.map((entry) => ({
|
|
326
|
+
filePath: entry.filePath,
|
|
327
|
+
stale: entry.stale ?? false,
|
|
328
|
+
recordedAtMs: entry.recordedAtMs,
|
|
329
|
+
sources: entry.sources,
|
|
330
|
+
demote: () => runtime.markInlineBlockerStale(entry.filePath, "dependency-drift"),
|
|
331
|
+
}));
|
|
332
|
+
// #1790 review F2: dedup key is `normalizeEphemeralMapKey`, not
|
|
333
|
+
// `normalizeMapKey` — the latter realpaths a live file and walks up the
|
|
334
|
+
// directory tree for a missing one (measured ~313µs per deleted path, exactly
|
|
335
|
+
// the population this sweep processes) on a turn-end hot path. The widget
|
|
336
|
+
// store this population is reconciled against keys on
|
|
337
|
+
// `normalizeEphemeralMapKey` too (see `widget-state.ts`'s module doc on why
|
|
338
|
+
// `normalizeMapKey` is wrong for this in-process, single-walk kind of key),
|
|
339
|
+
// so this also keeps the dedup consistent with the store it is deduping
|
|
340
|
+
// against, not just cheaper.
|
|
341
|
+
const inlineByKey = new Map();
|
|
342
|
+
for (const entry of population) {
|
|
343
|
+
inlineByKey.set(normalizeEphemeralMapKey(entry.filePath), entry);
|
|
344
|
+
}
|
|
345
|
+
for (const extra of options?.additionalEntries ?? []) {
|
|
346
|
+
const key = normalizeEphemeralMapKey(extra.filePath);
|
|
347
|
+
const inlineEntry = inlineByKey.get(key);
|
|
348
|
+
// #1790 review F5: chaining onto an INELIGIBLE inline entry is a silent
|
|
349
|
+
// drop, not a merge. The main loop below short-circuits BEFORE ever
|
|
350
|
+
// calling `demote()` for an already-stale entry (the one-way
|
|
351
|
+
// dependency-drift latch — a forever-ghost once it fires once), an
|
|
352
|
+
// unstamped legacy record, or a non-LSP/mixed-sources entry (e.g. an
|
|
353
|
+
// unrelated ast-grep finding on the same file). Any of those swallows a
|
|
354
|
+
// chained widget demote even though the widget row is pure-LSP by
|
|
355
|
+
// construction (`getWidgetBlockingFilesForSweep` only emits LSP-sourced
|
|
356
|
+
// rows) and carries its OWN baseline. Eligibility belongs to the STORE
|
|
357
|
+
// the row came from, not the file path two stores happen to share — so
|
|
358
|
+
// only chain when the inline entry would itself reach `demote()`;
|
|
359
|
+
// otherwise give the widget row its own population entry so its own
|
|
360
|
+
// gates and its own drift check decide its own fate.
|
|
361
|
+
if (inlineEntry && isEligibleForDriftCheck(inlineEntry)) {
|
|
362
|
+
// #1790 review F1: a duplicated path is counted and drift-checked ONCE
|
|
363
|
+
// (via the inline entry above), but BOTH stores must record the
|
|
364
|
+
// verdict — `markInlineBlockerStale` only ever touches
|
|
365
|
+
// `RuntimeCoordinator`'s map, so without this chain the widget store's
|
|
366
|
+
// own diagnostic for this file stays fully blocking (`isBlocking` true)
|
|
367
|
+
// even after the inline entry demotes. Chain, don't replace: either
|
|
368
|
+
// store's own untouched failure path must not suppress the other's
|
|
369
|
+
// write.
|
|
370
|
+
const demoteInline = inlineEntry.demote;
|
|
371
|
+
const demoteWidget = extra.demote;
|
|
372
|
+
inlineEntry.demote = () => {
|
|
373
|
+
const inlineChanged = demoteInline();
|
|
374
|
+
const widgetChanged = demoteWidget();
|
|
375
|
+
return inlineChanged || widgetChanged;
|
|
376
|
+
};
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
population.push({
|
|
380
|
+
filePath: extra.filePath,
|
|
381
|
+
stale: false,
|
|
382
|
+
recordedAtMs: extra.recordedAtMs,
|
|
383
|
+
sources: ["lsp"],
|
|
384
|
+
demote: extra.demote,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
counts.total = population.length;
|
|
388
|
+
for (const entry of population) {
|
|
389
|
+
try {
|
|
390
|
+
if (entry.stale) {
|
|
391
|
+
counts.alreadyStale += 1;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
// No timestamp baseline (legacy/unstamped record) — we cannot order disk
|
|
395
|
+
// mtimes against the verdict, so leave it untouched (fail toward the
|
|
396
|
+
// existing behavior rather than fabricating a drift signal).
|
|
397
|
+
if (entry.recordedAtMs === undefined) {
|
|
398
|
+
counts.kept += 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
// #1631 review F4: fail-closed on non-LSP (or unknown) provenance. Import
|
|
402
|
+
// drift only invalidates a language-server verdict; a mixed or non-`"lsp"`
|
|
403
|
+
// `sources` list is kept at full authority rather than demoted.
|
|
404
|
+
const isLspSourced = entry.sources !== undefined &&
|
|
405
|
+
entry.sources.length > 0 &&
|
|
406
|
+
entry.sources.every((source) => source === "lsp");
|
|
407
|
+
if (!isLspSourced) {
|
|
408
|
+
counts.kept += 1;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const { drifted, truncated } = await detectDrift(cwd, entry.filePath, entry.recordedAtMs, resolveForwardImports, turnIndex);
|
|
412
|
+
if (truncated)
|
|
413
|
+
counts.truncatedImports += 1;
|
|
414
|
+
if (drifted.length > 0) {
|
|
415
|
+
entry.demote();
|
|
416
|
+
counts.revalidated += 1;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
counts.kept += 1;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Per-entry failure: keep the entry as-is.
|
|
424
|
+
counts.kept += 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return counts;
|
|
428
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Past-EOF validity gate for the turn-end inline-blocker store (#1641
|
|
3
|
+
* remainder).
|
|
4
|
+
*
|
|
5
|
+
* #1664 wired `demotePastEofDiagnostics` into every surface that renders a
|
|
6
|
+
* `WidgetDiagnostic` — widget-state, `lens_diagnostics` (`mode=all`/`full`),
|
|
7
|
+
* and the TUI blocking-render loop. It deliberately deferred one surface:
|
|
8
|
+
* `RuntimeCoordinator`'s `_pendingInlineBlockers` map (the "Unresolved from
|
|
9
|
+
* this turn" blocker re-served at `turn_end`, `clients/runtime-turn.ts`).
|
|
10
|
+
* That store's records (`InlineBlockerRecord`) never carried a `WidgetDiagnostic`
|
|
11
|
+
* — just a rendered `summary` string — so the shared gate had nothing to run
|
|
12
|
+
* against. #1633 was meant to add the structured line field and never did;
|
|
13
|
+
* it merged with only the dependency-drift gate (`blocker-freshness.ts`)
|
|
14
|
+
* wired in. This module is that missing half.
|
|
15
|
+
*
|
|
16
|
+
* Design choice (#1641 remainder, decided here): carry a STRUCTURED `lines`
|
|
17
|
+
* field on `InlineBlockerRecord`, captured at write time from the same
|
|
18
|
+
* `Diagnostic[]` the summary text was rendered from
|
|
19
|
+
* (`dispatchResult.blockers` in `pipeline.ts`), rather than regex-parsing the
|
|
20
|
+
* cited line back out of the rendered summary prose. Re-deriving a number
|
|
21
|
+
* that was already in hand one call earlier — by parsing a string built for
|
|
22
|
+
* human display, not machine reading — is exactly the re-derivation-vs-
|
|
23
|
+
* correlation screen's failure shape: any future change to the summary's
|
|
24
|
+
* wording (a different prefix, a renamed tool label) would silently break
|
|
25
|
+
* the parse with no type error. Threading the number through is one field on
|
|
26
|
+
* an existing interface and a few lines at each hop.
|
|
27
|
+
*
|
|
28
|
+
* Composition with #1631's dependency-drift gate (`blocker-freshness.ts`,
|
|
29
|
+
* also swept at `turn_end`): each gate demotes with its own
|
|
30
|
+
* `InlineBlockerRecord.staleReason` and only re-derives (heals) entries
|
|
31
|
+
* carrying its OWN reason — `setInlineBlockerPastEofStale` refuses to touch a
|
|
32
|
+
* `"dependency-drift"` demotion, and this gate skips a record already
|
|
33
|
+
* demoted for that reason before it even computes a verdict. The two gates
|
|
34
|
+
* therefore layer rather than race: whichever ran first "wins" the record for
|
|
35
|
+
* this turn, and the other's condition is picked up again once the first
|
|
36
|
+
* heals or a fresh dispatch/retire clears the record outright.
|
|
37
|
+
*
|
|
38
|
+
* RE-ARMS, never latches (mirrors `demotePastEofDiagnostics`): the verdict is
|
|
39
|
+
* recomputed from the file's CURRENT line count on every turn end, so a
|
|
40
|
+
* transient shrink-then-restore un-demotes on its own next turn without any
|
|
41
|
+
* session-lifetime flag to reset.
|
|
42
|
+
*/
|
|
43
|
+
import { demotePastEofDiagnostics, resyncDocumentOnPastEof, } from "./diagnostic-line-freshness.js";
|
|
44
|
+
/**
|
|
45
|
+
* Past-EOF sweep over the cached inline blockers. Called at turn end
|
|
46
|
+
* alongside (and, per the module doc, ordered before) the dependency-drift
|
|
47
|
+
* sweep — a cheap `statSync` per file is worth paying first so the pricier
|
|
48
|
+
* import-parsing sweep can skip anything this gate already took out of the
|
|
49
|
+
* authoritative channel.
|
|
50
|
+
*
|
|
51
|
+
* Never throws: a per-entry failure leaves that entry untouched rather than
|
|
52
|
+
* failing the turn end.
|
|
53
|
+
*/
|
|
54
|
+
export function sweepInlineBlockerPastEof(runtime, cwd, options) {
|
|
55
|
+
const counts = {
|
|
56
|
+
total: 0,
|
|
57
|
+
checked: 0,
|
|
58
|
+
demoted: 0,
|
|
59
|
+
deadLinesByPath: new Map(),
|
|
60
|
+
};
|
|
61
|
+
const resync = options?.resync ?? resyncDocumentOnPastEof;
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = runtime.getInlineBlockersSnapshot();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return counts;
|
|
68
|
+
}
|
|
69
|
+
counts.total = entries.length;
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
try {
|
|
72
|
+
if (!entry.lines || entry.lines.length === 0)
|
|
73
|
+
continue;
|
|
74
|
+
// Not this gate's demotion to heal or re-assert (#1641 composition
|
|
75
|
+
// rule — see module doc).
|
|
76
|
+
if (entry.stale && entry.staleReason !== "past-eof")
|
|
77
|
+
continue;
|
|
78
|
+
counts.checked += 1;
|
|
79
|
+
// Reuse the shared gate's per-line verdict machinery: feed it one
|
|
80
|
+
// synthetic "diagnostic" per cited line, sharing this record's
|
|
81
|
+
// current stale/staleReason so it can detect the rising/falling
|
|
82
|
+
// edge itself instead of this module re-implementing that logic.
|
|
83
|
+
const probes = entry.lines.map((line) => ({
|
|
84
|
+
line,
|
|
85
|
+
stale: entry.stale,
|
|
86
|
+
staleReason: entry.staleReason,
|
|
87
|
+
}));
|
|
88
|
+
// `demotePastEofDiagnostics` already logs (`diagnostic_past_eof`) and
|
|
89
|
+
// fires `resync` exactly once per call on a rising edge — reusing it
|
|
90
|
+
// here (rather than re-implementing that bookkeeping) keeps this
|
|
91
|
+
// gate's telemetry and resync-on-demotion behavior identical to the
|
|
92
|
+
// widget/lens surfaces #1664 already wired.
|
|
93
|
+
const { diagnostics } = demotePastEofDiagnostics({
|
|
94
|
+
store: "inline-blocker",
|
|
95
|
+
cwd,
|
|
96
|
+
filePath: entry.filePath,
|
|
97
|
+
diagnostics: probes,
|
|
98
|
+
lineCountCache: options?.lineCountCache,
|
|
99
|
+
resync,
|
|
100
|
+
});
|
|
101
|
+
// The record cites any of its lines being past EOF as a whole-record
|
|
102
|
+
// demotion — one summary string, not one flag per line.
|
|
103
|
+
const isPastEof = diagnostics.some((d) => d.stale);
|
|
104
|
+
if (isPastEof) {
|
|
105
|
+
// #1944: record WHICH cited lines died, before the transition check
|
|
106
|
+
// below short-circuits. A record demoted on an earlier turn does not
|
|
107
|
+
// transition, yet its dead lines are exactly what the delivery
|
|
108
|
+
// surface must annotate and retire on.
|
|
109
|
+
counts.deadLinesByPath.set(entry.filePath, diagnostics
|
|
110
|
+
.filter((d) => d.stale)
|
|
111
|
+
.map((d) => d.line)
|
|
112
|
+
.filter((line) => typeof line === "number"));
|
|
113
|
+
}
|
|
114
|
+
const transitioned = runtime.setInlineBlockerPastEofStale(entry.filePath, isPastEof);
|
|
115
|
+
if (!transitioned)
|
|
116
|
+
continue;
|
|
117
|
+
// Only the rising edge is countable here — see `BlockerPastEofCounts`
|
|
118
|
+
// for why the falling edge cannot occur on this store anymore. The
|
|
119
|
+
// call below still un-demotes, so the STORE stays honest even though
|
|
120
|
+
// nothing can observe the transition.
|
|
121
|
+
if (isPastEof)
|
|
122
|
+
counts.demoted += 1;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Per-entry failure: leave the entry as-is.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return counts;
|
|
129
|
+
}
|
|
@@ -47,6 +47,14 @@ async function logBootstrapFailures(failures) {
|
|
|
47
47
|
// the per-analyzer lines above already named the failures
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Every per-client load below is individually fail-soft (`load`/`loadList`
|
|
52
|
+
* degrade to a stub instead of throwing), so this promise is not expected to
|
|
53
|
+
* reject in practice. It is still memoized with eviction-on-rejection —
|
|
54
|
+
* consistent with the other lazy-import memos (#1570) — so a genuinely
|
|
55
|
+
* unexpected throw (e.g. `logBootstrapFailures`) cannot latch a permanently
|
|
56
|
+
* rejected bootstrap for the rest of the process.
|
|
57
|
+
*/
|
|
50
58
|
export function loadBootstrapClients() {
|
|
51
59
|
bootstrapPromise ??= (async () => {
|
|
52
60
|
const failures = [];
|
|
@@ -112,6 +120,9 @@ export function loadBootstrapClients() {
|
|
|
112
120
|
agentBehaviorClient,
|
|
113
121
|
deadCodeClients,
|
|
114
122
|
};
|
|
115
|
-
})()
|
|
123
|
+
})().catch((err) => {
|
|
124
|
+
bootstrapPromise = null;
|
|
125
|
+
throw err;
|
|
126
|
+
});
|
|
116
127
|
return bootstrapPromise;
|
|
117
128
|
}
|
|
@@ -57,9 +57,18 @@ async function releaseQuarantineLock(lockPath, token) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
function quarantineOwnerIsStale(owner, staleMs) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// #1816: the two staleness signals are INDEPENDENT, and the original
|
|
61
|
+
// conjunction made the dead-PID one unreachable. An `owner.json` with a
|
|
62
|
+
// valid PID but a missing or non-numeric `createdAt` (an older writer, a
|
|
63
|
+
// half-written file, a hand-edited one) short-circuited on the
|
|
64
|
+
// `Number.isFinite` guard, so a dead owner never reclaimed and the lock
|
|
65
|
+
// stayed poisoned for the life of the directory. This is
|
|
66
|
+
// `installer/index.ts:173`'s predicate: a dead PID reclaims regardless of
|
|
67
|
+
// `createdAt`, and an aged lock reclaims regardless of what the PID says.
|
|
68
|
+
const pidUsable = Number.isInteger(owner.pid) && owner.pid > 0;
|
|
69
|
+
if (pidUsable && !ownerPidIsLive(owner.pid))
|
|
70
|
+
return true;
|
|
71
|
+
return (Number.isFinite(owner.createdAt) && Date.now() - owner.createdAt > staleMs);
|
|
63
72
|
}
|
|
64
73
|
async function reclaimQuarantineLock(lockPath, staleMs) {
|
|
65
74
|
const quarantined = quarantinePath(lockPath, `reclaim-${Date.now()}-${randomUUID()}`);
|