@herbertgao/pi-extensions 2026.8.5 → 2026.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +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 +272 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -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 +16 -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 +22 -15
- 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 +34 -10
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +29 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +46 -10
- 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 +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -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 +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
- package/node_modules/@narumitw/pi-btw/README.md +119 -103
- 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 +13 -9
- package/node_modules/@narumitw/pi-btw/src/btw.ts +40 -42
- 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 +78 -15
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/pi-lens/CHANGELOG.md +1004 -0
- package/node_modules/pi-lens/README.md +11 -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 +427 -0
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +79 -16
- 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 +285 -8
- package/node_modules/pi-lens/dist/clients/biome-client.js +176 -37
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -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 +192 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +76 -2
- 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/console-guard-install.js +16 -4
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +249 -52
- 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 +62 -13
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +43 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +153 -37
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +165 -11
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +325 -72
- 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/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/helm-render.js +895 -0
- 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 +81 -41
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
- 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 +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
- 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 +488 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +84 -0
- 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 +943 -93
- 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 +72 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +106 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +317 -3
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
- 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 +49 -21
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +219 -10
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +9 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +1119 -101
- 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 +86 -15
- package/node_modules/pi-lens/dist/clients/knip-client.js +361 -24
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +487 -9
- 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-events.js +56 -25
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +160 -20
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1666 -148
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +111 -0
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2496 -171
- 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 +517 -69
- 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 +172 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +61 -3
- 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/analyze.js +4 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +39 -18
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
- 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 +178 -30
- 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 +85 -15
- 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/runner-findings.js +23 -2
- 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 +5 -0
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- 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 +1 -1
- package/node_modules/pi-lens/dist/clients/read-guard.js +184 -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/query.js +76 -4
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +248 -13
- package/node_modules/pi-lens/dist/clients/runtime-context.js +112 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +472 -61
- package/node_modules/pi-lens/dist/clients/runtime-session.js +359 -13
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +269 -22
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +948 -87
- package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
- 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 +224 -8
- package/node_modules/pi-lens/dist/clients/sg-runner.js +249 -37
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-timing.js +26 -2
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +574 -32
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +127 -20
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- 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-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/warm-attach.js +17 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +64677 -49484
- package/node_modules/pi-lens/dist/mcp/server.js +8 -2
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +469 -56
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +190 -46
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +44 -14
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +518 -501
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +33 -6
- package/node_modules/pi-lens/docs/globalconfig.md +14 -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 +25 -1
- package/node_modules/pi-lens/docs/usage.md +24 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +10 -6
- 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 +22 -0
- 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-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-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 +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +9 -0
- 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-laundering-test.yml +11 -0
- 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 +66 -0
- 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-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-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 +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +42 -0
- 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 +32 -0
- 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 +132 -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/skills/pi-lens-ast-grep/SKILL.md +12 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +87 -116
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +170 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +49 -0
- package/node_modules/pi-mcp-adapter/README.md +51 -6
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/cli.js +188 -5
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +28 -10
- package/node_modules/pi-mcp-adapter/direct-tools.ts +23 -19
- 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/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +46 -25
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +4 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +330 -0
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +145 -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-metadata.ts +38 -18
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +95 -10
- 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 +38 -14
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +8 -3
- package/node_modules/pi-web-access/curator-server.ts +5 -1
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +134 -37
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +33 -7
- package/node_modules/pi-web-access/index.ts +160 -104
- package/node_modules/pi-web-access/package.json +2 -2
- 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/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +51 -23
- 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/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +13 -13
|
@@ -60,13 +60,24 @@ export function unrefChildAndPipes(child) {
|
|
|
60
60
|
* here already has).
|
|
61
61
|
*/
|
|
62
62
|
export function spawnCollectStdout(command, args, options) {
|
|
63
|
+
return spawnCollectStdoutResult(command, args, options).then((result) => result.stdout);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `spawnCollectStdout` plus the reason the output is what it is. Same spawn,
|
|
67
|
+
* unref, collect, settle plumbing; the only additions are an optional hard
|
|
68
|
+
* timeout and a status discriminator. Never rejects.
|
|
69
|
+
*/
|
|
70
|
+
export function spawnCollectStdoutResult(command, args, options, collectOptions = {}) {
|
|
63
71
|
return new Promise((resolve) => {
|
|
64
72
|
let settled = false;
|
|
65
|
-
|
|
73
|
+
let timer;
|
|
74
|
+
const settle = (result) => {
|
|
66
75
|
if (settled)
|
|
67
76
|
return;
|
|
68
77
|
settled = true;
|
|
69
|
-
|
|
78
|
+
if (timer)
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve(result);
|
|
70
81
|
};
|
|
71
82
|
try {
|
|
72
83
|
const child = nodeSpawn(command, args, options);
|
|
@@ -75,11 +86,48 @@ export function spawnCollectStdout(command, args, options) {
|
|
|
75
86
|
child.stdout?.on("data", (chunk) => {
|
|
76
87
|
out += chunk.toString();
|
|
77
88
|
});
|
|
78
|
-
child.once("error", () => settle(""));
|
|
79
|
-
child.once("close", () => settle(
|
|
89
|
+
child.once("error", (error) => settle({ stdout: "", status: "spawn-error", error }));
|
|
90
|
+
child.once("close", (code, signal) => settle(code === 0 && signal === null
|
|
91
|
+
? { stdout: out, status: "ok" }
|
|
92
|
+
: {
|
|
93
|
+
stdout: "",
|
|
94
|
+
status: "exit-error",
|
|
95
|
+
exitCode: code,
|
|
96
|
+
exitSignal: signal,
|
|
97
|
+
}));
|
|
98
|
+
const timeoutMs = collectOptions.timeoutMs;
|
|
99
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
// Settle only AFTER the child's fate is known. Killing and
|
|
102
|
+
// resolving in the same tick bounds the caller and abandons the
|
|
103
|
+
// child, which is the leak this timeout exists to prevent.
|
|
104
|
+
void terminateTimedOutChild(child, collectOptions.onTimeout).then((timeoutKill) => settle({ stdout: out, status: "timeout", timeoutKill }));
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
timer.unref();
|
|
107
|
+
}
|
|
80
108
|
}
|
|
81
|
-
catch {
|
|
82
|
-
settle("");
|
|
109
|
+
catch (error) {
|
|
110
|
+
settle({ stdout: "", status: "spawn-error", error });
|
|
83
111
|
}
|
|
84
112
|
});
|
|
85
113
|
}
|
|
114
|
+
/** Run the caller's termination handler, or the bare-signal default, and
|
|
115
|
+
* never let either throw out of the timeout path. */
|
|
116
|
+
async function terminateTimedOutChild(child, onTimeout) {
|
|
117
|
+
if (onTimeout) {
|
|
118
|
+
try {
|
|
119
|
+
return await onTimeout(child);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// A handler that threw did not verify anything.
|
|
123
|
+
return "unverified";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
child.kill();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// best-effort — the child may already be gone
|
|
131
|
+
}
|
|
132
|
+
return "unverified";
|
|
133
|
+
}
|
|
@@ -1,29 +1,47 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
5
4
|
import { getProjectDataDir } from "./file-utils.js";
|
|
5
|
+
import { normalizeMessage, stableFindingId } from "./finding-identity.js";
|
|
6
6
|
import { normalizeMapKey } from "./path-utils.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Report-budget precedence (#1777). A hint is an opinion; a warning is a
|
|
9
|
+
* finding. When the report is capped, warnings must be spent first, or an
|
|
10
|
+
* alphabetically-early file's hints can push every real warning out of the
|
|
11
|
+
* agent's view — the exact "hints dominate the stream" failure the hint tier
|
|
12
|
+
* exists to prevent.
|
|
13
|
+
*/
|
|
14
|
+
const TIER_BUDGET_ORDER = [
|
|
15
|
+
"warning",
|
|
16
|
+
"info",
|
|
17
|
+
"hint",
|
|
18
|
+
];
|
|
19
|
+
/** #1816: was a local `relativeFile|tool|rule|code|normalizedMessage|line`
|
|
20
|
+
* hash (raw, non-canonicalized `relativeFile`, 10-char hash), independently
|
|
21
|
+
* hand-rolled from `actionable-warnings.ts`'s twin and
|
|
22
|
+
* `diagnostic-dispositions.ts`'s canonicalizing original. Now the shared
|
|
23
|
+
* `finding-identity.js` builder (canonicalizes `cwd`/`filePath` through
|
|
24
|
+
* `normalizeMapKey`, hashes to 12 chars). No back-compat migration needed
|
|
25
|
+
* here: `cq:` ids are never a keyed lookup — `buildCodeQualityWarningsReport`
|
|
26
|
+
* regenerates the whole report fresh every turn, callers only ever cache and
|
|
27
|
+
* replay the report wholesale (`tools/lens-diagnostics.ts` reads it via
|
|
28
|
+
* `cacheManager.readCache<CodeQualityWarningsReport>`, never by `id`), and
|
|
29
|
+
* `appendCodeQualityWarningsHistory` just append-only-logs the id for
|
|
30
|
+
* observability, never look it back up. Contrast `actionable-warnings.ts`,
|
|
31
|
+
* whose `actionable-warning-state.json` DOES key a persisted suppression
|
|
32
|
+
* store on `aw:` — that one migrates. */
|
|
17
33
|
function createCodeQualityWarningId(args) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
args.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
return stableFindingId("cq:", {
|
|
35
|
+
cwd: args.cwd,
|
|
36
|
+
filePath: args.filePath,
|
|
37
|
+
parts: [
|
|
38
|
+
args.tool,
|
|
39
|
+
args.rule,
|
|
40
|
+
args.code,
|
|
41
|
+
normalizeMessage(args.message),
|
|
42
|
+
args.line,
|
|
43
|
+
],
|
|
44
|
+
});
|
|
27
45
|
}
|
|
28
46
|
function categorize(diagnostic) {
|
|
29
47
|
const haystack = `${diagnostic.tool} ${diagnostic.rule ?? ""} ${diagnostic.code ?? ""} ${diagnostic.message}`.toLowerCase();
|
|
@@ -93,11 +111,22 @@ export function buildCodeQualityWarningsReport(args) {
|
|
|
93
111
|
continue;
|
|
94
112
|
byId.set(warning.id, warning);
|
|
95
113
|
}
|
|
96
|
-
const
|
|
97
|
-
.sort((a, b) => a.displayPath.localeCompare(b.displayPath) ||
|
|
114
|
+
const byDisplayOrder = (a, b) => a.displayPath.localeCompare(b.displayPath) ||
|
|
98
115
|
(a.line ?? 0) - (b.line ?? 0) ||
|
|
99
|
-
a.message.localeCompare(b.message)
|
|
100
|
-
|
|
116
|
+
a.message.localeCompare(b.message);
|
|
117
|
+
// Spend the budget tier by tier (see TIER_BUDGET_ORDER), then restore
|
|
118
|
+
// display order so the report still reads file by file, line by line.
|
|
119
|
+
const candidates = [...byId.values()].sort(byDisplayOrder);
|
|
120
|
+
const merged = [];
|
|
121
|
+
for (const tier of TIER_BUDGET_ORDER) {
|
|
122
|
+
for (const warning of candidates) {
|
|
123
|
+
if (merged.length >= maxWarnings)
|
|
124
|
+
break;
|
|
125
|
+
if (warning.severity === tier)
|
|
126
|
+
merged.push(warning);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
merged.sort(byDisplayOrder);
|
|
101
130
|
const byFile = new Map();
|
|
102
131
|
for (const warning of merged) {
|
|
103
132
|
const arr = byFile.get(warning.filePath) ?? [];
|
|
@@ -131,6 +160,11 @@ export function buildCodeQualityWarningsReport(args) {
|
|
|
131
160
|
summary: {
|
|
132
161
|
warnings: merged.length,
|
|
133
162
|
files: files.length,
|
|
163
|
+
byTier: {
|
|
164
|
+
warning: merged.filter((w) => w.severity === "warning").length,
|
|
165
|
+
info: merged.filter((w) => w.severity === "info").length,
|
|
166
|
+
hint: merged.filter((w) => w.severity === "hint").length,
|
|
167
|
+
},
|
|
134
168
|
topRules,
|
|
135
169
|
},
|
|
136
170
|
};
|
|
@@ -205,8 +239,26 @@ export function formatCodeQualityWarningsAdvisory(report) {
|
|
|
205
239
|
.slice(0, 3)
|
|
206
240
|
.map((entry) => `${entry.rule}×${entry.count}`)
|
|
207
241
|
.join(", ");
|
|
242
|
+
// #1777: name the tiers, in the same descending-loudness order the on-demand
|
|
243
|
+
// ast-grep surface uses (`formatDiagnostics`, clients/ast-grep-client.ts).
|
|
244
|
+
// A tier with no findings is omitted rather than printed as zero.
|
|
245
|
+
const byTier = report.summary.byTier ?? { warning: 0, info: 0, hint: 0 };
|
|
246
|
+
// The line is worth its space only when a quiet tier is present; an
|
|
247
|
+
// all-warning turn already says everything in the count above.
|
|
248
|
+
const tiers = byTier.hint || byTier.info
|
|
249
|
+
? [
|
|
250
|
+
byTier.warning ? `${byTier.warning} warning` : undefined,
|
|
251
|
+
byTier.info ? `${byTier.info} info` : undefined,
|
|
252
|
+
byTier.hint ? `${byTier.hint} hint` : undefined,
|
|
253
|
+
]
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join(", ")
|
|
256
|
+
: "";
|
|
208
257
|
return [
|
|
209
258
|
`Code-quality warnings introduced/touched this turn: ${report.summary.warnings} across ${report.summary.files} file(s).`,
|
|
259
|
+
tiers
|
|
260
|
+
? `By tier: ${tiers}. Hint and info are style opinions, not defects.`
|
|
261
|
+
: undefined,
|
|
210
262
|
topRules ? `Top rules: ${topRules}` : undefined,
|
|
211
263
|
"Details written to .pi-lens/cache/code-quality-warnings.json",
|
|
212
264
|
"No action required unless you are already refactoring these areas.",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { performance } from "node:perf_hooks";
|
|
2
1
|
// Capture this before the guard's other module initialization so host boot is
|
|
3
|
-
// not charged to extension evaluation (#1374).
|
|
4
|
-
|
|
2
|
+
// not charged to extension evaluation (#1374). Lives in its own
|
|
3
|
+
// side-effect-free module (#1434 S3d) — see eval-timestamp.ts's docstring for
|
|
4
|
+
// why: `startup-timing.ts` needs this constant but must never trigger a
|
|
5
|
+
// console-guard install merely by importing it.
|
|
6
|
+
import { PI_LENS_EVAL_STARTED_MS } from "./eval-timestamp.js";
|
|
5
7
|
// #1333: install the console guard as an import side-effect so it runs before
|
|
6
8
|
// any other module's initialization code. Installing inside the extension
|
|
7
9
|
// factory is too late — by the time the factory runs, every static/transitive
|
|
@@ -10,5 +12,15 @@ export const PI_LENS_EVAL_STARTED_MS = performance.now();
|
|
|
10
12
|
// MUST remain the first import of index.ts; the enforcement test pins that.
|
|
11
13
|
// installConsoleGuard() itself is idempotent and no-ops under test mode and
|
|
12
14
|
// PI_LENS_CONSOLE_GUARD=0.
|
|
13
|
-
|
|
15
|
+
// #1434: the guard captures only while pi-lens owns execution, so the module
|
|
16
|
+
// window must be open for the rest of the import graph. index.ts closes it on
|
|
17
|
+
// its last line; an unref'd backstop closes it if that never runs.
|
|
18
|
+
import { installConsoleGuard, openModuleLoadConsoleWindow, } from "./extension-log.js";
|
|
14
19
|
installConsoleGuard();
|
|
20
|
+
openModuleLoadConsoleWindow();
|
|
21
|
+
// Not re-exported: nothing should import the timestamp from this module.
|
|
22
|
+
// `clients/startup-timing.ts` imports it straight from `eval-timestamp.ts`,
|
|
23
|
+
// which is what keeps that import side-effect-free (S3d). The `void` keeps
|
|
24
|
+
// the import from looking unused to lint while documenting that this module
|
|
25
|
+
// only needs the ordering (evaluate eval-timestamp.ts first), not the value.
|
|
26
|
+
void PI_LENS_EVAL_STARTED_MS;
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
* implementing DeadCodeClient and adding to getDeadCodeClients().
|
|
13
13
|
*/
|
|
14
14
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
15
|
+
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
16
|
+
import * as fs from "node:fs";
|
|
15
17
|
import * as path from "node:path";
|
|
16
18
|
import { findNearestMarkerRoot } from "./path-utils.js";
|
|
19
|
+
import { getScratchTreeFnmatchPatterns } from "./scratch-tree-policy.js";
|
|
17
20
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
21
|
+
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
22
|
+
import { firstOutputLine, spawnFailedWithNoOutput, } from "./dispatch/runners/utils/spawn-outcome.js";
|
|
23
|
+
import { formatToolFailure } from "./dispatch/runners/utils/tool-failure.js";
|
|
18
24
|
function emptyResult(language) {
|
|
19
25
|
return {
|
|
20
26
|
success: false,
|
|
@@ -27,17 +33,33 @@ function emptyResult(language) {
|
|
|
27
33
|
}
|
|
28
34
|
const ANALYSIS_TIMEOUT_MS = 30_000;
|
|
29
35
|
// Directories never worth scanning for the user's own dead code.
|
|
36
|
+
//
|
|
37
|
+
// Single-sourced from `scratch-tree-policy.ts`'s `EXCLUDED_DIRS`-derived list
|
|
38
|
+
// (#1562 sweep finding: this array had hand-drifted from `EXCLUDED_DIRS` —
|
|
39
|
+
// missing `.pi`/`.claude`/`.next`/`.turbo`/etc, the same single-source-of-
|
|
40
|
+
// truth defect #883 named for per-language lists). `site-packages`/`.eggs`
|
|
41
|
+
// are Python-packaging-specific and have no `EXCLUDED_DIRS` counterpart (no
|
|
42
|
+
// other scanner needs them), so they stay as a small local addition rather
|
|
43
|
+
// than polluting the shared cross-tool list.
|
|
44
|
+
const VULTURE_PYTHON_ONLY_EXCLUDES = ["*/site-packages/*", "*/.eggs/*"];
|
|
30
45
|
const VULTURE_EXCLUDES = [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
46
|
+
...getScratchTreeFnmatchPatterns(),
|
|
47
|
+
...VULTURE_PYTHON_ONLY_EXCLUDES,
|
|
48
|
+
];
|
|
49
|
+
// Decorators whose target is called by a framework, never by name in the tree.
|
|
50
|
+
// vulture cannot see those call sites, so every such symbol is a permanent
|
|
51
|
+
// false positive that reappears in every scan — the fastest way to teach a
|
|
52
|
+
// reader to stop reading the advisory. Glob-matched by vulture itself.
|
|
53
|
+
const VULTURE_IGNORE_DECORATORS = [
|
|
54
|
+
"@pytest.fixture",
|
|
55
|
+
"@pytest.fixture(*",
|
|
56
|
+
"@fixture",
|
|
57
|
+
"@fixture(*",
|
|
58
|
+
"@app.*",
|
|
59
|
+
"@router.*",
|
|
60
|
+
"@celery.task*",
|
|
61
|
+
"@task",
|
|
62
|
+
"@shared_task*",
|
|
41
63
|
];
|
|
42
64
|
// vulture line: `path/to/file.py:12: unused function 'foo' (60% confidence)`
|
|
43
65
|
const VULTURE_LINE = /^(.*?):(\d+): unused (\w[\w ]*?) '([^']+)' \((\d+)% confidence\)\s*$/;
|
|
@@ -86,7 +108,12 @@ export function parseVultureOutput(output, root) {
|
|
|
86
108
|
export class PythonDeadCodeClient {
|
|
87
109
|
id = "python";
|
|
88
110
|
language = "Python";
|
|
89
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Transient-aware memo (#1467): only a durable "vulture is not installed"
|
|
113
|
+
* verdict is remembered for the session. A probe that timed out expires and
|
|
114
|
+
* is re-probed, so vulture cannot be disabled for the process by one stall.
|
|
115
|
+
*/
|
|
116
|
+
availabilityLatch = createAvailabilityLatch();
|
|
90
117
|
resolved = null;
|
|
91
118
|
ensureInFlight = null;
|
|
92
119
|
inFlight = new Map();
|
|
@@ -103,6 +130,10 @@ export class PythonDeadCodeClient {
|
|
|
103
130
|
detect(cwd) {
|
|
104
131
|
return this.resolveProjectRoot(cwd) !== null;
|
|
105
132
|
}
|
|
133
|
+
owns(filePath) {
|
|
134
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
135
|
+
return ext === ".py" || ext === ".pyi";
|
|
136
|
+
}
|
|
106
137
|
/**
|
|
107
138
|
* Nearest dir with a Python project marker, never at/above $HOME and never
|
|
108
139
|
* escaping a VCS boundary — same containment rules as KnipClient so a scan
|
|
@@ -120,12 +151,13 @@ export class PythonDeadCodeClient {
|
|
|
120
151
|
"tox.ini",
|
|
121
152
|
], { boundaries: [".git", ".hg", ".svn"], homeDir: homeDirOverride });
|
|
122
153
|
}
|
|
123
|
-
async ensureAvailable() {
|
|
124
|
-
|
|
125
|
-
|
|
154
|
+
async ensureAvailable(root) {
|
|
155
|
+
const memo = this.availabilityLatch.read();
|
|
156
|
+
if (memo !== null)
|
|
157
|
+
return memo;
|
|
126
158
|
if (this.ensureInFlight)
|
|
127
159
|
return this.ensureInFlight;
|
|
128
|
-
this.ensureInFlight = this.doEnsureAvailable();
|
|
160
|
+
this.ensureInFlight = this.doEnsureAvailable(root);
|
|
129
161
|
try {
|
|
130
162
|
return await this.ensureInFlight;
|
|
131
163
|
}
|
|
@@ -133,32 +165,131 @@ export class PythonDeadCodeClient {
|
|
|
133
165
|
this.ensureInFlight = null;
|
|
134
166
|
}
|
|
135
167
|
}
|
|
136
|
-
|
|
168
|
+
/**
|
|
169
|
+
* `<root>/.venv/bin/vulture` (and `venv`/Windows `Scripts` equivalents),
|
|
170
|
+
* checked by existence only — no spawn. A project's own venv is the vulture
|
|
171
|
+
* that project's own config and dependency set were written against; the
|
|
172
|
+
* bare `vulture`/`python -m vulture` PATH probes below only ever see
|
|
173
|
+
* whatever happens to be active in the CALLING shell, not the project's
|
|
174
|
+
* (#1731, discipline B — the same venv-first shape sqlfluff's binary
|
|
175
|
+
* resolution already uses, `runner-helpers.ts` `createVenvFinder`).
|
|
176
|
+
*/
|
|
177
|
+
venvCandidates(root) {
|
|
178
|
+
const isWin = process.platform === "win32";
|
|
179
|
+
const relPaths = isWin
|
|
180
|
+
? [
|
|
181
|
+
path.join(".venv", "Scripts", "vulture.exe"),
|
|
182
|
+
path.join("venv", "Scripts", "vulture.exe"),
|
|
183
|
+
]
|
|
184
|
+
: [path.join(".venv", "bin", "vulture"), path.join("venv", "bin", "vulture")];
|
|
185
|
+
return relPaths
|
|
186
|
+
.map((rel) => path.join(root, rel))
|
|
187
|
+
.filter((full) => fs.existsSync(full))
|
|
188
|
+
.map((full) => ({ cmd: full, prefix: [] }));
|
|
189
|
+
}
|
|
190
|
+
async doEnsureAvailable(root) {
|
|
137
191
|
// Presence-gated, NOT auto-installed. vulture is a pure-Python package
|
|
138
192
|
// with no standalone binary, so "auto-install" would mean `pip install`
|
|
139
193
|
// into whatever Python environment happens to be active — wrong and
|
|
140
194
|
// intrusive for uv / poetry / conda / pipx users. So we only use vulture
|
|
141
|
-
// when the user already has it, probing
|
|
142
|
-
//
|
|
143
|
-
//
|
|
195
|
+
// when the user already has it, probing the project's own venv first
|
|
196
|
+
// (#1731), then the `vulture` console script and `python -m vulture` on
|
|
197
|
+
// PATH (the script dir is frequently not on PATH even when the package
|
|
198
|
+
// is installed). Mirrors govulncheck's no-install gating.
|
|
144
199
|
const candidates = [
|
|
200
|
+
...(root ? this.venvCandidates(root) : []),
|
|
145
201
|
{ cmd: "vulture", prefix: [] },
|
|
146
202
|
{ cmd: "python", prefix: ["-m", "vulture"] },
|
|
147
203
|
{ cmd: "python3", prefix: ["-m", "vulture"] },
|
|
148
204
|
];
|
|
205
|
+
// A timeout on ANY candidate means the machine, not the tool, answered —
|
|
206
|
+
// the run gets a bounded retry instead of a permanent skip (#1467).
|
|
207
|
+
let sawTransient = false;
|
|
208
|
+
let transientCause = "probe-timeout";
|
|
209
|
+
// Accumulated across ALL candidates, because the failure verdicts below
|
|
210
|
+
// are about the whole sweep rather than any one probe. Reporting zero
|
|
211
|
+
// here would erase the evidence that cracked #1467: four 5s probes and
|
|
212
|
+
// a stalled host look identical to an absent tool without these two
|
|
213
|
+
// numbers.
|
|
214
|
+
const sweepStartedAt = Date.now();
|
|
215
|
+
let sweepHostStallMs = 0;
|
|
216
|
+
/** What the last classified candidate returned (#1500 review): this was
|
|
217
|
+
* computed and thrown away, so every vulture row said "missing" with no
|
|
218
|
+
* trace of which candidate reported what. */
|
|
219
|
+
let sweepEvidence;
|
|
149
220
|
for (const c of candidates) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
221
|
+
const sampler = startHostStallSampler();
|
|
222
|
+
const startedAt = Date.now();
|
|
223
|
+
let probe;
|
|
224
|
+
let hostStallMs;
|
|
225
|
+
try {
|
|
226
|
+
probe = await safeSpawnAsync(c.cmd, [...c.prefix, "--version"], {
|
|
227
|
+
timeout: 5000,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
hostStallMs = sampler.stop();
|
|
232
|
+
sweepHostStallMs += hostStallMs;
|
|
233
|
+
}
|
|
153
234
|
if (!probe.error && probe.status === 0) {
|
|
154
235
|
this.resolved = c;
|
|
155
|
-
this.
|
|
236
|
+
this.availabilityLatch.noteAvailable();
|
|
156
237
|
this.log(`vulture found: ${[c.cmd, ...c.prefix].join(" ")}`);
|
|
238
|
+
logAvailabilityDecision({
|
|
239
|
+
tool: "vulture",
|
|
240
|
+
verdict: "available",
|
|
241
|
+
outcome: "success",
|
|
242
|
+
cause: "ok",
|
|
243
|
+
elapsedMs: Date.now() - startedAt,
|
|
244
|
+
latched: true,
|
|
245
|
+
hostStallMs,
|
|
246
|
+
budgetMs: 5000,
|
|
247
|
+
});
|
|
157
248
|
return true;
|
|
158
249
|
}
|
|
250
|
+
const classified = classifyProbeFailure(probe, {
|
|
251
|
+
hostStallMs,
|
|
252
|
+
command: c.cmd,
|
|
253
|
+
});
|
|
254
|
+
sweepEvidence = classified.evidence;
|
|
255
|
+
if (classified.outcome === "transient") {
|
|
256
|
+
sawTransient = true;
|
|
257
|
+
transientCause = classified.cause;
|
|
258
|
+
}
|
|
159
259
|
}
|
|
160
|
-
|
|
260
|
+
if (sawTransient) {
|
|
261
|
+
const retryAfterMs = this.availabilityLatch.noteUnavailable("transient", transientCause);
|
|
262
|
+
this.log("vulture probe timed out; will retry (not treated as missing)");
|
|
263
|
+
logAvailabilityDecision({
|
|
264
|
+
tool: "vulture",
|
|
265
|
+
verdict: "unavailable",
|
|
266
|
+
outcome: "transient",
|
|
267
|
+
cause: transientCause,
|
|
268
|
+
elapsedMs: Date.now() - sweepStartedAt,
|
|
269
|
+
hostStallMs: sweepHostStallMs,
|
|
270
|
+
latched: false,
|
|
271
|
+
retryAfterMs,
|
|
272
|
+
budgetMs: 5000,
|
|
273
|
+
classifiedBy: "probe",
|
|
274
|
+
...(sweepEvidence !== undefined && { evidence: sweepEvidence }),
|
|
275
|
+
});
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
this.availabilityLatch.noteUnavailable("missing", "not-found");
|
|
161
279
|
this.log("vulture not installed; skipping (no auto-install)");
|
|
280
|
+
logAvailabilityDecision({
|
|
281
|
+
tool: "vulture",
|
|
282
|
+
verdict: "unavailable",
|
|
283
|
+
outcome: "missing",
|
|
284
|
+
cause: "not-found",
|
|
285
|
+
elapsedMs: Date.now() - sweepStartedAt,
|
|
286
|
+
hostStallMs: sweepHostStallMs,
|
|
287
|
+
latched: true,
|
|
288
|
+
budgetMs: 5000,
|
|
289
|
+
// Every candidate probe was classified, and none was transient (#1500).
|
|
290
|
+
classifiedBy: "probe",
|
|
291
|
+
...(sweepEvidence !== undefined && { evidence: sweepEvidence }),
|
|
292
|
+
});
|
|
162
293
|
return false;
|
|
163
294
|
}
|
|
164
295
|
async analyze(cwd) {
|
|
@@ -170,7 +301,7 @@ export class PythonDeadCodeClient {
|
|
|
170
301
|
summary: "No Python project root found; vulture skipped",
|
|
171
302
|
};
|
|
172
303
|
}
|
|
173
|
-
if (!(await this.ensureAvailable())) {
|
|
304
|
+
if (!(await this.ensureAvailable(root))) {
|
|
174
305
|
return {
|
|
175
306
|
...emptyResult(this.language),
|
|
176
307
|
success: true,
|
|
@@ -185,14 +316,36 @@ export class PythonDeadCodeClient {
|
|
|
185
316
|
this.inFlight.set(key, promise);
|
|
186
317
|
return promise;
|
|
187
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* True when `<root>/pyproject.toml` carries a `[tool.vulture]` table.
|
|
321
|
+
* vulture's own config discovery reads that table, but CLI flags override
|
|
322
|
+
* it — passing `--min-confidence`/`--exclude` unconditionally silently
|
|
323
|
+
* overrode a project's own thresholds and ignore list (#1731, discipline A).
|
|
324
|
+
* Same shape as `hasSqlfluffConfig`/`hasMypyConfig` in `tool-policy.ts`: a
|
|
325
|
+
* plain string search, no TOML parser, matching the section header only.
|
|
326
|
+
*/
|
|
327
|
+
hasProjectVultureConfig(root) {
|
|
328
|
+
try {
|
|
329
|
+
const content = fs.readFileSync(path.join(root, "pyproject.toml"), "utf-8");
|
|
330
|
+
return content.includes("[tool.vulture]");
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
188
336
|
async runAnalyze(root) {
|
|
189
337
|
const startMs = Date.now();
|
|
190
338
|
const invocation = this.resolved ?? { cmd: "vulture", prefix: [] };
|
|
339
|
+
// Let the project's own [tool.vulture] config win (#1731, discipline A):
|
|
340
|
+
// vulture discovers it unaided, but these two flags override it when
|
|
341
|
+
// passed, so they are omitted whenever the project ships that table.
|
|
342
|
+
const hasConfig = this.hasProjectVultureConfig(root);
|
|
191
343
|
const args = [
|
|
192
344
|
...invocation.prefix,
|
|
193
345
|
".",
|
|
194
|
-
`--min-confidence=${this.minConfidence}
|
|
195
|
-
`--exclude=${VULTURE_EXCLUDES.join(",")}
|
|
346
|
+
...(hasConfig ? [] : [`--min-confidence=${this.minConfidence}`]),
|
|
347
|
+
...(hasConfig ? [] : [`--exclude=${VULTURE_EXCLUDES.join(",")}`]),
|
|
348
|
+
`--ignore-decorators=${VULTURE_IGNORE_DECORATORS.join(",")}`,
|
|
196
349
|
];
|
|
197
350
|
const result = await safeSpawnAsync(invocation.cmd, args, {
|
|
198
351
|
timeout: ANALYSIS_TIMEOUT_MS,
|
|
@@ -210,14 +363,36 @@ export class PythonDeadCodeClient {
|
|
|
210
363
|
}
|
|
211
364
|
// vulture writes parse/usage errors to stderr and exits 1 with no
|
|
212
365
|
// stdout findings; distinguish that from "found dead code" (exit 1 WITH
|
|
213
|
-
// findings on stdout).
|
|
366
|
+
// findings on stdout). #1736 sweep: the ORIGINAL guard here required
|
|
367
|
+
// non-empty stderr to call it an error, so a nonzero exit with BOTH
|
|
368
|
+
// empty stdout and empty stderr (a silent crash) still fell through to
|
|
369
|
+
// "No dead code found" -- the same empty-distinguishes-clean-from-errored
|
|
370
|
+
// gap the knip fix closes. A nonzero exit with no findings on stdout is
|
|
371
|
+
// never clean now, regardless of whether stderr said anything.
|
|
214
372
|
const output = result.stdout || "";
|
|
215
373
|
if (!output.trim()) {
|
|
216
374
|
const stderr = (result.stderr || "").trim();
|
|
217
|
-
|
|
375
|
+
// Same discriminator every dispatch/runners linter uses
|
|
376
|
+
// (`spawnFailedWithNoOutput`) rather than a parallel hand-rolled
|
|
377
|
+
// check (result.error is already handled above, so this reduces to
|
|
378
|
+
// the `status !== 0` half).
|
|
379
|
+
if (spawnFailedWithNoOutput(result, output)) {
|
|
380
|
+
// #1816: one shared wording, one truncation, signal named.
|
|
381
|
+
const reason = formatToolFailure({
|
|
382
|
+
tool: "vulture",
|
|
383
|
+
status: result.status,
|
|
384
|
+
signal: result.signal,
|
|
385
|
+
stderr: result.stderr,
|
|
386
|
+
});
|
|
387
|
+
this.log(reason);
|
|
388
|
+
incrementDegradationCount({
|
|
389
|
+
kind: "runner-empty-result",
|
|
390
|
+
subject: "vulture",
|
|
391
|
+
reason,
|
|
392
|
+
});
|
|
218
393
|
return {
|
|
219
394
|
...emptyResult(this.language),
|
|
220
|
-
summary: `vulture error: ${stderr
|
|
395
|
+
summary: stderr ? `vulture error: ${firstOutputLine(stderr)}` : reason,
|
|
221
396
|
durationMs,
|
|
222
397
|
};
|
|
223
398
|
}
|
|
@@ -258,30 +433,52 @@ export function deadCodeIssueCount(result) {
|
|
|
258
433
|
result.unusedDeps.length +
|
|
259
434
|
result.unlistedDeps.length);
|
|
260
435
|
}
|
|
436
|
+
/** Every bucket flattened — delta diffing and diagnostic mapping want one list. */
|
|
437
|
+
export function deadCodeIssues(result) {
|
|
438
|
+
return [
|
|
439
|
+
...result.unusedExports,
|
|
440
|
+
...result.unusedFiles,
|
|
441
|
+
...result.unusedDeps,
|
|
442
|
+
...result.unlistedDeps,
|
|
443
|
+
];
|
|
444
|
+
}
|
|
261
445
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
446
|
+
* Joins finding-identity parts into a stable key for diffing one scan
|
|
447
|
+
* against the previous one. The string-only signature is load-bearing: a
|
|
448
|
+
* line/column number cannot be passed without a compile error. An edit
|
|
449
|
+
* shifts every line below it, and a scan delta is then filtered to exactly
|
|
450
|
+
* the files the edit touched — so a line in the key turns each shifted
|
|
451
|
+
* pre-existing finding into a "newly unused" report under a heading that
|
|
452
|
+
* blames the agent's own edit for orphaning it. Inserting four lines above
|
|
453
|
+
* one real finding produced four false ones (#1477, #1483). Shared by
|
|
454
|
+
* knip's per-turn delta in runtime-turn.ts so the rule lives in one place.
|
|
266
455
|
*/
|
|
267
|
-
export function
|
|
268
|
-
|
|
269
|
-
|
|
456
|
+
export function stableFindingKey(...parts) {
|
|
457
|
+
return parts.map((part) => part ?? "").join(":");
|
|
458
|
+
}
|
|
459
|
+
/** Stable identity for diffing one scan against the previous one. */
|
|
460
|
+
export function deadCodeIssueKey(issue) {
|
|
461
|
+
return stableFindingKey(issue.category, issue.file, issue.name);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Format this turn's ATTRIBUTABLE delta — symbols that became unused in files
|
|
465
|
+
* the agent just edited — or "" when there is nothing to report. Mirrors the
|
|
466
|
+
* Knip advisory's contract deliberately: the project-wide list is not injected
|
|
467
|
+
* per turn (hundreds of pre-existing findings would drown the blockers and burn
|
|
468
|
+
* context every turn), it stays available on demand via lens_diagnostics.
|
|
469
|
+
*/
|
|
470
|
+
export function formatDeadCodeDelta(issues, language, max = 5) {
|
|
471
|
+
if (issues.length === 0)
|
|
270
472
|
return "";
|
|
271
|
-
|
|
272
|
-
for (const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
if (r.unusedExports.length > maxPerLang) {
|
|
282
|
-
lines.push(` … and ${r.unusedExports.length - maxPerLang} more`);
|
|
283
|
-
}
|
|
473
|
+
let report = `💀 Newly unused ${language} symbols in files you edited — check if callers need updating (dead-code):\n`;
|
|
474
|
+
for (const issue of issues.slice(0, max)) {
|
|
475
|
+
const loc = issue.file
|
|
476
|
+
? `${issue.file}${issue.line ? `:${issue.line}` : ""}`
|
|
477
|
+
: "(unknown)";
|
|
478
|
+
report += ` ${loc} — unused ${issue.kind} ${issue.name}\n`;
|
|
479
|
+
}
|
|
480
|
+
if (issues.length > max) {
|
|
481
|
+
report += ` … and ${issues.length - max} more\n`;
|
|
284
482
|
}
|
|
285
|
-
return
|
|
286
|
-
lines.join("\n "));
|
|
483
|
+
return report;
|
|
287
484
|
}
|