@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
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 +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -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/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -26,17 +26,22 @@ const rustClient = new RustClient();
|
|
|
26
26
|
// The verdict is governed by the shared availability policy (#1494): a probe
|
|
27
27
|
// timeout expires on a cooldown, so only a genuinely absent clippy latches.
|
|
28
28
|
const CLIPPY_PROBE_BUDGET_MS = 8000;
|
|
29
|
-
|
|
29
|
+
let clippyProbeRevision = 0;
|
|
30
|
+
const makeClippyProbe = (cargoExe, flightKeyComponent = cargoExe) => createCwdCachedProbe((cwd) => safeSpawnAsync(cargoExe, ["clippy", "--version"], {
|
|
30
31
|
timeout: CLIPPY_PROBE_BUDGET_MS,
|
|
31
32
|
cwd,
|
|
32
|
-
}), {
|
|
33
|
+
}), {
|
|
34
|
+
tool: "clippy",
|
|
35
|
+
budgetMs: CLIPPY_PROBE_BUDGET_MS,
|
|
36
|
+
flightKeyComponent,
|
|
37
|
+
});
|
|
33
38
|
const clippyAvailabilityByCargo = new Map();
|
|
34
39
|
function getClippyProbe(cargoExe) {
|
|
35
40
|
return (clippyAvailabilityByCargo.get(cargoExe) ?? refreshClippyProbe(cargoExe));
|
|
36
41
|
}
|
|
37
42
|
/** Replace the cached probe so a post-install state is observed. */
|
|
38
43
|
function refreshClippyProbe(cargoExe) {
|
|
39
|
-
const created = makeClippyProbe(cargoExe);
|
|
44
|
+
const created = makeClippyProbe(cargoExe, `${cargoExe}#refresh-${++clippyProbeRevision}`);
|
|
40
45
|
clippyAvailabilityByCargo.set(cargoExe, created);
|
|
41
46
|
return created;
|
|
42
47
|
}
|
|
@@ -10,6 +10,7 @@ import * as fs from "node:fs";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { logSessionStart } from "../../../sessionstart-logger.js";
|
|
13
|
+
import { incrementDegradationCount } from "../../../degradation-ledger.js";
|
|
13
14
|
import { getGlobalPiLensDir } from "../../../file-utils.js";
|
|
14
15
|
import { createGenerationSource, } from "../../../generation-guard.js";
|
|
15
16
|
import { PathKeyedMap } from "../../../path-keyed-map.js";
|
|
@@ -18,8 +19,10 @@ import { ensureTool, getInstallAttempt, getLastEnsureResolutionSource, getToolIn
|
|
|
18
19
|
import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/config.js";
|
|
19
20
|
import { findGlobalBinary } from "../../../package-manager.js";
|
|
20
21
|
import { safeSpawnAsync } from "../../../safe-spawn.js";
|
|
22
|
+
import { compareOrdinal } from "../../../string-utils.js";
|
|
21
23
|
import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
|
|
22
24
|
import { isInSpawnTimeoutCooldown } from "../../../spawn-timeout-cooldown.js";
|
|
25
|
+
import { createAvailabilityProbeFlight } from "../../../availability-probe-flight.js";
|
|
23
26
|
import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
|
|
24
27
|
export { createAvailabilityLatch, classifyProbeFailure, describeProbeEvidence, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
|
|
25
28
|
/**
|
|
@@ -118,14 +121,14 @@ const MANAGED_VERIFY_COOLDOWN_MAX_MS = 15 * 60_000;
|
|
|
118
121
|
function managedVerifyCooldownMs(attempts) {
|
|
119
122
|
return Math.min(MANAGED_VERIFY_COOLDOWN_MAX_MS, MANAGED_VERIFY_COOLDOWN_MS * 2 ** (attempts - 1));
|
|
120
123
|
}
|
|
121
|
-
async function runManagedVerification(candidate, stamp, priorAttempts, generation) {
|
|
124
|
+
async function runManagedVerification(candidate, stamp, priorAttempts, generation, verificationArgs = ["--version"]) {
|
|
122
125
|
let transient = false;
|
|
123
126
|
let ok;
|
|
124
127
|
try {
|
|
125
128
|
const { verifyToolBinary } = await import("../../../installer/index.js");
|
|
126
129
|
ok = await verifyToolBinary(candidate, undefined, () => {
|
|
127
130
|
transient = true;
|
|
128
|
-
}, MANAGED_VERIFY_TIMEOUT_MS);
|
|
131
|
+
}, MANAGED_VERIFY_TIMEOUT_MS, verificationArgs);
|
|
129
132
|
}
|
|
130
133
|
catch {
|
|
131
134
|
// The verifier itself could not run — installer-isolated unit tests mock
|
|
@@ -164,7 +167,7 @@ async function runManagedVerification(candidate, stamp, priorAttempts, generatio
|
|
|
164
167
|
}
|
|
165
168
|
return ok ? "ok" : "broken";
|
|
166
169
|
}
|
|
167
|
-
async function verifyManagedCandidate(candidate) {
|
|
170
|
+
async function verifyManagedCandidate(candidate, verificationArgs = ["--version"]) {
|
|
168
171
|
let stamp;
|
|
169
172
|
try {
|
|
170
173
|
const stat = fs.statSync(candidate);
|
|
@@ -186,7 +189,7 @@ async function verifyManagedCandidate(candidate) {
|
|
|
186
189
|
if (existing)
|
|
187
190
|
return existing;
|
|
188
191
|
const generation = availabilityGeneration.capture();
|
|
189
|
-
const probe = runManagedVerification(candidate, stamp, priorAttempts, generation).finally(() => {
|
|
192
|
+
const probe = runManagedVerification(candidate, stamp, priorAttempts, generation, verificationArgs).finally(() => {
|
|
190
193
|
// A settling old-session probe must not evict the live entry a new
|
|
191
194
|
// session already started for the same shim (#1674 review F5).
|
|
192
195
|
generation.guardedWrite(stamp, () => {
|
|
@@ -218,9 +221,9 @@ async function verifyManagedCandidate(candidate) {
|
|
|
218
221
|
* stalls on a cold cache still gets re-probed later (#1674 review F1).
|
|
219
222
|
* Concurrent first touches share one probe (#1674 review F2).
|
|
220
223
|
*/
|
|
221
|
-
export async function findManagedNodeToolBinary(tool) {
|
|
224
|
+
export async function findManagedNodeToolBinary(tool, verificationArgs = ["--version"]) {
|
|
222
225
|
for (const candidate of managedNodeToolCandidates(tool)) {
|
|
223
|
-
const verdict = await verifyManagedCandidate(candidate);
|
|
226
|
+
const verdict = await verifyManagedCandidate(candidate, verificationArgs);
|
|
224
227
|
if (verdict === "ok" || verdict === "unverified")
|
|
225
228
|
return candidate;
|
|
226
229
|
}
|
|
@@ -237,7 +240,7 @@ export async function findManagedNodeToolBinary(tool) {
|
|
|
237
240
|
* `shell: false` (safe-spawn, #817), so wrapping it in quotes would make it
|
|
238
241
|
* a literal filename that ENOENTs on every platform (#1508).
|
|
239
242
|
*/
|
|
240
|
-
export function createVenvFinder(command, windowsExt = "") {
|
|
243
|
+
export function createVenvFinder(command, windowsExt = "", verificationArgs = ["--version"]) {
|
|
241
244
|
return async (cwd) => {
|
|
242
245
|
const venvPaths = [
|
|
243
246
|
`.venv/bin/${command}`,
|
|
@@ -261,7 +264,7 @@ export function createVenvFinder(command, windowsExt = "") {
|
|
|
261
264
|
// check settles it without a spawn — after one verification per shim per
|
|
262
265
|
// session, so a shim that cannot run falls through to PATH instead of
|
|
263
266
|
// shadowing a working binary (#1657).
|
|
264
|
-
const managed = await findManagedNodeToolBinary(command);
|
|
267
|
+
const managed = await findManagedNodeToolBinary(command, verificationArgs);
|
|
265
268
|
if (managed)
|
|
266
269
|
return managed;
|
|
267
270
|
// Fall back to global
|
|
@@ -366,6 +369,19 @@ const latchedUnavailableByCwd = new PathKeyedMap(normalizeMapKey);
|
|
|
366
369
|
// instead of five hand-rolled ones, and a dropped straddling write is visible
|
|
367
370
|
// in the degradation ledger instead of silent.
|
|
368
371
|
const availabilityGeneration = createGenerationSource("dispatch-availability");
|
|
372
|
+
export const getDispatchAvailabilityGeneration = () => availabilityGeneration.current();
|
|
373
|
+
const checkerProbeFlights = createAvailabilityProbeFlight({ generation: getDispatchAvailabilityGeneration });
|
|
374
|
+
export function recordAvailabilityProbeOverrun(tool, key, elapsedMs, budgetMs, failure) {
|
|
375
|
+
if (budgetMs === undefined || elapsedMs <= budgetMs)
|
|
376
|
+
return;
|
|
377
|
+
if (failure === "timeout" && elapsedMs <= budgetMs * 2)
|
|
378
|
+
return;
|
|
379
|
+
incrementDegradationCount({
|
|
380
|
+
kind: "availability-probe-overrun",
|
|
381
|
+
subject: `${tool}:${key}`,
|
|
382
|
+
reason: `probe took ${Math.round(elapsedMs)}ms; budget was ${Math.round(budgetMs)}ms`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
369
385
|
function installStateFor(cwd, toolId) {
|
|
370
386
|
let states = installAttemptsByCwd.get(cwd);
|
|
371
387
|
if (!states) {
|
|
@@ -598,6 +614,9 @@ export function resetDispatchAvailabilityState() {
|
|
|
598
614
|
managedVerifyInFlight.clear();
|
|
599
615
|
resetPathWalkMemo();
|
|
600
616
|
availabilityGeneration.bump();
|
|
617
|
+
checkerProbeFlights.clear();
|
|
618
|
+
cwdProbeFlights.clear();
|
|
619
|
+
// The generation bump invalidates dispatcherProbeFlights in its owner module.
|
|
601
620
|
}
|
|
602
621
|
/**
|
|
603
622
|
* Create a cached availability checker for a command.
|
|
@@ -620,18 +639,21 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
620
639
|
const cacheByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
621
640
|
const inFlightByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
622
641
|
let checkerGeneration = availabilityGeneration.current();
|
|
623
|
-
|
|
642
|
+
let checkerFlightGeneration = 0;
|
|
643
|
+
const findCommand = createVenvFinder(command, windowsExt, versionArgs);
|
|
624
644
|
function ensureCurrentGeneration() {
|
|
625
645
|
if (checkerGeneration === availabilityGeneration.current())
|
|
626
646
|
return;
|
|
627
647
|
cacheByCwd.clear();
|
|
628
648
|
inFlightByCwd.clear();
|
|
629
649
|
checkerGeneration = availabilityGeneration.current();
|
|
650
|
+
checkerFlightGeneration++;
|
|
630
651
|
}
|
|
631
652
|
const reset = () => {
|
|
632
653
|
cacheByCwd.clear();
|
|
633
654
|
inFlightByCwd.clear();
|
|
634
655
|
checkerGeneration = availabilityGeneration.current();
|
|
656
|
+
checkerFlightGeneration++;
|
|
635
657
|
};
|
|
636
658
|
function getCache(cwd) {
|
|
637
659
|
ensureCurrentGeneration();
|
|
@@ -692,9 +714,9 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
692
714
|
cause: verdict.cause,
|
|
693
715
|
elapsedMs: verdict.elapsedMs,
|
|
694
716
|
latched,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
717
|
+
// Every caller passes this (#2209); a direct key, not a conditional
|
|
718
|
+
// spread, keeps it visible to the call-shaped classifiedBy sweep.
|
|
719
|
+
classifiedBy: verdict.classifiedBy,
|
|
698
720
|
...(verdict.evidence !== undefined && { evidence: verdict.evidence }),
|
|
699
721
|
...(verdict.hostStallMs !== undefined && {
|
|
700
722
|
hostStallMs: verdict.hostStallMs,
|
|
@@ -807,17 +829,22 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
807
829
|
const startedAt = Date.now();
|
|
808
830
|
let result;
|
|
809
831
|
let hostStallMs;
|
|
832
|
+
let probeJoined = false;
|
|
810
833
|
try {
|
|
811
|
-
|
|
834
|
+
const shared = checkerProbeFlights.run(`checker:${command}|${versionArgs.join("|")}|${key}|${checkerFlightGeneration}|${windowsExt}|${cmd}|${JSON.stringify(Object.entries(env ?? {}).sort(([a], [b]) => compareOrdinal(a, b)))}`, () => safeSpawnAsync(cmd, versionArgs, {
|
|
812
835
|
timeout: options.probeTimeout ?? 5000,
|
|
813
836
|
cwd: resolvedCwd,
|
|
814
837
|
env,
|
|
815
|
-
|
|
838
|
+
input: "",
|
|
839
|
+
}));
|
|
840
|
+
probeJoined = shared.joined;
|
|
841
|
+
result = await shared.promise;
|
|
816
842
|
}
|
|
817
843
|
finally {
|
|
818
844
|
hostStallMs = stallSampler.stop();
|
|
819
845
|
}
|
|
820
846
|
const elapsedMs = Date.now() - startedAt;
|
|
847
|
+
recordAvailabilityProbeOverrun(command, resolvedCwd, elapsedMs, options.probeTimeout ?? 5000, result.failure);
|
|
821
848
|
if (!result.error && result.status === 0) {
|
|
822
849
|
cache.command = cmd;
|
|
823
850
|
noteDecision(cache, resolvedCwd, {
|
|
@@ -826,7 +853,7 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
826
853
|
cause: "ok",
|
|
827
854
|
elapsedMs,
|
|
828
855
|
hostStallMs,
|
|
829
|
-
classifiedBy: "probe",
|
|
856
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
830
857
|
evidence: describeProbeEvidence(result),
|
|
831
858
|
});
|
|
832
859
|
return true;
|
|
@@ -846,7 +873,7 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
846
873
|
cause,
|
|
847
874
|
elapsedMs,
|
|
848
875
|
hostStallMs,
|
|
849
|
-
classifiedBy: "probe",
|
|
876
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
850
877
|
evidence,
|
|
851
878
|
});
|
|
852
879
|
return false;
|
|
@@ -885,6 +912,9 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
885
912
|
}
|
|
886
913
|
return { isAvailableAsync, getCommand, getOutcome, getVerdict, reset };
|
|
887
914
|
}
|
|
915
|
+
const cwdProbeFlights = createAvailabilityProbeFlight({
|
|
916
|
+
generation: getDispatchAvailabilityGeneration,
|
|
917
|
+
});
|
|
888
918
|
/**
|
|
889
919
|
* Per-cwd cached availability probe for spawn signatures that don't fit
|
|
890
920
|
* `createAvailabilityChecker` — multi-arg subcommands like `cargo clippy
|
|
@@ -957,7 +987,7 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
957
987
|
...(retryAfterMs !== undefined && { retryAfterMs }),
|
|
958
988
|
...(options.budgetMs !== undefined && { budgetMs: options.budgetMs }),
|
|
959
989
|
// Every verdict here is derived from the probe this seam just ran.
|
|
960
|
-
classifiedBy: "probe",
|
|
990
|
+
classifiedBy: verdict.classifiedBy ?? "probe",
|
|
961
991
|
...(verdict.evidence !== undefined && { evidence: verdict.evidence }),
|
|
962
992
|
}, key);
|
|
963
993
|
}
|
|
@@ -981,20 +1011,24 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
981
1011
|
const startedAt = Date.now();
|
|
982
1012
|
let result;
|
|
983
1013
|
let thrown;
|
|
1014
|
+
let probeJoined = false;
|
|
984
1015
|
try {
|
|
985
|
-
|
|
1016
|
+
const shared = cwdProbeFlights.run(`cwd:${options.tool}|${options.flightKeyComponent ?? ""}|${key}`, () => probe(key));
|
|
1017
|
+
probeJoined = shared.joined;
|
|
1018
|
+
result = await shared.promise;
|
|
986
1019
|
}
|
|
987
1020
|
catch (error) {
|
|
988
1021
|
thrown = error;
|
|
989
1022
|
}
|
|
990
1023
|
const hostStallMs = stallSampler.stop();
|
|
991
1024
|
const elapsedMs = Date.now() - startedAt;
|
|
992
|
-
// A probe that threw carries its errno in the Error, which is exactly
|
|
993
|
-
// what the taxonomy reads — so a thrown EAGAIN stays transient instead
|
|
994
|
-
// of collapsing into an untyped `false`.
|
|
995
1025
|
const shape = result ?? {
|
|
996
1026
|
error: thrown instanceof Error ? thrown : new Error(String(thrown)),
|
|
997
1027
|
};
|
|
1028
|
+
recordAvailabilityProbeOverrun(options.tool, key, elapsedMs, options.budgetMs, shape.failure);
|
|
1029
|
+
// A probe that threw carries its errno in the Error, which is exactly
|
|
1030
|
+
// what the taxonomy reads — so a thrown EAGAIN stays transient instead
|
|
1031
|
+
// of collapsing into an untyped `false`.
|
|
998
1032
|
if (result && !result.error && result.status === 0) {
|
|
999
1033
|
note(latch, key, {
|
|
1000
1034
|
available: true,
|
|
@@ -1002,6 +1036,7 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
1002
1036
|
cause: "ok",
|
|
1003
1037
|
elapsedMs,
|
|
1004
1038
|
hostStallMs,
|
|
1039
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
1005
1040
|
evidence: describeProbeEvidence(result, options.tool),
|
|
1006
1041
|
});
|
|
1007
1042
|
return true;
|
|
@@ -1017,6 +1052,7 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
1017
1052
|
cause,
|
|
1018
1053
|
elapsedMs,
|
|
1019
1054
|
hostStallMs,
|
|
1055
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
1020
1056
|
evidence,
|
|
1021
1057
|
});
|
|
1022
1058
|
return false;
|
|
@@ -1108,6 +1144,7 @@ async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--ve
|
|
|
1108
1144
|
const versionCheck = await safeSpawnAsync(command, versionArgs, {
|
|
1109
1145
|
timeout,
|
|
1110
1146
|
cwd,
|
|
1147
|
+
input: "",
|
|
1111
1148
|
});
|
|
1112
1149
|
if (!versionCheck.error && versionCheck.status === 0) {
|
|
1113
1150
|
return command;
|
|
@@ -1141,7 +1178,7 @@ async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--ve
|
|
|
1141
1178
|
return null;
|
|
1142
1179
|
}
|
|
1143
1180
|
export async function resolveCommandArgsWithInstallFallback(command, toolId, cwd, versionArgs = ["--version"], timeout = 5000) {
|
|
1144
|
-
const versionCheck = await safeSpawnAsync(command.cmd, [...command.args, ...versionArgs], { timeout, cwd });
|
|
1181
|
+
const versionCheck = await safeSpawnAsync(command.cmd, [...command.args, ...versionArgs], { timeout, cwd, input: "" });
|
|
1145
1182
|
if (!versionCheck.error && versionCheck.status === 0) {
|
|
1146
1183
|
return command;
|
|
1147
1184
|
}
|
|
@@ -1353,7 +1390,7 @@ export async function isSgAvailableAsync() {
|
|
|
1353
1390
|
return memo;
|
|
1354
1391
|
if (sgAvailableInFlight)
|
|
1355
1392
|
return sgAvailableInFlight;
|
|
1356
|
-
|
|
1393
|
+
const flight = (async () => {
|
|
1357
1394
|
const startedAt = Date.now();
|
|
1358
1395
|
sgSweepSawTransient = false;
|
|
1359
1396
|
sgSweepTransientCause = "probe-timeout";
|
|
@@ -1415,9 +1452,19 @@ export async function isSgAvailableAsync() {
|
|
|
1415
1452
|
noteSgUnavailable(startedAt, sgSweepSawTransient ? "transient" : "missing", sgSweepSawTransient ? sgSweepTransientCause : "not-found");
|
|
1416
1453
|
return false;
|
|
1417
1454
|
})().finally(() => {
|
|
1418
|
-
|
|
1455
|
+
// Identity-guarded release (#1968's pattern): clear the slot only if THIS
|
|
1456
|
+
// flight still owns it. `ensureCurrentSgGeneration()` above nulls the slot
|
|
1457
|
+
// on a generation bump (a session reset arriving mid-flight) so the NEXT
|
|
1458
|
+
// caller starts a fresh flight rather than sharing this one's stale-generation
|
|
1459
|
+
// answer — that fresh flight is a live successor. Without the guard, this
|
|
1460
|
+
// flight's own late settlement would stomp the slot back to null out from
|
|
1461
|
+
// under that successor, and the caller after it would start a redundant
|
|
1462
|
+
// THIRD probe instead of sharing the still-running second one.
|
|
1463
|
+
if (sgAvailableInFlight === flight)
|
|
1464
|
+
sgAvailableInFlight = null;
|
|
1419
1465
|
});
|
|
1420
|
-
|
|
1466
|
+
sgAvailableInFlight = flight;
|
|
1467
|
+
return flight;
|
|
1421
1468
|
}
|
|
1422
1469
|
/**
|
|
1423
1470
|
* Record a successful shared-ast-grep sweep, with one decision record.
|
|
@@ -1449,6 +1496,7 @@ function noteSgAvailable(startedAt, opts = {}) {
|
|
|
1449
1496
|
cause: provisional ? sgSweepTransientCause : "ok",
|
|
1450
1497
|
elapsedMs: Date.now() - startedAt,
|
|
1451
1498
|
latched: !provisional,
|
|
1499
|
+
classifiedBy: "probe",
|
|
1452
1500
|
hostStallMs: sgSweepHostStallMs,
|
|
1453
1501
|
budgetMs: 5000,
|
|
1454
1502
|
...(provisional && {
|
|
@@ -1472,6 +1520,9 @@ function noteSgUnavailable(startedAt, outcome, cause) {
|
|
|
1472
1520
|
hostStallMs: sgSweepHostStallMs,
|
|
1473
1521
|
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
1474
1522
|
budgetMs: 5000,
|
|
1523
|
+
// Sibling of noteSgAvailable's "probe": both report the same
|
|
1524
|
+
// candidate sweep this function ran (#2209).
|
|
1525
|
+
classifiedBy: "probe",
|
|
1475
1526
|
});
|
|
1476
1527
|
}
|
|
1477
1528
|
export function getSgCommand() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LEDGER_FIELD_MAX } from "../../../ledger-bounds.js";
|
|
2
|
+
import { truncatedByOutputCap } from "../../../spawn-output-cap.js";
|
|
2
3
|
/**
|
|
3
4
|
* The first non-empty line of a tool's output, bounded.
|
|
4
5
|
*
|
|
@@ -58,6 +59,9 @@ export function classifyRunOutcome(input) {
|
|
|
58
59
|
firstOutputLine: firstOutputLine(result.stderr) ||
|
|
59
60
|
firstOutputLine(result.stdout) ||
|
|
60
61
|
firstOutputLine(result.error?.message),
|
|
62
|
+
// Evidence carried alongside the verdict, never part of deciding it — the
|
|
63
|
+
// branches below are byte-for-byte the pre-#2100 rules.
|
|
64
|
+
outputCapped: truncatedByOutputCap(result),
|
|
61
65
|
};
|
|
62
66
|
if (result.error || signal)
|
|
63
67
|
return { kind: "did-not-run", ...base };
|
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
import { incrementDegradationCount } from "../../../degradation-ledger.js";
|
|
6
6
|
import { truncateForLedger } from "../../../ledger-bounds.js";
|
|
7
7
|
import { classifyRunOutcome, firstOutputLine, } from "./spawn-outcome.js";
|
|
8
|
+
/**
|
|
9
|
+
* What ended this run, as one clause, most specific cause first.
|
|
10
|
+
*
|
|
11
|
+
* A signal is a stronger explanation than the exit status, so it displaces a
|
|
12
|
+
* `null` one rather than sitting beside it. Our own output cap is stronger
|
|
13
|
+
* still: the signal it killed with is a detail of OUR decision, and naming that
|
|
14
|
+
* signal alone reads as the tool misbehaving (#2100).
|
|
15
|
+
*/
|
|
16
|
+
function describeEnding(input) {
|
|
17
|
+
if (input.outputCapped)
|
|
18
|
+
return "was stopped at its output cap";
|
|
19
|
+
if (input.signal)
|
|
20
|
+
return `was killed by ${input.signal}`;
|
|
21
|
+
if (input.status === null)
|
|
22
|
+
return "did not run (spawn failure)";
|
|
23
|
+
return `exited ${input.status}`;
|
|
24
|
+
}
|
|
8
25
|
/**
|
|
9
26
|
* One wording for "this tool did not produce a usable result" (#1816).
|
|
10
27
|
*
|
|
@@ -21,13 +38,7 @@ export function formatToolFailure(input) {
|
|
|
21
38
|
const identity = entries.length
|
|
22
39
|
? `${input.tool} (${entries.map(([key, value]) => `${key}=${value}`).join(", ")})`
|
|
23
40
|
: input.tool;
|
|
24
|
-
|
|
25
|
-
// status rather than sitting beside a `null` one.
|
|
26
|
-
const what = input.signal
|
|
27
|
-
? `was killed by ${input.signal}`
|
|
28
|
-
: input.status === null
|
|
29
|
-
? "did not run (spawn failure)"
|
|
30
|
-
: `exited ${input.status}`;
|
|
41
|
+
const what = describeEnding(input);
|
|
31
42
|
const missing = input.reportMissing ? "no report file" : "no output";
|
|
32
43
|
const detail = firstOutputLine(input.stderr) ||
|
|
33
44
|
firstOutputLine(input.stdout) ||
|
|
@@ -44,6 +55,7 @@ export function formatRunOutcomeFailure(tool, outcome, fields) {
|
|
|
44
55
|
tool,
|
|
45
56
|
status: outcome.status,
|
|
46
57
|
signal: outcome.signal,
|
|
58
|
+
outputCapped: outcome.outputCapped,
|
|
47
59
|
stderr: outcome.firstOutputLine,
|
|
48
60
|
reportMissing: outcome.kind === "report-missing",
|
|
49
61
|
fields: { outcome: outcome.kind, ...fields },
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createAvailabilityLatch, logAvailabilityDecision, } from "./availability-policy.js";
|
|
15
15
|
import { probeAvailabilityCandidates } from "./candidate-probe.js";
|
|
16
|
+
import { createAvailabilityProbeFlight } from "../../../availability-probe-flight.js";
|
|
17
|
+
import { createSingleFlight } from "../../../single-flight.js";
|
|
18
|
+
const toolchainProbeFlights = createAvailabilityProbeFlight();
|
|
16
19
|
/**
|
|
17
20
|
* Own one toolchain's availability: sweep the platform candidate list, memoize
|
|
18
21
|
* the path that answered, and park the verdict behind the shared latch so a
|
|
@@ -28,12 +31,13 @@ export function createToolchainAvailability(config) {
|
|
|
28
31
|
let sweepHostStallMs = 0;
|
|
29
32
|
/** What the last classified candidate returned, for the decision record. */
|
|
30
33
|
let sweepEvidence;
|
|
31
|
-
|
|
34
|
+
const ensureFlight = createSingleFlight();
|
|
32
35
|
async function findPath() {
|
|
33
36
|
if (toolPath)
|
|
34
37
|
return toolPath;
|
|
35
38
|
const paths = process.platform === "win32" ? config.windowsPaths : config.unixPaths;
|
|
36
|
-
const
|
|
39
|
+
const shared = toolchainProbeFlights.run(`toolchain:${config.tool}|${config.probeArgs.join("|")}|${config.windowsPaths.join("|")}|${config.unixPaths.join("|")}`, () => probeAvailabilityCandidates(paths, config.probeArgs, config.budgetMs));
|
|
40
|
+
const sweep = await shared.promise;
|
|
37
41
|
sweepSawTransient = sweep.sawTransient;
|
|
38
42
|
sweepTransientCause = sweep.transientCause;
|
|
39
43
|
sweepHostStallMs = sweep.hostStallMs;
|
|
@@ -90,17 +94,7 @@ export function createToolchainAvailability(config) {
|
|
|
90
94
|
const memo = availabilityLatch.read();
|
|
91
95
|
if (memo !== null)
|
|
92
96
|
return memo;
|
|
93
|
-
|
|
94
|
-
// cooldown must share ONE sweep rather than each spawning their own.
|
|
95
|
-
if (ensureInFlight)
|
|
96
|
-
return ensureInFlight;
|
|
97
|
-
ensureInFlight = resolveAvailability();
|
|
98
|
-
try {
|
|
99
|
-
return await ensureInFlight;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
ensureInFlight = null;
|
|
103
|
-
}
|
|
97
|
+
return ensureFlight.run("availability", resolveAvailability);
|
|
104
98
|
}
|
|
105
99
|
return { findPath, isAvailable };
|
|
106
100
|
}
|
|
@@ -16,6 +16,7 @@ import * as fs from "node:fs";
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import yaml from "../../deps/js-yaml.js";
|
|
18
18
|
import { BoundedLruCache } from "../../bounded-cache.js";
|
|
19
|
+
import { compareOrdinal } from "../../string-utils.js";
|
|
19
20
|
// --- Constants ---
|
|
20
21
|
/** Overly broad patterns that match everything (cause false positive explosions) */
|
|
21
22
|
export const OVERLY_BROAD_PATTERNS = [
|
|
@@ -28,18 +29,22 @@ export const OVERLY_BROAD_PATTERNS = [
|
|
|
28
29
|
];
|
|
29
30
|
/** Maximum complexity score for rules in blockingOnly mode */
|
|
30
31
|
export const MAX_BLOCKING_RULE_COMPLEXITY = 8;
|
|
32
|
+
/**
|
|
33
|
+
* `getCachedRules` re-stats a rule directory's files at most once per this
|
|
34
|
+
* window (#2262 round 2). `scanner.ts:235` calls `loadYamlRules` per file
|
|
35
|
+
* scanned; a full metadata sweep (readdir + stat every rule file) on every
|
|
36
|
+
* warmed call measured 250x master's per-call cost on a mid-size catalog.
|
|
37
|
+
* Same value and same shape as `file-utils.ts`'s
|
|
38
|
+
* `PROJECT_IGNORE_FRESHNESS_CADENCE_MS` (#2159/#2071): 2s is the accepted
|
|
39
|
+
* staleness bound for a rule tree pi-lens itself did not just write.
|
|
40
|
+
*/
|
|
41
|
+
export const RULES_CACHE_FRESHNESS_CADENCE_MS = 2_000;
|
|
31
42
|
// --- Caches ---
|
|
32
43
|
const rulesCache = new BoundedLruCache(64);
|
|
33
44
|
const blockingRulesCache = new BoundedLruCache(64);
|
|
34
45
|
const contentRulesCache = new BoundedLruCache(64);
|
|
35
46
|
const contentBlockingRulesCache = new BoundedLruCache(64);
|
|
36
47
|
// --- Public API ---
|
|
37
|
-
export function clearRulesCache() {
|
|
38
|
-
rulesCache.clear();
|
|
39
|
-
blockingRulesCache.clear();
|
|
40
|
-
contentRulesCache.clear();
|
|
41
|
-
contentBlockingRulesCache.clear();
|
|
42
|
-
}
|
|
43
48
|
export function loadYamlRules(ruleDir, severityFilter) {
|
|
44
49
|
return getCachedRules(ruleDir, severityFilter);
|
|
45
50
|
}
|
|
@@ -48,7 +53,14 @@ function findYamlRuleFiles(ruleDir) {
|
|
|
48
53
|
try {
|
|
49
54
|
entries = fs
|
|
50
55
|
.readdirSync(ruleDir, { withFileTypes: true })
|
|
51
|
-
|
|
56
|
+
// Ordinal (code-unit), not locale, comparison: this order feeds the
|
|
57
|
+
// index-aligned file-stamp identity compared in `getCachedRules` and
|
|
58
|
+
// the hash input order in `loadYamlRulesFresh`. `localeCompare` can
|
|
59
|
+
// order the same file set differently across locales/processes,
|
|
60
|
+
// which would desync the two and mint spurious cache misses or
|
|
61
|
+
// (worse) content-hash collisions across differently-ordered runs.
|
|
62
|
+
// Same reasoning as `rule-cache.ts`'s `computeRuleHash` (#2155/#2165).
|
|
63
|
+
.sort((a, b) => compareOrdinal(a.name, b.name));
|
|
52
64
|
}
|
|
53
65
|
catch {
|
|
54
66
|
return [];
|
|
@@ -118,20 +130,42 @@ export function getCachedRules(ruleDir, severityFilter) {
|
|
|
118
130
|
if (!fs.existsSync(ruleDir)) {
|
|
119
131
|
return [];
|
|
120
132
|
}
|
|
121
|
-
let currentMtime = 0;
|
|
122
|
-
try {
|
|
123
|
-
currentMtime = fs.statSync(ruleDir).mtimeMs;
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
133
|
const cache = severityFilter === "error" ? blockingRulesCache : rulesCache;
|
|
129
134
|
const cached = cache.get(ruleDir);
|
|
130
|
-
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if (cached && now - cached.checkedAtMs < RULES_CACHE_FRESHNESS_CADENCE_MS) {
|
|
137
|
+
// Inside the cadence window: no stat sweep at all. `getCachedRules` is
|
|
138
|
+
// the bundled-catalog path only (project rules route through
|
|
139
|
+
// `loadYamlRulesFresh` — see `ast-grep-napi.ts`'s route split), and
|
|
140
|
+
// `scanner.ts:235` calls it once per scanned file, so a per-call
|
|
141
|
+
// `readdirSync`+`statSync*N` sweep is the hot-path cost this window
|
|
142
|
+
// exists to remove.
|
|
131
143
|
return cached.rules;
|
|
132
144
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
145
|
+
const files = findYamlRuleFiles(ruleDir);
|
|
146
|
+
const fileStamps = files.flatMap((file) => {
|
|
147
|
+
try {
|
|
148
|
+
const stat = fs.statSync(file);
|
|
149
|
+
return [{ path: file, mtimeMs: stat.mtimeMs, size: stat.size }];
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const metadataMatches = cached?.files.length === fileStamps.length &&
|
|
156
|
+
cached.files.every((file, index) => file.path === fileStamps[index].path &&
|
|
157
|
+
file.mtimeMs === fileStamps[index].mtimeMs &&
|
|
158
|
+
file.size === fileStamps[index].size);
|
|
159
|
+
if (cached && metadataMatches) {
|
|
160
|
+
// Nothing drifted. Re-arm the window from *now* regardless — otherwise
|
|
161
|
+
// the next call re-stats immediately instead of waiting a full cadence
|
|
162
|
+
// (the #2260/#2071 "checkedAtMs must re-arm on every check, not only on
|
|
163
|
+
// a miss" lesson).
|
|
164
|
+
cached.checkedAtMs = now;
|
|
165
|
+
return cached.rules;
|
|
166
|
+
}
|
|
167
|
+
const rules = loadYamlRuleFiles(files, severityFilter);
|
|
168
|
+
cache.set(ruleDir, { rules, files: fileStamps, checkedAtMs: now });
|
|
135
169
|
return rules;
|
|
136
170
|
}
|
|
137
171
|
export function isOverlyBroadPattern(pattern) {
|
|
@@ -82,6 +82,66 @@ export function startEventLoopMonitor(resolutionMs = 20) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Below this, a block is not classified at all (`below-floor`). #1980 mined
|
|
87
|
+
* the 5 s+ tier; 1 s is a deliberately conservative floor that still covers
|
|
88
|
+
* every block that tier cares about while keeping sub-second jitter — where
|
|
89
|
+
* measurement slop rivals the signal — out of the verdict.
|
|
90
|
+
*/
|
|
91
|
+
export const STALL_CLASSIFY_FLOOR_MS = 1000;
|
|
92
|
+
/**
|
|
93
|
+
* Slack allowed to the CPU budget before a block counts as unaccounted.
|
|
94
|
+
* `process.cpuUsage()` and the histogram are sampled at slightly different
|
|
95
|
+
* instants and the histogram quantizes, so a block whose CPU coverage is
|
|
96
|
+
* within this much of exact is called `cpu-accounted`, not a stall.
|
|
97
|
+
*/
|
|
98
|
+
export const STALL_CLASSIFY_SLOP_MS = 250;
|
|
99
|
+
/**
|
|
100
|
+
* Split a block into compute-vs-stall from the CPU it could possibly have
|
|
101
|
+
* burned (#1980). Pure, so the discrimination is testable without the native
|
|
102
|
+
* histogram or a real machine stall — the same reason
|
|
103
|
+
* {@link isSuspendSuspectedBlock} is pure.
|
|
104
|
+
*
|
|
105
|
+
* ## Why this is sound in exactly one direction
|
|
106
|
+
*
|
|
107
|
+
* `windowCpuMs` is the CPU the WHOLE process consumed across the WHOLE window
|
|
108
|
+
* — every thread, every phase, not just this block. So it is an UPPER BOUND on
|
|
109
|
+
* the CPU this one block could have burned. A synchronous compute block of D ms
|
|
110
|
+
* needs ≈ D ms of main-thread CPU. Therefore:
|
|
111
|
+
*
|
|
112
|
+
* - `windowCpuMs + slop < maxMs` PROVES the block was not compute. Even if the
|
|
113
|
+
* block had been the only thing running, there was not enough CPU in the
|
|
114
|
+
* whole window to cover it. This is the `non-cpu-stall` verdict and it is a
|
|
115
|
+
* real inference, not a heuristic.
|
|
116
|
+
* - The converse proves nothing. A window can burn 30 s of CPU in other phases
|
|
117
|
+
* and still contain a 5 s I/O park. That is why the label is
|
|
118
|
+
* `cpu-accounted` ("the budget covers it") rather than "compute": it reports
|
|
119
|
+
* the absence of proof, not the presence of compute.
|
|
120
|
+
*
|
|
121
|
+
* The existing 20 s suspend verdict is not re-derived here; this delegates to
|
|
122
|
+
* {@link isSuspendSuspectedBlock} so there is one definition of a suspend, and
|
|
123
|
+
* `system-stall` is checked BEFORE `non-cpu-stall` because a suspend is the
|
|
124
|
+
* strictly more specific case of the same evidence.
|
|
125
|
+
*
|
|
126
|
+
* ## Known ambiguity, carried forward not silenced
|
|
127
|
+
*
|
|
128
|
+
* `isSuspendSuspectedBlock`'s own doc records that a >20 s synchronous syscall
|
|
129
|
+
* stall is CPU-indistinguishable from a suspend. Nothing here fixes that. What
|
|
130
|
+
* this adds is the tier BELOW that floor: a 5-20 s non-CPU block, which used
|
|
131
|
+
* to read as ordinary compute and now reads as `non-cpu-stall` with the
|
|
132
|
+
* `inFlightPhase` attribution beside it.
|
|
133
|
+
*/
|
|
134
|
+
export function classifyLoopBlock(maxMs, windowCpuMs, options = {}) {
|
|
135
|
+
const { floorMs = STALL_CLASSIFY_FLOOR_MS, slopMs = STALL_CLASSIFY_SLOP_MS, suspendFloorMs, suspendSlopMs, } = options;
|
|
136
|
+
const cpuCoverageRatio = maxMs > 0 ? Math.round((windowCpuMs / maxMs) * 100) / 100 : undefined;
|
|
137
|
+
if (maxMs < floorMs)
|
|
138
|
+
return { stallClass: "below-floor", cpuCoverageRatio };
|
|
139
|
+
if (isSuspendSuspectedBlock(maxMs, windowCpuMs, suspendFloorMs, suspendSlopMs))
|
|
140
|
+
return { stallClass: "system-stall", cpuCoverageRatio };
|
|
141
|
+
if (windowCpuMs + slopMs < maxMs)
|
|
142
|
+
return { stallClass: "non-cpu-stall", cpuCoverageRatio };
|
|
143
|
+
return { stallClass: "cpu-accounted", cpuCoverageRatio };
|
|
144
|
+
}
|
|
85
145
|
const safeMs = (ns) => Number.isFinite(ns) ? Math.round((ns / NS_PER_MS) * 10) / 10 : 0;
|
|
86
146
|
/**
|
|
87
147
|
* Classify a worst-block sample as genuine CPU work or a suspend/freeze
|
|
@@ -123,13 +183,18 @@ export function getEventLoopStats() {
|
|
|
123
183
|
const maxMs = safeMs(histogram.max);
|
|
124
184
|
const windowWallMs = Math.max(0, Date.now() - windowStartWallMs);
|
|
125
185
|
const windowCpuMs = Math.max(0, cpuTotalMs() - windowStartCpuMs);
|
|
186
|
+
// One classification pass feeds both fields, so `suspectSystemStall` and
|
|
187
|
+
// `stallClass` can never disagree about the same sample (#1980).
|
|
188
|
+
const classification = classifyLoopBlock(maxMs, windowCpuMs);
|
|
126
189
|
return {
|
|
127
190
|
maxMs,
|
|
128
191
|
p99Ms: safeMs(histogram.percentile(99)),
|
|
129
192
|
meanMs: safeMs(histogram.mean),
|
|
130
193
|
windowWallMs: Math.round(windowWallMs),
|
|
131
194
|
windowCpuMs: Math.round(windowCpuMs),
|
|
132
|
-
suspectSystemStall:
|
|
195
|
+
suspectSystemStall: classification.stallClass === "system-stall",
|
|
196
|
+
stallClass: classification.stallClass,
|
|
197
|
+
cpuCoverageRatio: classification.cpuCoverageRatio,
|
|
133
198
|
};
|
|
134
199
|
}
|
|
135
200
|
/**
|