@herbertgao/pi-extensions 2026.8.4 → 2026.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +32 -24
- package/node_modules/@narumitw/pi-btw/package.json +6 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +2535 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
- package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
- package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
- package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
- package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +51785 -36118
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +219 -46
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +40 -15
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +45 -3
- package/node_modules/pi-lens/docs/globalconfig.md +56 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +18 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +41 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
- package/node_modules/pi-mcp-adapter/README.md +38 -4
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +4 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +176 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +12 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -2,13 +2,17 @@ import * as path from "node:path";
|
|
|
2
2
|
import { appendActionableWarningsHistory, buildActionableWarningsReport, formatActionableWarningsAdvisory, writeActionableWarningsReport, } from "./actionable-warnings.js";
|
|
3
3
|
import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
|
|
4
4
|
import { appendCodeQualityWarningsHistory, buildCodeQualityWarningsReport, formatCodeQualityWarningsAdvisory, writeCodeQualityWarningsReport, } from "./code-quality-warnings.js";
|
|
5
|
+
import { clearGitGuardTestFailure, mergeGitGuardTestFailure, writeGitGuardRecord, } from "./git-guard.js";
|
|
5
6
|
import { logCascade } from "./cascade-logger.js";
|
|
6
7
|
import { normalizeMapKey } from "./path-utils.js";
|
|
7
8
|
import { resolveRunnerPath, toRunnerDisplayPath, } from "./dispatch/runner-context.js";
|
|
8
9
|
import { getKnipIgnorePatterns } from "./file-utils.js";
|
|
10
|
+
import { isTestRoleCollateral } from "./collateral-test-role.js";
|
|
9
11
|
import { dedupeSecretFindings, fromAstGrepWarnings, fromGitleaks, fromTrivySecrets, isSecretWarning, secretLocationKey, } from "./secret-findings.js";
|
|
10
|
-
import {
|
|
12
|
+
import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, } from "./dead-code-client.js";
|
|
13
|
+
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
11
14
|
import { PROJECT_DIAGNOSTICS_CACHE_VERSION, writeProjectDiagnosticsDeltaReport, } from "./project-diagnostics/cache.js";
|
|
15
|
+
import { deadCodeIssueToProjectDiagnostic } from "./project-diagnostics/runner-adapters/dead-code.js";
|
|
12
16
|
import { knipIssuesToProjectDiagnostics } from "./project-diagnostics/runner-adapters/knip.js";
|
|
13
17
|
import { logLatency } from "./latency-logger.js";
|
|
14
18
|
import { getLspBudgetIdleTimeoutMs, shouldShortenLspIdleTimeout, } from "./lsp-budget.js";
|
|
@@ -16,6 +20,19 @@ import { updateHeartbeat } from "./instance-registry.js";
|
|
|
16
20
|
import { emitLensTurnFindings } from "./lens-events.js";
|
|
17
21
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
18
22
|
import { isSubagentSession } from "./subagent-mode.js";
|
|
23
|
+
import { formatRunDurationMs } from "./run-duration.js";
|
|
24
|
+
import { MAX_ADVISORY_AFFECTED_FILES, dropFindingsForMissingPaths, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
|
|
25
|
+
/**
|
|
26
|
+
* Would writing `next` over `prev` throw away a good scan for a failed one?
|
|
27
|
+
*
|
|
28
|
+
* A failed run carries no findings. Writing it evicts the last good result, and
|
|
29
|
+
* every later reader then serves the failure as the answer — a 194-byte "not
|
|
30
|
+
* available" record replaced 149 KB of real findings in every dogfood project
|
|
31
|
+
* (#925, #1467). Callers keep the previous cache when this returns true.
|
|
32
|
+
*/
|
|
33
|
+
function wouldPoisonCache(prev, next) {
|
|
34
|
+
return !next.success && prev?.data.success === true;
|
|
35
|
+
}
|
|
19
36
|
// LSP idle reset scheduling — prevents thrashing by delaying shutdown
|
|
20
37
|
let lspIdleResetTimeout = null;
|
|
21
38
|
function emitIdleResetReporterWarning(reportErr) {
|
|
@@ -86,7 +103,7 @@ function capTurnEndMessage(content) {
|
|
|
86
103
|
return out;
|
|
87
104
|
}
|
|
88
105
|
export async function handleTurnEnd(deps) {
|
|
89
|
-
const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, resetLSPService, resetFormatService, } = deps;
|
|
106
|
+
const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, owner, resetLSPService, resetFormatService, } = deps;
|
|
90
107
|
// #449 slice 1: piggyback the instance-registry heartbeat on this existing
|
|
91
108
|
// per-turn touchpoint rather than adding a new timer/interval. Cheap (reads
|
|
92
109
|
// process.memoryUsage().rss, one read-modify-write of instances.json) and
|
|
@@ -106,16 +123,48 @@ export async function handleTurnEnd(deps) {
|
|
|
106
123
|
});
|
|
107
124
|
const cwd = ctxCwd ?? process.cwd();
|
|
108
125
|
let turnState = cacheManager.readTurnState(cwd);
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
// A live foreign writer owns this worklist. Do not clear or consume another
|
|
127
|
+
// pi/MCP session's files; a dead/aged owner is safely evicted instead.
|
|
128
|
+
const currentOwner = owner ?? {
|
|
129
|
+
kind: "pi",
|
|
130
|
+
id: runtime.telemetrySessionId,
|
|
131
|
+
pid: process.pid,
|
|
132
|
+
lastSeen: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
const access = cacheManager.getTurnStateAccess(cwd, currentOwner);
|
|
135
|
+
const sameProcessPiSessionHandoff = access === "foreign-live" &&
|
|
136
|
+
currentOwner.kind === "pi" &&
|
|
137
|
+
turnState.owner?.kind === "pi" &&
|
|
138
|
+
turnState.owner.pid === process.pid &&
|
|
139
|
+
turnState.owner.id !== currentOwner.id;
|
|
140
|
+
if (access === "foreign-live" && !sameProcessPiSessionHandoff) {
|
|
141
|
+
dbg(`turn_end: foreign live owner retained (${turnState.owner?.kind ?? "legacy"}:${turnState.owner?.id ?? turnState.sessionId})`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (access === "available" && (turnState.files || turnState.owner || turnState.sessionId)) {
|
|
145
|
+
dbg("turn_end: evicting stale turn-state owner");
|
|
146
|
+
cacheManager.clearTurnState(cwd, currentOwner);
|
|
115
147
|
turnState = cacheManager.readTurnState(cwd);
|
|
116
148
|
}
|
|
117
149
|
const files = Object.keys(turnState.files);
|
|
118
|
-
|
|
150
|
+
// R1 (#1443 follow-up): a read-only turn (no files touched) must not take
|
|
151
|
+
// the fast idle-reset path while a carried cascade run — or one still
|
|
152
|
+
// settling — is waiting for its delivery opportunity. Falling through to
|
|
153
|
+
// the normal pipeline lets the settle/drain/merge logic below run exactly
|
|
154
|
+
// as it does for an edit turn, so a carried finding reaches the agent
|
|
155
|
+
// instead of dying unrendered. `hasCascadeRuns()` is a cheap peek (no
|
|
156
|
+
// pending work almost every turn), so the common read-only turn still
|
|
157
|
+
// takes the early return below.
|
|
158
|
+
if (files.length === 0 && !runtime.hasCascadeRuns()) {
|
|
159
|
+
// A genuinely clean session must invalidate the persisted guard record.
|
|
160
|
+
// Blocker records are retained only while the runtime still reports one.
|
|
161
|
+
if (getFlag("lens-guard") && !runtime.gitGuardHasBlockers) {
|
|
162
|
+
const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
163
|
+
if (guardRecord?.sessionId === runtime.telemetrySessionId &&
|
|
164
|
+
guardRecord.testFailures !== true) {
|
|
165
|
+
cacheManager.clearCache("turn-end-findings", cwd);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
119
168
|
// #713: subagent sessions use a shorter idle reset (60s) — a short-lived
|
|
120
169
|
// task agent holding a warm fleet for 4 minutes after its last turn is
|
|
121
170
|
// pure waste under fan-out. Classify ONCE here so every tick in this call
|
|
@@ -143,7 +192,7 @@ export async function handleTurnEnd(deps) {
|
|
|
143
192
|
dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}`);
|
|
144
193
|
if (cacheManager.isMaxCyclesExceeded(cwd)) {
|
|
145
194
|
dbg("turn_end: max cycles exceeded, clearing state and forcing through");
|
|
146
|
-
cacheManager.clearTurnState(cwd);
|
|
195
|
+
cacheManager.clearTurnState(cwd, currentOwner);
|
|
147
196
|
runtime.fixedThisTurn.clear();
|
|
148
197
|
resetFormatService();
|
|
149
198
|
return;
|
|
@@ -155,7 +204,7 @@ export async function handleTurnEnd(deps) {
|
|
|
155
204
|
const projectDiagnosticsSources = new Set();
|
|
156
205
|
// Re-surface inline blockers from this turn that the agent didn't fix.
|
|
157
206
|
// These were shown inline during write/edit but the agent moved on without resolving them.
|
|
158
|
-
const unresolvedBlockers = runtime.
|
|
207
|
+
const unresolvedBlockers = runtime.getInlineBlockersSnapshot();
|
|
159
208
|
for (const { filePath: bPath, summary } of unresolvedBlockers) {
|
|
160
209
|
const displayPath = toRunnerDisplayPath(cwd, bPath);
|
|
161
210
|
blockerParts.push(`Unresolved from this turn — ${displayPath}:\n${summary}`);
|
|
@@ -180,7 +229,62 @@ export async function handleTurnEnd(deps) {
|
|
|
180
229
|
// 2. Neighbor-level: each neighbor is claimed by the latest cascade result
|
|
181
230
|
// that covers it — suppresses stale neighbor state from earlier writes.
|
|
182
231
|
const t0 = Date.now();
|
|
183
|
-
const cascadeRuns = runtime.consumeCascadeRuns()
|
|
232
|
+
const cascadeRuns = runtime.consumeCascadeRuns().filter((run) => {
|
|
233
|
+
const originSeq = run.origin?.projectSeq;
|
|
234
|
+
const originTurn = run.origin?.turnSeq;
|
|
235
|
+
// A deferred result from AFTER a later write is not current state. Old test
|
|
236
|
+
// fixtures without provenance remain accepted for compatibility.
|
|
237
|
+
//
|
|
238
|
+
// #1443: `turnSeq` alone is NOT a supersede signal, and it used to be an
|
|
239
|
+
// unconditional reject. Every LATE run — one whose compute missed the
|
|
240
|
+
// settle cap and was re-parked by `settleCascadeRuns`, and one the
|
|
241
|
+
// quiet-window reconcile appended after this turn's predecessor already
|
|
242
|
+
// consumed (carried across turn_start by `beginTurn`) — is BY DEFINITION
|
|
243
|
+
// from an earlier turn, so `originTurn === runtime.turnIndex` was always
|
|
244
|
+
// false for exactly the runs the carry-over was built to preserve. Both
|
|
245
|
+
// producers' contracts were dead code: the measured cases were the two
|
|
246
|
+
// highest-fan-out cascades of the day (38 and 40 neighbours).
|
|
247
|
+
//
|
|
248
|
+
// R2 (#1443 follow-up): `projectSeq` alone is NOT a per-file supersede
|
|
249
|
+
// signal — it is GLOBAL, advancing on every pi-observed write anywhere in
|
|
250
|
+
// the project. Rejecting on any mismatch meant an edit to an unrelated
|
|
251
|
+
// file superseded a run that had nothing to do with it, reintroducing the
|
|
252
|
+
// exact 38/40-neighbour loss #1443 was written to fix, one filter down.
|
|
253
|
+
// `getFilesChangedSince` (#451) is the honest per-file signal: a run is
|
|
254
|
+
// superseded only if its own primary file or one of its neighbours was
|
|
255
|
+
// actually rewritten since it launched. A late-but-not-superseded run is
|
|
256
|
+
// surfaced; a superseded one is dropped with a RECORD (never silently),
|
|
257
|
+
// so the loss stays countable.
|
|
258
|
+
if (originSeq !== undefined) {
|
|
259
|
+
const changedSince = runtime.getFilesChangedSince(originSeq);
|
|
260
|
+
if (changedSince.length > 0) {
|
|
261
|
+
const changedSet = new Set(changedSince);
|
|
262
|
+
const primaryKey = normalizeMapKey(path.resolve(run.filePath));
|
|
263
|
+
const neighborKeys = (run.result?.neighbors ?? []).map((n) => normalizeMapKey(path.resolve(n.filePath)));
|
|
264
|
+
const supersededByOwnFile = changedSet.has(primaryKey) ||
|
|
265
|
+
neighborKeys.some((k) => changedSet.has(k));
|
|
266
|
+
if (supersededByOwnFile) {
|
|
267
|
+
logCascade({
|
|
268
|
+
phase: "cascade_carry_over_drop",
|
|
269
|
+
filePath: run.filePath,
|
|
270
|
+
neighborCount: run.neighborCount,
|
|
271
|
+
diagnosticCount: run.diagnosticCount,
|
|
272
|
+
reason: "superseded_by_later_write",
|
|
273
|
+
metadata: {
|
|
274
|
+
originProjectSeq: originSeq,
|
|
275
|
+
projectSeq: runtime.projectSeq,
|
|
276
|
+
originTurnSeq: originTurn,
|
|
277
|
+
turnIndex: runtime.turnIndex,
|
|
278
|
+
carriedTurns: run.carriedTurns,
|
|
279
|
+
changedFiles: changedSince,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
});
|
|
184
288
|
const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
|
|
185
289
|
if (cascadeResults.length > 0) {
|
|
186
290
|
const seen = new Map();
|
|
@@ -198,11 +302,25 @@ export async function handleTurnEnd(deps) {
|
|
|
198
302
|
}
|
|
199
303
|
}
|
|
200
304
|
const parts = [];
|
|
305
|
+
// #1446 item 1: track what actually gets injected — a suppressed result
|
|
306
|
+
// (real formatted cascade text, but every one of its neighbors was claimed
|
|
307
|
+
// by a LATER result — see the reverse-iteration ownership pass above) was
|
|
308
|
+
// previously indistinguishable from "no output"; this counts it explicitly
|
|
309
|
+
// instead of letting it vanish.
|
|
310
|
+
let injectedNeighborCount = 0;
|
|
311
|
+
let injectedDiagnosticCount = 0;
|
|
312
|
+
let suppressedByOwnership = 0;
|
|
201
313
|
for (const result of seen.values()) {
|
|
202
314
|
const pk = normalizeMapKey(result.filePath);
|
|
203
315
|
const ownsAny = result.neighbors.some((n) => neighborOwner.get(normalizeMapKey(n.filePath)) === pk);
|
|
204
|
-
if (ownsAny && result.formatted)
|
|
316
|
+
if (ownsAny && result.formatted) {
|
|
205
317
|
parts.push(result.formatted);
|
|
318
|
+
injectedNeighborCount += result.neighbors.length;
|
|
319
|
+
injectedDiagnosticCount += result.neighbors.reduce((s, n) => s + n.diagnostics.length, 0);
|
|
320
|
+
}
|
|
321
|
+
else if (!ownsAny && result.formatted) {
|
|
322
|
+
suppressedByOwnership++;
|
|
323
|
+
}
|
|
206
324
|
}
|
|
207
325
|
// Suggest tests for cascade neighbors (files with diagnostics)
|
|
208
326
|
const neighborFilesWithErrors = cascadeResults
|
|
@@ -210,9 +328,27 @@ export async function handleTurnEnd(deps) {
|
|
|
210
328
|
.filter((n) => n.diagnostics.length > 0)
|
|
211
329
|
.map((n) => n.filePath);
|
|
212
330
|
const uniqueNeighborFiles = [...new Set(neighborFilesWithErrors)];
|
|
331
|
+
let testSuggestionCount = 0;
|
|
213
332
|
if (uniqueNeighborFiles.length > 0 &&
|
|
214
333
|
typeof testRunnerClient.suggestTestFiles === "function") {
|
|
215
334
|
const testSuggestions = testRunnerClient.suggestTestFiles(uniqueNeighborFiles, cwd);
|
|
335
|
+
testSuggestionCount = testSuggestions.length;
|
|
336
|
+
// #1446 item 2: this path previously emitted nothing to any log — a
|
|
337
|
+
// zero-suggestion outcome (neighbors had errors but no test file
|
|
338
|
+
// resolved for any of them) is the more interesting case, so it is
|
|
339
|
+
// recorded on the same phase rather than only logging on a hit.
|
|
340
|
+
logCascade({
|
|
341
|
+
phase: "cascade_test_targets",
|
|
342
|
+
filePath: files[0] ?? cwd,
|
|
343
|
+
neighborCount: uniqueNeighborFiles.length,
|
|
344
|
+
metadata: {
|
|
345
|
+
neighborFiles: uniqueNeighborFiles.slice(0, 10),
|
|
346
|
+
suggestedTestFiles: testSuggestions.slice(0, 10).map((s) => s.testFile),
|
|
347
|
+
runner: testSuggestions[0]?.runner,
|
|
348
|
+
truncated: testSuggestions.length > 10,
|
|
349
|
+
zeroSuggestions: testSuggestions.length === 0,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
216
352
|
if (testSuggestions.length > 0) {
|
|
217
353
|
const testLines = testSuggestions
|
|
218
354
|
.slice(0, 5)
|
|
@@ -224,8 +360,30 @@ export async function handleTurnEnd(deps) {
|
|
|
224
360
|
parts.push(testSection);
|
|
225
361
|
}
|
|
226
362
|
}
|
|
227
|
-
if (parts.length > 0)
|
|
228
|
-
|
|
363
|
+
if (parts.length > 0) {
|
|
364
|
+
const section = parts.join("\n\n");
|
|
365
|
+
blockerParts.push(section);
|
|
366
|
+
// #1446 item 1: proves the cascade section reached `blockerParts` —
|
|
367
|
+
// i.e. it was QUEUED for persistence into the turn-end advisory — not
|
|
368
|
+
// that it reached the agent. The counters alone (cascade_result,
|
|
369
|
+
// cascade_turn_end) never confirmed even that much, only computation.
|
|
370
|
+
// Actual delivery happens later, via consumeTurnEndFindings/
|
|
371
|
+
// peekTurnEndFindings, and can still be suppressed after this point
|
|
372
|
+
// (e.g. allFilesDeleted, cross-turn dedup, or the session ending
|
|
373
|
+
// before the next turn_end drains it) — this record does not prove
|
|
374
|
+
// the agent ever saw the text.
|
|
375
|
+
logCascade({
|
|
376
|
+
phase: "cascade_injected",
|
|
377
|
+
filePath: files[0] ?? cwd,
|
|
378
|
+
neighborCount: injectedNeighborCount,
|
|
379
|
+
diagnosticCount: injectedDiagnosticCount,
|
|
380
|
+
metadata: {
|
|
381
|
+
sectionChars: section.length,
|
|
382
|
+
testSuggestionCount,
|
|
383
|
+
suppressedByOwnership,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
229
387
|
logCascade({
|
|
230
388
|
phase: "cascade_turn_end",
|
|
231
389
|
filePath: files[0] ?? cwd,
|
|
@@ -249,32 +407,70 @@ export async function handleTurnEnd(deps) {
|
|
|
249
407
|
// (over-correction guard).
|
|
250
408
|
const indeterminateRuns = cascadeRuns.filter((r) => r.indeterminate);
|
|
251
409
|
if (indeterminateRuns.length > 0) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
410
|
+
// #1104 (review P3 on PR #1143, rides with the resultId main body): this
|
|
411
|
+
// preamble used to hardcode a graph-unavailability frame for EVERY
|
|
412
|
+
// indeterminate reason. That's accurate for `graph_degraded`/
|
|
413
|
+
// `missing_node`/`error` (the graph really couldn't produce a dependent
|
|
414
|
+
// set), but `lsp_binding_rejected` is a DIFFERENT failure shape — the
|
|
415
|
+
// graph WAS available and dependents WERE derived; only their LSP
|
|
416
|
+
// diagnostics display was withheld because a fallback snapshot's content
|
|
417
|
+
// binding didn't match current disk. Saying "the review graph was
|
|
418
|
+
// unavailable" for that case mis-attributes the cause. Bucket by reason
|
|
419
|
+
// family so each gets its own accurate frame.
|
|
420
|
+
const buildAdvisory = (runs, frame) => {
|
|
421
|
+
if (runs.length === 0)
|
|
422
|
+
return undefined;
|
|
423
|
+
const byDetail = new Map();
|
|
424
|
+
for (const r of runs) {
|
|
425
|
+
const detail = r.indeterminate?.detail ?? frame.fallbackDetail(r);
|
|
426
|
+
const files = byDetail.get(detail) ?? [];
|
|
427
|
+
files.push(toRunnerDisplayPath(cwd, r.filePath));
|
|
428
|
+
byDetail.set(detail, files);
|
|
429
|
+
}
|
|
430
|
+
const lines = [];
|
|
431
|
+
for (const [detail, filesRaw] of byDetail) {
|
|
432
|
+
const files = [...new Set(filesRaw)];
|
|
433
|
+
const shown = files.slice(0, 5).join(", ");
|
|
434
|
+
const more = files.length > 5 ? ` (+${files.length - 5} more)` : "";
|
|
435
|
+
lines.push(` • ${detail}: ${shown}${more}`);
|
|
436
|
+
}
|
|
437
|
+
const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath))).size;
|
|
438
|
+
const reasons = [...byDetail.keys()].join("; ");
|
|
439
|
+
return `${frame.lead(fileCount, reasons)}\n${lines.join("\n")}`;
|
|
440
|
+
};
|
|
441
|
+
// #1445: `excluded_by_role` (test files excluded from the graph BY DESIGN,
|
|
442
|
+
// #260) is never agent-facing — it is not a graph failure, and #1080
|
|
443
|
+
// already excludes test-role files from every neighbor surface, so "a
|
|
444
|
+
// clean result does not cover them" would itself be a false claim. It
|
|
445
|
+
// stays visible in the `cascade_indeterminate` log below (metadata-only,
|
|
446
|
+
// info-level) so the log can tell an intentional exclusion from a real
|
|
447
|
+
// graph gap, but it never reaches `buildAdvisory`/the agent.
|
|
448
|
+
const graphRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason !== "lsp_binding_rejected" &&
|
|
449
|
+
r.indeterminate?.reason !== "excluded_by_role");
|
|
450
|
+
const bindingRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "lsp_binding_rejected");
|
|
271
451
|
// Factual/informational phrasing — the advisory tier wraps this with an
|
|
272
452
|
// "ℹ️ Advisory — no action required this turn:" label, so an imperative
|
|
273
453
|
// ("review dependents manually") would contradict it. The #533 substance
|
|
274
454
|
// stays: a clean cascade result does NOT cover these files' dependents.
|
|
275
|
-
|
|
276
|
-
`
|
|
277
|
-
|
|
455
|
+
const graphAdvisory = buildAdvisory(graphRuns, {
|
|
456
|
+
lead: (fileCount, reasons) => `Cascade could not compute downstream impact for ${fileCount} edited file(s) this turn — ` +
|
|
457
|
+
`the review graph was unavailable (${reasons}), so their dependents were not ` +
|
|
458
|
+
`cascade-checked and a clean cascade result does not cover them.`,
|
|
459
|
+
fallbackDetail: (r) => r.indeterminate?.reason === "missing_node"
|
|
460
|
+
? "changed file not in the review graph"
|
|
461
|
+
: "review graph unavailable",
|
|
462
|
+
});
|
|
463
|
+
if (graphAdvisory)
|
|
464
|
+
advisoryParts.push(graphAdvisory);
|
|
465
|
+
const bindingAdvisory = buildAdvisory(bindingRuns, {
|
|
466
|
+
lead: (fileCount, reasons) => `Cascade identified dependents for ${fileCount} edited file(s) this turn, but their ` +
|
|
467
|
+
`diagnostics could not be freshly confirmed (${reasons}) and were withheld — a clean ` +
|
|
468
|
+
`cascade result does not cover them.`,
|
|
469
|
+
fallbackDetail: () => "cascade diagnostics withheld (binding rejected)",
|
|
470
|
+
});
|
|
471
|
+
if (bindingAdvisory)
|
|
472
|
+
advisoryParts.push(bindingAdvisory);
|
|
473
|
+
const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
|
|
278
474
|
logCascade({
|
|
279
475
|
phase: "cascade_indeterminate",
|
|
280
476
|
filePath: files[0] ?? cwd,
|
|
@@ -322,8 +518,12 @@ export async function handleTurnEnd(deps) {
|
|
|
322
518
|
// run tool checks every turn. Also back off after a timeout/kill so every
|
|
323
519
|
// agent turn does not spend 30s launching another heavyweight knip process.
|
|
324
520
|
const prevKnip = cacheManager.readCache("knip", cwd);
|
|
521
|
+
// An availability failure is NOT a hard knip failure: knip never ran, so
|
|
522
|
+
// there is nothing to back off from, and backing off would make an
|
|
523
|
+
// expiring probe verdict permanent again (#1467).
|
|
325
524
|
const previousFailedHard = prevKnip &&
|
|
326
525
|
!prevKnip.data.success &&
|
|
526
|
+
!prevKnip.data.failureKind &&
|
|
327
527
|
/(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prevKnip.data.summary);
|
|
328
528
|
if (previousFailedHard) {
|
|
329
529
|
dbg(`turn_end: skipping knip after recent failure: ${prevKnip.data.summary}`);
|
|
@@ -331,13 +531,23 @@ export async function handleTurnEnd(deps) {
|
|
|
331
531
|
}
|
|
332
532
|
else {
|
|
333
533
|
const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns());
|
|
334
|
-
|
|
534
|
+
// Never overwrite a good scan with a failure (#925, #1467): the last
|
|
535
|
+
// good result stays until a new successful scan replaces it.
|
|
536
|
+
const knipWouldPoison = wouldPoisonCache(prevKnip, knipResult);
|
|
537
|
+
if (knipWouldPoison) {
|
|
538
|
+
dbg(`turn_end: keeping last good knip cache; this run failed: ${knipResult.summary}`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
cacheManager.writeCache("knip", knipResult, cwd);
|
|
542
|
+
}
|
|
335
543
|
knipMeta = {
|
|
336
544
|
success: knipResult.success,
|
|
337
545
|
totalIssues: knipResult.issues.length,
|
|
338
546
|
newIssues: 0,
|
|
339
547
|
blockerIssues: 0,
|
|
340
548
|
...(!knipResult.success && { reason: knipResult.summary }),
|
|
549
|
+
...(knipResult.failureKind && { failureKind: knipResult.failureKind }),
|
|
550
|
+
...(knipWouldPoison && { cacheKept: true }),
|
|
341
551
|
};
|
|
342
552
|
if (knipResult.success && knipResult.issues.length > 0) {
|
|
343
553
|
const issueKey = (i) => `${i.type}:${i.file ?? ""}:${i.name}:${i.line ?? 0}:${i.package ?? ""}`;
|
|
@@ -404,6 +614,140 @@ export async function handleTurnEnd(deps) {
|
|
|
404
614
|
durationMs: Date.now() - t2,
|
|
405
615
|
metadata: knipMeta,
|
|
406
616
|
});
|
|
617
|
+
// Cross-file dead-code (#127) for non-JS/TS languages, on knip's contract:
|
|
618
|
+
// re-scan only when this turn touched a file the client owns, then inject the
|
|
619
|
+
// ATTRIBUTABLE delta — symbols in those files that became unused because of
|
|
620
|
+
// the edit. The project-wide list is deliberately NOT injected per turn (the
|
|
621
|
+
// same reasoning as knip above) and stays available via lens_diagnostics.
|
|
622
|
+
// MUST run before the projectDiagnosticsDelta write below, or a dead-code-only
|
|
623
|
+
// turn would persist nothing.
|
|
624
|
+
const tDeadCode = Date.now();
|
|
625
|
+
const deadCodeMeta = {};
|
|
626
|
+
if (runtime.isStartupScanInFlight("dead-code")) {
|
|
627
|
+
dbg("turn_end: skipping dead-code (startup scan still in flight)");
|
|
628
|
+
deadCodeMeta.skipped = true;
|
|
629
|
+
deadCodeMeta.reason = "startup_scan_in_flight";
|
|
630
|
+
}
|
|
631
|
+
else if (deadCodeClients.length === 0) {
|
|
632
|
+
deadCodeMeta.reason = "no_clients";
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// The modified-file set costs a resolveRunnerPath per file, and that walks
|
|
636
|
+
// every ancestor to the filesystem root on a miss. Build it lazily, only
|
|
637
|
+
// once a client has actually claimed this project, so an all-JS repo with
|
|
638
|
+
// no dead-code client pays nothing per turn. Knip does the same.
|
|
639
|
+
let modifiedSet = null;
|
|
640
|
+
const modifiedFiles = () => (modifiedSet ??= new Set(files.map((f) => resolveRunnerPath(cwd, f))));
|
|
641
|
+
let newIssueTotal = 0;
|
|
642
|
+
const reasons = [];
|
|
643
|
+
// A malformed client or deps object must never abort turn_end. Before the
|
|
644
|
+
// per-turn delta this block only read a cache; now it iterates and awaits,
|
|
645
|
+
// so the whole thing needs the guard, not just `client.analyze`.
|
|
646
|
+
try {
|
|
647
|
+
for (const client of deadCodeClients) {
|
|
648
|
+
if (!client.detect(cwd)) {
|
|
649
|
+
reasons.push(`${client.id}:not_detected`);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (![...modifiedFiles()].some((f) => client.owns(f))) {
|
|
653
|
+
reasons.push(`${client.id}:no_owned_files`);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const cacheKey = `dead-code-${client.id}`;
|
|
657
|
+
const prev = cacheManager.readCache(cacheKey, cwd);
|
|
658
|
+
// Back off after a timeout/kill so an unresponsive scanner cannot cost
|
|
659
|
+
// every later turn its full analysis budget (mirrors knip).
|
|
660
|
+
if (prev &&
|
|
661
|
+
!prev.data.success &&
|
|
662
|
+
/(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prev.data.summary)) {
|
|
663
|
+
dbg(`turn_end: skipping dead-code after failure: ${prev.data.summary}`);
|
|
664
|
+
deadCodeMeta.skipped = true;
|
|
665
|
+
reasons.push(`${client.id}:backoff:${prev.data.summary}`);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const startMs = Date.now();
|
|
669
|
+
try {
|
|
670
|
+
const result = await client.analyze(cwd);
|
|
671
|
+
const durationMs = Date.now() - startMs;
|
|
672
|
+
// Never overwrite a good scan with a failure (#925, #1467): a
|
|
673
|
+
// vulture timeout on one .py turn would otherwise evict the
|
|
674
|
+
// session_start scan, and the backoff above would then latch
|
|
675
|
+
// off the poisoned record.
|
|
676
|
+
if (wouldPoisonCache(prev, result)) {
|
|
677
|
+
dbg(`turn_end: keeping last good dead-code(${client.id}) cache; this run failed: ${result.summary}`);
|
|
678
|
+
deadCodeMeta.cacheKept = true;
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
cacheManager.writeCache(cacheKey, result, cwd, {
|
|
682
|
+
scanDurationMs: durationMs,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
// One event per cross-file scan (AGENTS.md) — the per-turn scan is
|
|
686
|
+
// now the primary path, so dead-code.log must see it too.
|
|
687
|
+
logDeadCodeScan({
|
|
688
|
+
language: client.language,
|
|
689
|
+
success: result.success,
|
|
690
|
+
cached: false,
|
|
691
|
+
unusedExports: result.unusedExports.length,
|
|
692
|
+
unusedFiles: result.unusedFiles.length,
|
|
693
|
+
unusedDeps: result.unusedDeps.length,
|
|
694
|
+
unlistedDeps: result.unlistedDeps.length,
|
|
695
|
+
durationMs: result.durationMs ?? durationMs,
|
|
696
|
+
...(!result.success && { reason: result.summary }),
|
|
697
|
+
});
|
|
698
|
+
deadCodeMeta.success = result.success;
|
|
699
|
+
if (!result.success) {
|
|
700
|
+
reasons.push(`${client.id}:scan_failed:${result.summary}`);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
deadCodeMeta.totalIssues =
|
|
704
|
+
(deadCodeMeta.totalIssues ?? 0) + deadCodeIssues(result).length;
|
|
705
|
+
// No baseline means every finding looks new. Report nothing rather
|
|
706
|
+
// than blame the edit for the whole project's pre-existing debt.
|
|
707
|
+
if (!prev?.data.success) {
|
|
708
|
+
reasons.push(`${client.id}:no_previous_scan`);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const prevKeys = new Set(deadCodeIssues(prev.data).map(deadCodeIssueKey));
|
|
712
|
+
const modified = modifiedFiles();
|
|
713
|
+
const newIssues = deadCodeIssues(result).filter((issue) => {
|
|
714
|
+
if (prevKeys.has(deadCodeIssueKey(issue)))
|
|
715
|
+
return false;
|
|
716
|
+
if (!issue.file)
|
|
717
|
+
return false;
|
|
718
|
+
return modified.has(resolveRunnerPath(cwd, issue.file));
|
|
719
|
+
});
|
|
720
|
+
if (newIssues.length === 0) {
|
|
721
|
+
reasons.push(`${client.id}:clean`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
newIssueTotal += newIssues.length;
|
|
725
|
+
projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
|
|
726
|
+
projectDiagnosticsSources.add("dead-code");
|
|
727
|
+
advisoryParts.push(formatDeadCodeDelta(newIssues, result.language));
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
dbg(`turn_end: dead-code(${client.id}) failed: ${err}`);
|
|
731
|
+
reasons.push(`${client.id}:threw`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
dbg(`turn_end: dead-code block failed: ${err}`);
|
|
737
|
+
reasons.push("block_threw");
|
|
738
|
+
}
|
|
739
|
+
deadCodeMeta.newIssues = newIssueTotal;
|
|
740
|
+
if (reasons.length > 0)
|
|
741
|
+
deadCodeMeta.reason = reasons.join(",");
|
|
742
|
+
}
|
|
743
|
+
logLatency({
|
|
744
|
+
type: "phase",
|
|
745
|
+
toolName: "turn_end",
|
|
746
|
+
filePath: cwd,
|
|
747
|
+
phase: "dead-code",
|
|
748
|
+
durationMs: Date.now() - tDeadCode,
|
|
749
|
+
metadata: deadCodeMeta,
|
|
750
|
+
});
|
|
407
751
|
// govulncheck — surface session_start-cached Go CVE findings as advisory.
|
|
408
752
|
// No per-turn re-run in this slice; the cache refreshes at next session_start.
|
|
409
753
|
const govCacheEntry = cacheManager.readCache("govulncheck", cwd);
|
|
@@ -432,11 +776,25 @@ export async function handleTurnEnd(deps) {
|
|
|
432
776
|
// location so a committed/hardcoded secret is reported ONCE (with combined
|
|
433
777
|
// provenance) — a blocker, since credentials need rotation before merge.
|
|
434
778
|
const gitleaksData = cacheManager.readCache("gitleaks", cwd)?.data;
|
|
779
|
+
// #1461 slice 1 (#1460): the gitleaks cache is TTL-only, so a finding for a
|
|
780
|
+
// file deleted after the scan is still served as a 🔴 blocker for the rest
|
|
781
|
+
// of the 30-minute window — the live case, and 119 of 126 findings in
|
|
782
|
+
// pi-lens's own cache. This read is the single agent-facing consumer of that
|
|
783
|
+
// store (session_start's read only decides whether to re-scan; the
|
|
784
|
+
// project-diagnostics path re-scans fresh and reconciles at load), so the
|
|
785
|
+
// drop belongs here, before the findings enter the shared secret pipeline.
|
|
786
|
+
// gitleaks only in this slice — trivy secrets are slice 1's sibling store.
|
|
787
|
+
const gitleaksFindings = dropFindingsForMissingPaths({
|
|
788
|
+
store: "gitleaks",
|
|
789
|
+
findings: gitleaksData?.findings ?? [],
|
|
790
|
+
cwd,
|
|
791
|
+
citedPath: (finding) => finding.file,
|
|
792
|
+
});
|
|
435
793
|
const astSecretWarnings = runtime
|
|
436
794
|
.peekActionableWarnings()
|
|
437
795
|
.filter(isSecretWarning);
|
|
438
796
|
const sessionSecrets = dedupeSecretFindings([
|
|
439
|
-
...fromGitleaks(
|
|
797
|
+
...fromGitleaks(gitleaksFindings),
|
|
440
798
|
...fromTrivySecrets(trivyCacheEntry?.data?.secrets ?? []),
|
|
441
799
|
]);
|
|
442
800
|
// Locations already surfaced as session-scan secret blockers — used to enrich
|
|
@@ -516,6 +874,7 @@ export async function handleTurnEnd(deps) {
|
|
|
516
874
|
advisoryParts.push(report);
|
|
517
875
|
}
|
|
518
876
|
const t3 = Date.now();
|
|
877
|
+
let madgeStats;
|
|
519
878
|
if (await depChecker.ensureAvailable()) {
|
|
520
879
|
const madgeFiles = cacheManager.getFilesForMadge(cwd);
|
|
521
880
|
if (madgeFiles.length > 0) {
|
|
@@ -524,7 +883,9 @@ export async function handleTurnEnd(deps) {
|
|
|
524
883
|
// the shared circular-dep state update is deferred/folded inside
|
|
525
884
|
// checkFilesBatch so concurrent spawns can't clobber each other (#766).
|
|
526
885
|
const absFiles = madgeFiles.map((file) => path.resolve(cwd, file));
|
|
527
|
-
const
|
|
886
|
+
const batch = await depChecker.checkFilesBatch(absFiles, cwd);
|
|
887
|
+
const depResults = batch.results;
|
|
888
|
+
madgeStats = batch.stats;
|
|
528
889
|
for (const file of madgeFiles) {
|
|
529
890
|
const absPath = path.resolve(cwd, file);
|
|
530
891
|
const depResult = depResults.get(absPath);
|
|
@@ -550,6 +911,7 @@ export async function handleTurnEnd(deps) {
|
|
|
550
911
|
filePath: cwd,
|
|
551
912
|
phase: "madge",
|
|
552
913
|
durationMs: Date.now() - t3,
|
|
914
|
+
...(madgeStats && { metadata: madgeStats }),
|
|
553
915
|
});
|
|
554
916
|
// --- Test runner: fire once per turn after all edits are done ---
|
|
555
917
|
// Runs for each unique test target across modified files; results appear
|
|
@@ -600,8 +962,33 @@ export async function handleTurnEnd(deps) {
|
|
|
600
962
|
if (targets.length > 0) {
|
|
601
963
|
dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking)`);
|
|
602
964
|
const firedAtTurn = runtime.turnIndex;
|
|
965
|
+
const firedSessionId = runtime.telemetrySessionId;
|
|
966
|
+
const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
967
|
+
const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
|
|
968
|
+
const provenanceFiles = [
|
|
969
|
+
...candidates.map((candidate) => ({ path: candidate.abs, role: "source" })),
|
|
970
|
+
...targets.map((target) => ({ path: target.testFile, role: "test" })),
|
|
971
|
+
];
|
|
972
|
+
const launchedFrom = snapshotAdvisoryProvenance({
|
|
973
|
+
cwd,
|
|
974
|
+
runtime,
|
|
975
|
+
generation: testRunGeneration,
|
|
976
|
+
files: provenanceFiles,
|
|
977
|
+
});
|
|
978
|
+
cacheManager.writeCache("test-runner-findings", { ...(priorTestCache ?? { content: "" }), testRunGeneration }, cwd);
|
|
603
979
|
Promise.allSettled(targets.map((t) => testRunnerClient.runTestFileAsync(t.testFile, cwd, t.runner, t.config)))
|
|
604
980
|
.then((results) => {
|
|
981
|
+
const publishedAgainst = snapshotAdvisoryProvenance({
|
|
982
|
+
cwd,
|
|
983
|
+
runtime,
|
|
984
|
+
generation: testRunGeneration,
|
|
985
|
+
files: provenanceFiles,
|
|
986
|
+
});
|
|
987
|
+
const superseded = launchedFrom.revision.sessionId !== publishedAgainst.revision.sessionId ||
|
|
988
|
+
launchedFrom.revision.projectSeq !== publishedAgainst.revision.projectSeq ||
|
|
989
|
+
launchedFrom.revision.turnIndex !== publishedAgainst.revision.turnIndex ||
|
|
990
|
+
launchedFrom.files.some((file, index) => publishedAgainst.files[index]?.sha256 !== file.sha256 ||
|
|
991
|
+
publishedAgainst.files[index]?.path !== file.path);
|
|
605
992
|
// #628: the turn advancing while tests ran no longer means the
|
|
606
993
|
// results are thrown away — a late result is still real
|
|
607
994
|
// information about what's currently broken. It's tagged `stale`
|
|
@@ -618,9 +1005,29 @@ export async function handleTurnEnd(deps) {
|
|
|
618
1005
|
resultValues.push(r.value);
|
|
619
1006
|
const { file, runner, passed, failed, duration, error } = r.value;
|
|
620
1007
|
const shortFile = path.basename(file);
|
|
1008
|
+
// #1479: `(0ms)` used to be printed for a run nobody
|
|
1009
|
+
// timed — a payload with no suite timestamps, an
|
|
1010
|
+
// unrecognised summary line, or an empty result — and
|
|
1011
|
+
// that is the same string a genuinely sub-millisecond
|
|
1012
|
+
// run produces. A reader could not tell "measured 0"
|
|
1013
|
+
// from "not measured", which is the confusion #1452 was
|
|
1014
|
+
// reported for. `duration` is now absent when it was
|
|
1015
|
+
// never measured, and this line says which one it has.
|
|
1016
|
+
//
|
|
1017
|
+
// #1480: the test is `formatRunDurationMs`, not an
|
|
1018
|
+
// inline comparison. The "absent = unmeasured" contract
|
|
1019
|
+
// was being re-derived at every site that read a
|
|
1020
|
+
// duration, and a site that gets it slightly wrong —
|
|
1021
|
+
// treating a measured `0` as absent — puts the bug back
|
|
1022
|
+
// without touching this comment.
|
|
1023
|
+
const elapsed = formatRunDurationMs(duration);
|
|
1024
|
+
// Lifted out of the template below for the same reason
|
|
1025
|
+
// `elapsed` is: the pair read as a nested ternary, which
|
|
1026
|
+
// this line only got flagged for because #1479 touched it.
|
|
1027
|
+
const verdict = failed > 0 ? "FAIL" : "PASS";
|
|
621
1028
|
const summary = error && passed === 0 && failed === 0
|
|
622
1029
|
? `error: ${error}`
|
|
623
|
-
: `${
|
|
1030
|
+
: `${verdict} ${passed}p/${failed}f (${elapsed})`;
|
|
624
1031
|
dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} → ${summary}`);
|
|
625
1032
|
if (failed > 0) {
|
|
626
1033
|
const formatted = testRunnerClient.formatResult(r.value);
|
|
@@ -629,13 +1036,38 @@ export async function handleTurnEnd(deps) {
|
|
|
629
1036
|
}
|
|
630
1037
|
}
|
|
631
1038
|
if (failures.length > 0) {
|
|
1039
|
+
const currentGeneration = cacheManager.readCache("test-runner-findings", cwd)?.data?.testRunGeneration;
|
|
1040
|
+
if (currentGeneration !== undefined && currentGeneration > testRunGeneration) {
|
|
1041
|
+
dbg(`turn_end: test generation ${testRunGeneration} superseded by ${currentGeneration}`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
632
1044
|
const content = stale
|
|
633
1045
|
? `[from a prior turn — the edit that triggered this run had already been superseded by the time results came back]\n\n${failures.join("\n\n")}`
|
|
634
1046
|
: failures.join("\n\n");
|
|
635
|
-
cacheManager.writeCache("test-runner-findings", {
|
|
1047
|
+
cacheManager.writeCache("test-runner-findings", {
|
|
1048
|
+
content,
|
|
1049
|
+
stale,
|
|
1050
|
+
results: resultValues,
|
|
1051
|
+
testRunGeneration,
|
|
1052
|
+
launchedFrom,
|
|
1053
|
+
publishedAgainst,
|
|
1054
|
+
provenance: publishedAgainst,
|
|
1055
|
+
superseded,
|
|
1056
|
+
}, cwd);
|
|
1057
|
+
if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
|
|
1058
|
+
clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues
|
|
1059
|
+
.filter((value) => value.failed === 0)
|
|
1060
|
+
.map((value) => value.file));
|
|
1061
|
+
mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, resultValues
|
|
1062
|
+
.filter((value) => value.failed > 0)
|
|
1063
|
+
.map((value) => value.file));
|
|
1064
|
+
}
|
|
636
1065
|
dbg(`turn_end: ${failures.length} test failure(s) cached for next context injection${stale ? " (stale — turn advanced while tests ran)" : ""}`);
|
|
637
1066
|
}
|
|
638
1067
|
else if (results.length > 0) {
|
|
1068
|
+
if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
|
|
1069
|
+
clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues.map((value) => value.file));
|
|
1070
|
+
}
|
|
639
1071
|
dbg(`turn_end: all tests passed${stale ? " (stale — turn advanced while tests ran)" : ""}`);
|
|
640
1072
|
}
|
|
641
1073
|
})
|
|
@@ -659,37 +1091,68 @@ export async function handleTurnEnd(deps) {
|
|
|
659
1091
|
// would drop the call-graph entries — so lens_diagnostics (which only reads the
|
|
660
1092
|
// persisted report) would never surface the findings (#179/#533).
|
|
661
1093
|
if (runtime.callGraph && files.length > 0) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1094
|
+
const coverage = runtime.callGraph.coverage;
|
|
1095
|
+
if (!coverage || coverage.complete !== true) {
|
|
1096
|
+
// An incomplete graph can still contain useful edges, but emitting them
|
|
1097
|
+
// as ordinary impact findings would turn unsupported/partial extraction
|
|
1098
|
+
// into an authoritative-looking clean result for the rest of the file.
|
|
1099
|
+
// Keep the limitation visible and require a complete graph for this
|
|
1100
|
+
// user-facing impact surface (#1070).
|
|
1101
|
+
advisoryParts.push("Call-graph impact was not emitted because call-graph extraction coverage is incomplete; " +
|
|
1102
|
+
"the affected files may have unreported callers.");
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
try {
|
|
1106
|
+
const { impact, formatImpact, parseSymbolKey } = await import("./call-graph.js");
|
|
1107
|
+
const { callGraphImpactToProjectDiagnostics } = await import("./project-diagnostics/runner-adapters/call-graph-impact.js");
|
|
1108
|
+
const impactLines = [];
|
|
1109
|
+
const impactFindings = [];
|
|
1110
|
+
for (const filePath of files.slice(0, 5)) {
|
|
1111
|
+
// Turn-state files may be cwd-relative while graph keys are absolute,
|
|
1112
|
+
// and persisted graphs can contain either slash style/casing. Compare
|
|
1113
|
+
// through the shared normalized path seam; keep the original filePath
|
|
1114
|
+
// only for display and diagnostics.
|
|
1115
|
+
const changedFileKey = normalizeMapKey(resolveRunnerPath(cwd, filePath));
|
|
1116
|
+
const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => {
|
|
1117
|
+
const graphFilePath = parseSymbolKey(k).filePath;
|
|
1118
|
+
return normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) === changedFileKey;
|
|
1119
|
+
});
|
|
1120
|
+
for (const calleeKey of fileCallerKeys.slice(0, 3)) {
|
|
1121
|
+
// #1080: drop KNOWN test-role callers BEFORE both the human advisory
|
|
1122
|
+
// (formatImpact below) and the persisted delta (impactFindings →
|
|
1123
|
+
// callGraphImpactToProjectDiagnostics) — the advisory is rendered
|
|
1124
|
+
// first, so the filter must reach the shared `results` set that feeds
|
|
1125
|
+
// both. A test caller supplied by an old/fixture/expanded graph must
|
|
1126
|
+
// appear in neither surface. Fail-open: an unparseable/unclassifiable
|
|
1127
|
+
// key is retained (the adapter re-applies the same predicate).
|
|
1128
|
+
const results = impact(runtime.callGraph, calleeKey).filter((r) => {
|
|
1129
|
+
const callerFile = parseSymbolKey(r.symbolKey).filePath;
|
|
1130
|
+
return (!callerFile ||
|
|
1131
|
+
!isTestRoleCollateral(resolveRunnerPath(cwd, callerFile)));
|
|
1132
|
+
});
|
|
1133
|
+
if (results.length > 0) {
|
|
1134
|
+
impactFindings.push({ calleeKey, results });
|
|
1135
|
+
const summary = formatImpact(results, cwd);
|
|
1136
|
+
if (summary)
|
|
1137
|
+
impactLines.push(` ${parseSymbolKey(calleeKey).symbolName ?? calleeKey}: ${summary}`);
|
|
1138
|
+
}
|
|
677
1139
|
}
|
|
678
1140
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1141
|
+
if (impactLines.length > 0) {
|
|
1142
|
+
advisoryParts.push(`📊 Call-graph impact (changed symbols have callers):\n${impactLines.join("\n")}`);
|
|
1143
|
+
}
|
|
1144
|
+
if (impactFindings.length > 0) {
|
|
1145
|
+
const impactDiagnostics = callGraphImpactToProjectDiagnostics(cwd, impactFindings);
|
|
1146
|
+
if (impactDiagnostics.length > 0) {
|
|
1147
|
+
projectDiagnosticsDelta.push(...impactDiagnostics);
|
|
1148
|
+
projectDiagnosticsSources.add("call-graph");
|
|
1149
|
+
}
|
|
688
1150
|
}
|
|
1151
|
+
// Non-fatal — call graph is best-effort
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
// Non-fatal — call graph is best-effort
|
|
689
1155
|
}
|
|
690
|
-
}
|
|
691
|
-
catch {
|
|
692
|
-
// Non-fatal — call graph is best-effort
|
|
693
1156
|
}
|
|
694
1157
|
}
|
|
695
1158
|
if (projectDiagnosticsDelta.length > 0) {
|
|
@@ -818,27 +1281,7 @@ export async function handleTurnEnd(deps) {
|
|
|
818
1281
|
},
|
|
819
1282
|
});
|
|
820
1283
|
}
|
|
821
|
-
|
|
822
|
-
// advisory (project-wide unused symbols are slow to compute, so we read the
|
|
823
|
-
// cache rather than re-scanning every turn — the analogue of knip's cache
|
|
824
|
-
// for non-JS/TS languages). Merged across languages for polyglot repos.
|
|
825
|
-
try {
|
|
826
|
-
const deadCodeResults = [];
|
|
827
|
-
for (const client of deadCodeClients) {
|
|
828
|
-
if (!client.detect(cwd))
|
|
829
|
-
continue;
|
|
830
|
-
const cached = cacheManager.readCache(`dead-code-${client.id}`, cwd);
|
|
831
|
-
if (cached?.data)
|
|
832
|
-
deadCodeResults.push(cached.data);
|
|
833
|
-
}
|
|
834
|
-
const advisory = formatDeadCodeAdvisory(deadCodeResults);
|
|
835
|
-
if (advisory)
|
|
836
|
-
advisoryParts.push(advisory);
|
|
837
|
-
}
|
|
838
|
-
catch (err) {
|
|
839
|
-
dbg(`turn_end: dead-code advisory failed: ${err}`);
|
|
840
|
-
}
|
|
841
|
-
cacheManager.incrementTurnCycle(cwd);
|
|
1284
|
+
cacheManager.incrementTurnCycle(cwd, currentOwner);
|
|
842
1285
|
const labeledAdvisoryParts = advisoryParts.map((p) => `ℹ️ Advisory — no action required this turn:\n${p}`);
|
|
843
1286
|
const findingParts = [...blockerParts, ...labeledAdvisoryParts];
|
|
844
1287
|
if (findingParts.length > 0) {
|
|
@@ -852,12 +1295,88 @@ export async function handleTurnEnd(deps) {
|
|
|
852
1295
|
if (last?.data?.signature === signature &&
|
|
853
1296
|
last?.data?.sessionId === runtime.telemetrySessionId) {
|
|
854
1297
|
dbg("turn_end: duplicate findings detected (same session), suppressing re-prompt");
|
|
855
|
-
|
|
1298
|
+
if (getFlag("lens-guard")) {
|
|
1299
|
+
const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
1300
|
+
if (existingGuard) {
|
|
1301
|
+
writeGitGuardRecord(cacheManager, runtime, cwd, {
|
|
1302
|
+
...existingGuard,
|
|
1303
|
+
content,
|
|
1304
|
+
blockerContent: blockerParts.length > 0
|
|
1305
|
+
? capTurnEndMessage(blockerParts.join("\n\n"))
|
|
1306
|
+
: undefined,
|
|
1307
|
+
hasBlockers: blockerParts.length > 0 || existingGuard.testFailures === true,
|
|
1308
|
+
blockingFiles: blockerParts.length > 0 ? existingGuard.affectedFiles : undefined,
|
|
1309
|
+
projectSeqStart: runtime.turnStartProjectSeq,
|
|
1310
|
+
projectSeqEnd: runtime.projectSeq,
|
|
1311
|
+
fileSeqByPath: Object.fromEntries(runtime.getFileSeqEntries().map(([filePath, seq]) => [normalizeMapKey(path.resolve(filePath)), seq])),
|
|
1312
|
+
fileContentHashes: {},
|
|
1313
|
+
consumed: false,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
cacheManager.clearTurnState(cwd, currentOwner);
|
|
856
1318
|
runtime.fixedThisTurn.clear();
|
|
857
1319
|
resetFormatService();
|
|
858
1320
|
return;
|
|
859
1321
|
}
|
|
860
|
-
|
|
1322
|
+
const fileSeqByPath = {};
|
|
1323
|
+
for (const [filePath, seq] of runtime.getFileSeqEntries()) {
|
|
1324
|
+
fileSeqByPath[normalizeMapKey(path.resolve(filePath))] = seq;
|
|
1325
|
+
}
|
|
1326
|
+
if (getFlag("lens-guard")) {
|
|
1327
|
+
const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
1328
|
+
const blockingContent = blockerParts.length > 0
|
|
1329
|
+
? capTurnEndMessage(blockerParts.join("\n\n"))
|
|
1330
|
+
: undefined;
|
|
1331
|
+
const affectedFiles = [
|
|
1332
|
+
...(existingGuard?.affectedFiles ?? []),
|
|
1333
|
+
...files.map((file) => resolveRunnerPath(cwd, file)),
|
|
1334
|
+
...cascadeResults.flatMap((result) => result.neighbors
|
|
1335
|
+
.filter((neighbor) => neighbor.diagnostics.length > 0)
|
|
1336
|
+
.map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
|
|
1337
|
+
];
|
|
1338
|
+
writeGitGuardRecord(cacheManager, runtime, cwd, {
|
|
1339
|
+
content: [content, existingGuard?.testFailureContent]
|
|
1340
|
+
.filter((value) => !!value)
|
|
1341
|
+
.join("\n\n"),
|
|
1342
|
+
blockerContent: blockingContent,
|
|
1343
|
+
blockingFiles: blockerParts.length > 0 ? affectedFiles : undefined,
|
|
1344
|
+
hasBlockers: !!blockingContent || existingGuard?.testFailures === true,
|
|
1345
|
+
affectedFiles,
|
|
1346
|
+
sessionId: runtime.telemetrySessionId,
|
|
1347
|
+
projectSeqStart: runtime.turnStartProjectSeq,
|
|
1348
|
+
projectSeqEnd: runtime.projectSeq,
|
|
1349
|
+
fileSeqByPath,
|
|
1350
|
+
fileContentHashes: {},
|
|
1351
|
+
consumed: false,
|
|
1352
|
+
testFailures: existingGuard?.testFailures,
|
|
1353
|
+
testFailureContent: existingGuard?.testFailureContent,
|
|
1354
|
+
testFailureFiles: existingGuard?.testFailureFiles,
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
const allAffectedFiles = [
|
|
1359
|
+
...files.map((file) => resolveRunnerPath(cwd, file)),
|
|
1360
|
+
...cascadeResults.flatMap((result) => result.neighbors
|
|
1361
|
+
.filter((neighbor) => neighbor.diagnostics.length > 0)
|
|
1362
|
+
.map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
|
|
1363
|
+
];
|
|
1364
|
+
const affectedFiles = [...new Set(allAffectedFiles)]
|
|
1365
|
+
.slice(0, MAX_ADVISORY_AFFECTED_FILES);
|
|
1366
|
+
const affectedFilesTruncated = new Set(allAffectedFiles).size > affectedFiles.length;
|
|
1367
|
+
cacheManager.writeCache("turn-end-findings", {
|
|
1368
|
+
content,
|
|
1369
|
+
affectedFiles,
|
|
1370
|
+
affectedFilesTruncated,
|
|
1371
|
+
provenance: snapshotAdvisoryProvenance({
|
|
1372
|
+
cwd,
|
|
1373
|
+
runtime,
|
|
1374
|
+
generation: 0,
|
|
1375
|
+
files: affectedFiles.map((file) => ({ path: file, role: "affected" })),
|
|
1376
|
+
truncated: affectedFilesTruncated,
|
|
1377
|
+
}),
|
|
1378
|
+
}, cwd);
|
|
1379
|
+
}
|
|
861
1380
|
cacheManager.writeCache("turn-end-findings-last", {
|
|
862
1381
|
signature,
|
|
863
1382
|
sessionId: runtime.telemetrySessionId,
|
|
@@ -875,7 +1394,14 @@ export async function handleTurnEnd(deps) {
|
|
|
875
1394
|
});
|
|
876
1395
|
}
|
|
877
1396
|
if (blockerParts.length === 0) {
|
|
878
|
-
cacheManager.clearTurnState(cwd);
|
|
1397
|
+
cacheManager.clearTurnState(cwd, currentOwner);
|
|
1398
|
+
if (getFlag("lens-guard") && advisoryParts.length === 0 && !runtime.gitGuardHasBlockers) {
|
|
1399
|
+
const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
1400
|
+
if (guardRecord?.sessionId === runtime.telemetrySessionId &&
|
|
1401
|
+
guardRecord.testFailures !== true) {
|
|
1402
|
+
cacheManager.clearCache("turn-end-findings", cwd);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
879
1405
|
}
|
|
880
1406
|
runtime.fixedThisTurn.clear();
|
|
881
1407
|
runtime.clearActionableWarnings();
|