@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
|
@@ -3,23 +3,93 @@ import { appendActionableWarningsHistory, buildActionableWarningsReport, formatA
|
|
|
3
3
|
import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
|
|
4
4
|
import { appendCodeQualityWarningsHistory, buildCodeQualityWarningsReport, formatCodeQualityWarningsAdvisory, writeCodeQualityWarningsReport, } from "./code-quality-warnings.js";
|
|
5
5
|
import { clearGitGuardTestFailure, mergeGitGuardTestFailure, writeGitGuardRecord, } from "./git-guard.js";
|
|
6
|
+
import { cascadeSettleWaitMs } from "./cascade-budget.js";
|
|
6
7
|
import { logCascade } from "./cascade-logger.js";
|
|
7
8
|
import { normalizeMapKey } from "./path-utils.js";
|
|
8
9
|
import { resolveRunnerPath, toRunnerDisplayPath, } from "./dispatch/runner-context.js";
|
|
9
10
|
import { getKnipIgnorePatterns } from "./file-utils.js";
|
|
11
|
+
import { formatCacheAgeLabel } from "./finding-delivery-gate.js";
|
|
12
|
+
import { getFullScanWallClockMs, isWorkspaceSweepActive, runWhenWorkspaceSweepIdle, SWEEP_IDLE_SAFETY_MARGIN_MS, } from "./lsp/workspace-sweep-hold.js";
|
|
10
13
|
import { isTestRoleCollateral } from "./collateral-test-role.js";
|
|
11
14
|
import { dedupeSecretFindings, fromAstGrepWarnings, fromGitleaks, fromTrivySecrets, isSecretWarning, secretLocationKey, } from "./secret-findings.js";
|
|
12
|
-
import {
|
|
15
|
+
import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, stableFindingKey, } from "./dead-code-client.js";
|
|
16
|
+
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
13
17
|
import { PROJECT_DIAGNOSTICS_CACHE_VERSION, writeProjectDiagnosticsDeltaReport, } from "./project-diagnostics/cache.js";
|
|
18
|
+
import { deadCodeIssueToProjectDiagnostic } from "./project-diagnostics/runner-adapters/dead-code.js";
|
|
19
|
+
import { gitleaksFindingToProjectDiagnostic } from "./project-diagnostics/runner-adapters/gitleaks.js";
|
|
20
|
+
import { govulncheckFindingToProjectDiagnostic } from "./project-diagnostics/runner-adapters/govulncheck.js";
|
|
21
|
+
import { trivyFindingToProjectDiagnostic, trivySecretFindingToProjectDiagnostic, } from "./project-diagnostics/runner-adapters/trivy.js";
|
|
14
22
|
import { knipIssuesToProjectDiagnostics } from "./project-diagnostics/runner-adapters/knip.js";
|
|
23
|
+
import { applyDispositionsMultiFile } from "./diagnostic-dispositions.js";
|
|
15
24
|
import { logLatency } from "./latency-logger.js";
|
|
16
25
|
import { getLspBudgetIdleTimeoutMs, shouldShortenLspIdleTimeout, } from "./lsp-budget.js";
|
|
17
26
|
import { updateHeartbeat } from "./instance-registry.js";
|
|
18
27
|
import { emitLensTurnFindings } from "./lens-events.js";
|
|
19
28
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
20
29
|
import { isSubagentSession } from "./subagent-mode.js";
|
|
30
|
+
import { formatRunDurationMs } from "./run-duration.js";
|
|
31
|
+
import { MAX_ADVISORY_AFFECTED_FILES, gateFindingsByPathFreshness, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
|
|
32
|
+
import { sweepInlineBlockerFreshness } from "./blocker-freshness.js";
|
|
33
|
+
import { sweepInlineBlockerPastEof } from "./blocker-past-eof.js";
|
|
34
|
+
// #1631 review V2: moved to its own leaf module so a low-level store
|
|
35
|
+
// (widget-state.ts) can use the marker without importing this orchestrator â
|
|
36
|
+
// see clients/stale-marker.ts's doc comment.
|
|
37
|
+
import { STALE_LINE_MARKER } from "./stale-marker.js";
|
|
38
|
+
import { getWidgetBlockingFilesForSweep, markWidgetFileBlockersStale, } from "./widget-state.js";
|
|
39
|
+
/**
|
|
40
|
+
* #1617: turn_end reads gitleaks/govulncheck/trivy straight from their
|
|
41
|
+
* session-scan caches and formats them into advisory/blocker text â a
|
|
42
|
+
* reporting lane parallel to (and, before this fix, entirely bypassing)
|
|
43
|
+
* `dispatcher.ts:924`'s `applyDispositions` filter. An agent-marked
|
|
44
|
+
* false-positive/won't-fix on one of these findings never suppressed it
|
|
45
|
+
* here, so it re-reported on every turn.
|
|
46
|
+
*
|
|
47
|
+
* Filters `findings` through the SAME anchor derivation the dispatch path
|
|
48
|
+
* and `lens_diagnostics mode=full` use (`applyDispositionsMultiFile` in
|
|
49
|
+
* `diagnostic-dispositions.ts`), keyed off each lane's own canonical
|
|
50
|
+
* `ProjectDiagnostic` adapter (`toDiagnostic`) â the exact tool/rule/message
|
|
51
|
+
* identity `lens_diagnostics` already surfaces and `lens_diagnostic_mark`
|
|
52
|
+
* already anchors a mark against, not a second, cloned identity that would
|
|
53
|
+
* silently diverge from what the agent actually marked.
|
|
54
|
+
*
|
|
55
|
+
* Returns the surviving findings plus how many were dropped, so a caller can
|
|
56
|
+
* still surface a "suppressed by disposition: N" trace (the #1616
|
|
57
|
+
* suppressed-bucket rule â a security finding must never vanish with no
|
|
58
|
+
* trace, even when the disposition that dropped it is working as intended).
|
|
59
|
+
*/
|
|
60
|
+
function filterFindingsByDisposition(findings, cwd, toDiagnostic) {
|
|
61
|
+
if (findings.length === 0)
|
|
62
|
+
return { kept: findings, suppressed: 0 };
|
|
63
|
+
const candidates = findings.map((finding) => ({
|
|
64
|
+
finding,
|
|
65
|
+
diagnostic: toDiagnostic(finding),
|
|
66
|
+
}));
|
|
67
|
+
const survivors = new Set(applyDispositionsMultiFile(candidates.map((c) => c.diagnostic), cwd, (d) => d.filePath));
|
|
68
|
+
const kept = candidates
|
|
69
|
+
.filter((c) => survivors.has(c.diagnostic))
|
|
70
|
+
.map((c) => c.finding);
|
|
71
|
+
return { kept, suppressed: findings.length - kept.length };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Would writing `next` over `prev` throw away a good scan for a failed one?
|
|
75
|
+
*
|
|
76
|
+
* A failed run carries no findings. Writing it evicts the last good result, and
|
|
77
|
+
* every later reader then serves the failure as the answer â a 194-byte "not
|
|
78
|
+
* available" record replaced 149 KB of real findings in every dogfood project
|
|
79
|
+
* (#925, #1467). Callers keep the previous cache when this returns true.
|
|
80
|
+
*/
|
|
81
|
+
function wouldPoisonCache(prev, next) {
|
|
82
|
+
return !next.success && prev?.data.success === true;
|
|
83
|
+
}
|
|
21
84
|
// LSP idle reset scheduling â prevents thrashing by delaying shutdown
|
|
22
85
|
let lspIdleResetTimeout = null;
|
|
86
|
+
// #1618: set while this timer's fire is deferred behind an in-flight
|
|
87
|
+
// workspace sweep (see `scheduleLSPIdleReset`'s `isWorkspaceSweepActive`
|
|
88
|
+
// branch). `cancelLSPIdleReset` must be able to cancel THIS too â otherwise
|
|
89
|
+
// an active-editing turn that cancels idle reset while a sweep is still
|
|
90
|
+
// running would have it silently resurrected once the sweep finishes, even
|
|
91
|
+
// though the session is no longer idle.
|
|
92
|
+
let pendingSweepRearm = null;
|
|
23
93
|
function emitIdleResetReporterWarning(reportErr) {
|
|
24
94
|
try {
|
|
25
95
|
process.emitWarning(`pi-lens LSP idle reset error reporter failed: ${reportErr}`, { code: "PI_LENS_LSP_IDLE_RESET_REPORTER_FAILED" });
|
|
@@ -38,12 +108,40 @@ function reportIdleResetError(onError, err) {
|
|
|
38
108
|
}
|
|
39
109
|
}
|
|
40
110
|
function scheduleLSPIdleReset(resetFn, delayMs, options = {}) {
|
|
41
|
-
// Clear any pending reset to avoid multiple timers
|
|
111
|
+
// Clear any pending reset to avoid multiple timers. #1618: also cancel a
|
|
112
|
+
// rearm still waiting on a prior sweep's hold â otherwise re-scheduling
|
|
113
|
+
// here (this call) leaves that OLD waiter armed too, and the sweep's
|
|
114
|
+
// eventual release would fire a SECOND, independent `scheduleLSPIdleReset`
|
|
115
|
+
// alongside this fresh one.
|
|
42
116
|
if (lspIdleResetTimeout) {
|
|
43
117
|
clearTimeout(lspIdleResetTimeout);
|
|
44
118
|
}
|
|
119
|
+
if (pendingSweepRearm) {
|
|
120
|
+
pendingSweepRearm.cancelled = true;
|
|
121
|
+
pendingSweepRearm = null;
|
|
122
|
+
}
|
|
45
123
|
lspIdleResetTimeout = setTimeout(() => {
|
|
46
124
|
lspIdleResetTimeout = null;
|
|
125
|
+
// #1618: a full workspace sweep (`lens_diagnostics mode=full`) grants
|
|
126
|
+
// itself a wall-clock ceiling that can outlive this timer's delay â this
|
|
127
|
+
// used to fire straight into an in-flight sweep and destroy the very
|
|
128
|
+
// service the sweep was actively touching, mislabeling every file the
|
|
129
|
+
// sweep had not yet reached as budget exhaustion. Defer instead of
|
|
130
|
+
// firing: re-arm a FRESH `delayMs` timer once the sweep releases its
|
|
131
|
+
// hold, rather than resuming a countdown that's already elapsed (which
|
|
132
|
+
// would fire the instant the hold releases) or destroying mid-sweep.
|
|
133
|
+
if (isWorkspaceSweepActive()) {
|
|
134
|
+
const rearmToken = { cancelled: false };
|
|
135
|
+
pendingSweepRearm = rearmToken;
|
|
136
|
+
runWhenWorkspaceSweepIdle(() => {
|
|
137
|
+
if (rearmToken.cancelled)
|
|
138
|
+
return;
|
|
139
|
+
if (pendingSweepRearm === rearmToken)
|
|
140
|
+
pendingSweepRearm = null;
|
|
141
|
+
scheduleLSPIdleReset(resetFn, delayMs, options);
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
47
145
|
try {
|
|
48
146
|
if (options.isCurrentSession && !options.isCurrentSession()) {
|
|
49
147
|
return;
|
|
@@ -62,17 +160,58 @@ function scheduleLSPIdleReset(resetFn, delayMs, options = {}) {
|
|
|
62
160
|
// exit after completing its work, not wait 240 seconds for this to fire)
|
|
63
161
|
lspIdleResetTimeout.unref();
|
|
64
162
|
}
|
|
163
|
+
// #1618 acceptance criterion 6: FULL_SCAN_WALL_CLOCK_MS (the full-sweep wall
|
|
164
|
+
// clock ceiling, `tools/lens-diagnostics.ts`) must stay under EVERY idle
|
|
165
|
+
// reset delay this module can arm â derived, not asserted, so the constants
|
|
166
|
+
// can't drift back into a relationship where a still-running sweep can
|
|
167
|
+
// outlive the timer. The AC1 hold above already makes a mid-sweep fire
|
|
168
|
+
// impossible regardless of this margin; this is defense in depth against a
|
|
169
|
+
// future caller that touches the LSP service outside
|
|
170
|
+
// `runWorkspaceDiagnostics`' hold. `SWEEP_IDLE_SAFETY_MARGIN_MS` is
|
|
171
|
+
// single-sourced from `workspace-sweep-hold.ts`, which also uses it for its
|
|
172
|
+
// own max-hold-age failsafe â one tunable, not two.
|
|
173
|
+
const DEFAULT_LSP_IDLE_RESET_MS = 240_000;
|
|
174
|
+
function sweepDerivedFloorMs() {
|
|
175
|
+
return getFullScanWallClockMs() + SWEEP_IDLE_SAFETY_MARGIN_MS;
|
|
176
|
+
}
|
|
177
|
+
/** The normal (non-subagent, non-budget-pressured) idle-reset delay. */
|
|
178
|
+
function getBaseLspIdleResetMs() {
|
|
179
|
+
return Math.max(DEFAULT_LSP_IDLE_RESET_MS, sweepDerivedFloorMs());
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* #1618 (R4): the subagent-light (#713) and cross-process-budget-pressured
|
|
183
|
+
* (#449) paths used to arm a flat, much SHORTER delay (60s default) than the
|
|
184
|
+
* sweep's own 300s ceiling â a 5:1 inversion covered only by the AC1 hold.
|
|
185
|
+
* Deriving this path too means AC6 ("the sweep's ceiling stays under every
|
|
186
|
+
* idle-reset delay") holds universally, not just for the common path, and an
|
|
187
|
+
* env override to either constant can never invert it (`Math.max` floors at
|
|
188
|
+
* the derived value no matter how small the override pushes the other side).
|
|
189
|
+
*
|
|
190
|
+
* Accepted cost (deliberate, not incidental â see R6 in the PR body): under
|
|
191
|
+
* default settings this now ALSO arms the ~360s derived floor rather than a
|
|
192
|
+
* true 60s teardown, trading some of #713's "release a short-lived
|
|
193
|
+
* subagent's fleet fast" benefit for AC6 holding without exceptions.
|
|
194
|
+
*/
|
|
195
|
+
function getShortenedLspIdleResetMs() {
|
|
196
|
+
return Math.max(getLspBudgetIdleTimeoutMs(), sweepDerivedFloorMs());
|
|
197
|
+
}
|
|
198
|
+
/** The idle-reset delay `handleTurnEnd` actually arms on a file-less turn â
|
|
199
|
+
* exported so tests assert against the REAL computed value instead of a
|
|
200
|
+
* hand-derived literal that can silently drift from this function. */
|
|
201
|
+
export function getEffectiveLspIdleResetMs() {
|
|
202
|
+
return isSubagentSession() || shouldShortenLspIdleTimeout()
|
|
203
|
+
? getShortenedLspIdleResetMs()
|
|
204
|
+
: getBaseLspIdleResetMs();
|
|
205
|
+
}
|
|
65
206
|
export function cancelLSPIdleReset() {
|
|
66
207
|
if (lspIdleResetTimeout) {
|
|
67
208
|
clearTimeout(lspIdleResetTimeout);
|
|
68
209
|
lspIdleResetTimeout = null;
|
|
69
210
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const raw = Number(process.env.PI_LENS_CASCADE_SETTLE_WAIT_MS);
|
|
75
|
-
return Number.isFinite(raw) && raw >= 0 ? raw : 5000;
|
|
211
|
+
if (pendingSweepRearm) {
|
|
212
|
+
pendingSweepRearm.cancelled = true;
|
|
213
|
+
pendingSweepRearm = null;
|
|
214
|
+
}
|
|
76
215
|
}
|
|
77
216
|
function capTurnEndMessage(content) {
|
|
78
217
|
const maxLines = RUNTIME_CONFIG.turnEnd.maxLines;
|
|
@@ -132,7 +271,15 @@ export async function handleTurnEnd(deps) {
|
|
|
132
271
|
turnState = cacheManager.readTurnState(cwd);
|
|
133
272
|
}
|
|
134
273
|
const files = Object.keys(turnState.files);
|
|
135
|
-
|
|
274
|
+
// R1 (#1443 follow-up): a read-only turn (no files touched) must not take
|
|
275
|
+
// the fast idle-reset path while a carried cascade run â or one still
|
|
276
|
+
// settling â is waiting for its delivery opportunity. Falling through to
|
|
277
|
+
// the normal pipeline lets the settle/drain/merge logic below run exactly
|
|
278
|
+
// as it does for an edit turn, so a carried finding reaches the agent
|
|
279
|
+
// instead of dying unrendered. `hasCascadeRuns()` is a cheap peek (no
|
|
280
|
+
// pending work almost every turn), so the common read-only turn still
|
|
281
|
+
// takes the early return below.
|
|
282
|
+
if (files.length === 0 && !runtime.hasCascadeRuns()) {
|
|
136
283
|
// A genuinely clean session must invalidate the persisted guard record.
|
|
137
284
|
// Blocker records are retained only while the runtime still reports one.
|
|
138
285
|
if (getFlag("lens-guard") && !runtime.gitGuardHasBlockers) {
|
|
@@ -142,14 +289,15 @@ export async function handleTurnEnd(deps) {
|
|
|
142
289
|
cacheManager.clearCache("turn-end-findings", cwd);
|
|
143
290
|
}
|
|
144
291
|
}
|
|
145
|
-
// #713: subagent sessions use a shorter idle reset (60s) â a
|
|
146
|
-
// task agent holding a warm fleet for 4 minutes after its
|
|
147
|
-
// pure waste under fan-out. Classify ONCE here so every
|
|
148
|
-
// path shares the same answer. PI_LENS_SUBAGENT_FULL=1
|
|
149
|
-
// isSubagentSession() returning false.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
292
|
+
// #713: subagent sessions use a shorter idle reset (nominally 60s) â a
|
|
293
|
+
// short-lived task agent holding a warm fleet for 4 minutes after its
|
|
294
|
+
// last turn is pure waste under fan-out. Classify ONCE here so every
|
|
295
|
+
// tick in this call path shares the same answer. PI_LENS_SUBAGENT_FULL=1
|
|
296
|
+
// restores the base delay via isSubagentSession() returning false.
|
|
297
|
+
// #1618: both branches route through `getEffectiveLspIdleResetMs` so
|
|
298
|
+
// AC6's derivation applies universally â see that function's doc for
|
|
299
|
+
// why the "shorter" path is not always literally 60s anymore.
|
|
300
|
+
const idleResetMs = getEffectiveLspIdleResetMs();
|
|
153
301
|
dbg(`turn_end: no modified files, scheduling LSP idle reset (${idleResetMs / 1000}s)`);
|
|
154
302
|
if (!getFlag("no-lsp")) {
|
|
155
303
|
const sessionGeneration = runtime.sessionGeneration;
|
|
@@ -161,8 +309,14 @@ export async function handleTurnEnd(deps) {
|
|
|
161
309
|
resetFormatService();
|
|
162
310
|
return;
|
|
163
311
|
}
|
|
164
|
-
// Cancel any pending idle reset since we're actively working
|
|
165
|
-
|
|
312
|
+
// Cancel any pending idle reset since we're actively working. #1618: also
|
|
313
|
+
// checks `pendingSweepRearm` â a timer deferred behind an in-flight
|
|
314
|
+
// workspace sweep already nulled `lspIdleResetTimeout` (the setTimeout
|
|
315
|
+
// callback clears it before checking the hold), so this guard used to
|
|
316
|
+
// read "nothing pending" and skip the cancel while a rearm was still
|
|
317
|
+
// queued to fire the instant the sweep released its hold â resurrecting
|
|
318
|
+
// idle reset on a session that had since gone back to active editing.
|
|
319
|
+
if (lspIdleResetTimeout || pendingSweepRearm) {
|
|
166
320
|
cancelLSPIdleReset();
|
|
167
321
|
dbg("turn_end: cancelled pending LSP idle reset (active editing)");
|
|
168
322
|
}
|
|
@@ -176,22 +330,91 @@ export async function handleTurnEnd(deps) {
|
|
|
176
330
|
}
|
|
177
331
|
const turnEndStart = Date.now();
|
|
178
332
|
const blockerParts = [];
|
|
333
|
+
/**
|
|
334
|
+
* #1622 review M2: findings the freshness gate demoted. A third tier between
|
|
335
|
+
* blockers and advisories â not a blocker, because the cached coordinate is
|
|
336
|
+
* untrustworthy; not an advisory, because the advisory label reads "no action
|
|
337
|
+
* required this turn" and these DO require a re-scan. Each part carries its
|
|
338
|
+
* own imperative preamble rather than inheriting that label.
|
|
339
|
+
*/
|
|
340
|
+
const staleSecretParts = [];
|
|
179
341
|
const advisoryParts = [];
|
|
180
342
|
const projectDiagnosticsDelta = [];
|
|
181
343
|
const projectDiagnosticsSources = new Set();
|
|
344
|
+
// #1641: past-EOF gate. Runs BEFORE the dependency-drift sweep below â a
|
|
345
|
+
// cheap statSync per cited file is worth paying first so the pricier
|
|
346
|
+
// import-parsing sweep can skip anything already taken out of the
|
|
347
|
+
// authoritative channel this turn (see blocker-past-eof.ts's module doc
|
|
348
|
+
// for the full composition rule with #1631's gate).
|
|
349
|
+
const blockerPastEofStart = Date.now();
|
|
350
|
+
const blockerPastEof = sweepInlineBlockerPastEof(runtime, cwd);
|
|
351
|
+
logLatency({
|
|
352
|
+
type: "phase",
|
|
353
|
+
toolName: "turn_end",
|
|
354
|
+
filePath: cwd,
|
|
355
|
+
phase: "blocker_past_eof_sweep",
|
|
356
|
+
durationMs: Date.now() - blockerPastEofStart,
|
|
357
|
+
metadata: {
|
|
358
|
+
total: blockerPastEof.total,
|
|
359
|
+
checked: blockerPastEof.checked,
|
|
360
|
+
demoted: blockerPastEof.demoted,
|
|
361
|
+
healed: blockerPastEof.healed,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
// #1631: freshness gate. A cached blocker is a verdict about the file AND
|
|
365
|
+
// everything it imports; before re-serving it, sweep for out-of-band drift of
|
|
366
|
+
// the file or its forward imports and demote drifted entries to a
|
|
367
|
+
// `[stale â re-run to confirm]` advisory instead of re-asserting them at full
|
|
368
|
+
// authority (#1419 demote-not-drop).
|
|
369
|
+
const blockerFreshnessStart = Date.now();
|
|
370
|
+
// #1790: widen the sweep's population with widget-store rows a cache-served
|
|
371
|
+
// replay populated without ever touching RuntimeCoordinator's inline-blocker
|
|
372
|
+
// map â see blocker-freshness.ts's `WidgetSweepBlockerEntry` doc for why this
|
|
373
|
+
// is injected here rather than imported by blocker-freshness.ts itself.
|
|
374
|
+
const blockerFreshness = await sweepInlineBlockerFreshness(runtime, cwd, {
|
|
375
|
+
additionalEntries: getWidgetBlockingFilesForSweep().map((row) => ({
|
|
376
|
+
filePath: row.filePath,
|
|
377
|
+
recordedAtMs: row.recordedAtMs,
|
|
378
|
+
demote: () => markWidgetFileBlockersStale(row.filePath, "dependency-drift"),
|
|
379
|
+
})),
|
|
380
|
+
});
|
|
381
|
+
logLatency({
|
|
382
|
+
type: "phase",
|
|
383
|
+
toolName: "turn_end",
|
|
384
|
+
filePath: cwd,
|
|
385
|
+
phase: "blocker_freshness_sweep",
|
|
386
|
+
durationMs: Date.now() - blockerFreshnessStart,
|
|
387
|
+
metadata: {
|
|
388
|
+
total: blockerFreshness.total,
|
|
389
|
+
kept: blockerFreshness.kept,
|
|
390
|
+
revalidated: blockerFreshness.revalidated,
|
|
391
|
+
alreadyStale: blockerFreshness.alreadyStale,
|
|
392
|
+
truncatedImports: blockerFreshness.truncatedImports,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
182
395
|
// Re-surface inline blockers from this turn that the agent didn't fix.
|
|
183
396
|
// These were shown inline during write/edit but the agent moved on without resolving them.
|
|
184
397
|
const unresolvedBlockers = runtime.getInlineBlockersSnapshot();
|
|
185
|
-
for (const { filePath: bPath, summary } of unresolvedBlockers) {
|
|
398
|
+
for (const { filePath: bPath, summary, stale } of unresolvedBlockers) {
|
|
186
399
|
const displayPath = toRunnerDisplayPath(cwd, bPath);
|
|
187
|
-
|
|
400
|
+
if (stale) {
|
|
401
|
+
// #1631: demoted â out of the authoritative blocker channel and into the
|
|
402
|
+
// advisory channel with a stale marker, so the agent is told to re-run
|
|
403
|
+
// rather than pressured by a verdict that may already be resolved.
|
|
404
|
+
// @delivery-surface: runtime-turn:unresolved-inline-blocker
|
|
405
|
+
advisoryParts.push(`${STALE_LINE_MARKER} ${displayPath}:\n${summary}`);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// @delivery-surface: runtime-turn:unresolved-inline-blocker
|
|
409
|
+
blockerParts.push(`Unresolved from this turn â ${displayPath}:\n${summary}`);
|
|
410
|
+
}
|
|
188
411
|
}
|
|
189
412
|
// Drain the deferred cascade computes kicked off this turn (#450). They ran
|
|
190
413
|
// concurrently off the write hot path; wait a bounded time for them here so
|
|
191
414
|
// their runs are available to the merge below. A compute still in flight at
|
|
192
415
|
// the cap is carried over to the next turn_end (never dropped).
|
|
193
416
|
const cascadeSettleStart = Date.now();
|
|
194
|
-
const { settled, timedOut } = await runtime.settleCascadeRuns(cascadeSettleWaitMs());
|
|
417
|
+
const { settled, timedOut } = await runtime.settleCascadeRuns(cascadeSettleWaitMs(), { trackTurnEndClock: true });
|
|
195
418
|
logLatency({
|
|
196
419
|
type: "phase",
|
|
197
420
|
toolName: "turn_end",
|
|
@@ -209,12 +432,72 @@ export async function handleTurnEnd(deps) {
|
|
|
209
432
|
const cascadeRuns = runtime.consumeCascadeRuns().filter((run) => {
|
|
210
433
|
const originSeq = run.origin?.projectSeq;
|
|
211
434
|
const originTurn = run.origin?.turnSeq;
|
|
212
|
-
// A deferred result from
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
435
|
+
// A deferred result from AFTER a later write is not current state. Old test
|
|
436
|
+
// fixtures without provenance remain accepted for compatibility.
|
|
437
|
+
//
|
|
438
|
+
// #1443: `turnSeq` alone is NOT a supersede signal, and it used to be an
|
|
439
|
+
// unconditional reject. Every LATE run â one whose compute missed the
|
|
440
|
+
// settle cap and was re-parked by `settleCascadeRuns`, and one the
|
|
441
|
+
// quiet-window reconcile appended after this turn's predecessor already
|
|
442
|
+
// consumed (carried across turn_start by `beginTurn`) â is BY DEFINITION
|
|
443
|
+
// from an earlier turn, so `originTurn === runtime.turnIndex` was always
|
|
444
|
+
// false for exactly the runs the carry-over was built to preserve. Both
|
|
445
|
+
// producers' contracts were dead code: the measured cases were the two
|
|
446
|
+
// highest-fan-out cascades of the day (38 and 40 neighbours).
|
|
447
|
+
//
|
|
448
|
+
// R2 (#1443 follow-up): `projectSeq` alone is NOT a per-file supersede
|
|
449
|
+
// signal â it is GLOBAL, advancing on every pi-observed write anywhere in
|
|
450
|
+
// the project. Rejecting on any mismatch meant an edit to an unrelated
|
|
451
|
+
// file superseded a run that had nothing to do with it, reintroducing the
|
|
452
|
+
// exact 38/40-neighbour loss #1443 was written to fix, one filter down.
|
|
453
|
+
// `getFilesChangedSince` (#451) is the honest per-file signal: a run is
|
|
454
|
+
// superseded only if its own primary file or one of its neighbours was
|
|
455
|
+
// actually rewritten since it launched. A late-but-not-superseded run is
|
|
456
|
+
// surfaced; a superseded one is dropped with a RECORD (never silently),
|
|
457
|
+
// so the loss stays countable.
|
|
458
|
+
if (originSeq !== undefined) {
|
|
459
|
+
const changedSince = runtime.getFilesChangedSince(originSeq);
|
|
460
|
+
if (changedSince.length > 0) {
|
|
461
|
+
const changedSet = new Set(changedSince);
|
|
462
|
+
const primaryKey = normalizeMapKey(path.resolve(run.filePath));
|
|
463
|
+
const neighborKeys = [
|
|
464
|
+
...(run.result?.neighbors ?? []).map((n) => n.filePath),
|
|
465
|
+
...(run.selectedNeighborPaths ?? []),
|
|
466
|
+
].map((filePath) => normalizeMapKey(path.resolve(filePath)));
|
|
467
|
+
const supersededByOwnFile = changedSet.has(primaryKey) ||
|
|
468
|
+
neighborKeys.some((k) => changedSet.has(k));
|
|
469
|
+
if (supersededByOwnFile) {
|
|
470
|
+
logCascade({
|
|
471
|
+
phase: "cascade_carry_over_drop",
|
|
472
|
+
filePath: run.filePath,
|
|
473
|
+
neighborCount: run.neighborCount,
|
|
474
|
+
diagnosticCount: run.diagnosticCount,
|
|
475
|
+
reason: "superseded_by_later_write",
|
|
476
|
+
metadata: {
|
|
477
|
+
originProjectSeq: originSeq,
|
|
478
|
+
projectSeq: runtime.projectSeq,
|
|
479
|
+
originTurnSeq: originTurn,
|
|
480
|
+
turnIndex: runtime.turnIndex,
|
|
481
|
+
carriedTurns: run.carriedTurns,
|
|
482
|
+
changedFiles: changedSince,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return true;
|
|
216
490
|
});
|
|
217
491
|
const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
|
|
492
|
+
// #1550 class sweep: every cascade record below summarises `cascadeResults`
|
|
493
|
+
// â runs, which carry their own paths and can be carried across turns
|
|
494
|
+
// (#1443) â so labelling them with the turn's first EDITED file is the same
|
|
495
|
+
// mis-attribution the `cascade_indeterminate` fix removes. On a read-only
|
|
496
|
+
// drain turn `files` is empty and the old `?? cwd` fallback stamped a bare
|
|
497
|
+
// DIRECTORY as the record's file. These three are turn-level AGGREGATES (no
|
|
498
|
+
// per-file cause is claimed), so one label suffices; the edited file and cwd
|
|
499
|
+
// stay as fallbacks.
|
|
500
|
+
const cascadeLogFilePath = cascadeResults[0]?.filePath ?? files[0] ?? cwd;
|
|
218
501
|
if (cascadeResults.length > 0) {
|
|
219
502
|
const seen = new Map();
|
|
220
503
|
for (const result of cascadeResults) {
|
|
@@ -231,11 +514,25 @@ export async function handleTurnEnd(deps) {
|
|
|
231
514
|
}
|
|
232
515
|
}
|
|
233
516
|
const parts = [];
|
|
517
|
+
// #1446 item 1: track what actually gets injected â a suppressed result
|
|
518
|
+
// (real formatted cascade text, but every one of its neighbors was claimed
|
|
519
|
+
// by a LATER result â see the reverse-iteration ownership pass above) was
|
|
520
|
+
// previously indistinguishable from "no output"; this counts it explicitly
|
|
521
|
+
// instead of letting it vanish.
|
|
522
|
+
let injectedNeighborCount = 0;
|
|
523
|
+
let injectedDiagnosticCount = 0;
|
|
524
|
+
let suppressedByOwnership = 0;
|
|
234
525
|
for (const result of seen.values()) {
|
|
235
526
|
const pk = normalizeMapKey(result.filePath);
|
|
236
527
|
const ownsAny = result.neighbors.some((n) => neighborOwner.get(normalizeMapKey(n.filePath)) === pk);
|
|
237
|
-
if (ownsAny && result.formatted)
|
|
528
|
+
if (ownsAny && result.formatted) {
|
|
238
529
|
parts.push(result.formatted);
|
|
530
|
+
injectedNeighborCount += result.neighbors.length;
|
|
531
|
+
injectedDiagnosticCount += result.neighbors.reduce((s, n) => s + n.diagnostics.length, 0);
|
|
532
|
+
}
|
|
533
|
+
else if (!ownsAny && result.formatted) {
|
|
534
|
+
suppressedByOwnership++;
|
|
535
|
+
}
|
|
239
536
|
}
|
|
240
537
|
// Suggest tests for cascade neighbors (files with diagnostics)
|
|
241
538
|
const neighborFilesWithErrors = cascadeResults
|
|
@@ -243,9 +540,27 @@ export async function handleTurnEnd(deps) {
|
|
|
243
540
|
.filter((n) => n.diagnostics.length > 0)
|
|
244
541
|
.map((n) => n.filePath);
|
|
245
542
|
const uniqueNeighborFiles = [...new Set(neighborFilesWithErrors)];
|
|
543
|
+
let testSuggestionCount = 0;
|
|
246
544
|
if (uniqueNeighborFiles.length > 0 &&
|
|
247
545
|
typeof testRunnerClient.suggestTestFiles === "function") {
|
|
248
546
|
const testSuggestions = testRunnerClient.suggestTestFiles(uniqueNeighborFiles, cwd);
|
|
547
|
+
testSuggestionCount = testSuggestions.length;
|
|
548
|
+
// #1446 item 2: this path previously emitted nothing to any log â a
|
|
549
|
+
// zero-suggestion outcome (neighbors had errors but no test file
|
|
550
|
+
// resolved for any of them) is the more interesting case, so it is
|
|
551
|
+
// recorded on the same phase rather than only logging on a hit.
|
|
552
|
+
logCascade({
|
|
553
|
+
phase: "cascade_test_targets",
|
|
554
|
+
filePath: cascadeLogFilePath,
|
|
555
|
+
neighborCount: uniqueNeighborFiles.length,
|
|
556
|
+
metadata: {
|
|
557
|
+
neighborFiles: uniqueNeighborFiles.slice(0, 10),
|
|
558
|
+
suggestedTestFiles: testSuggestions.slice(0, 10).map((s) => s.testFile),
|
|
559
|
+
runner: testSuggestions[0]?.runner,
|
|
560
|
+
truncated: testSuggestions.length > 10,
|
|
561
|
+
zeroSuggestions: testSuggestions.length === 0,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
249
564
|
if (testSuggestions.length > 0) {
|
|
250
565
|
const testLines = testSuggestions
|
|
251
566
|
.slice(0, 5)
|
|
@@ -257,11 +572,34 @@ export async function handleTurnEnd(deps) {
|
|
|
257
572
|
parts.push(testSection);
|
|
258
573
|
}
|
|
259
574
|
}
|
|
260
|
-
if (parts.length > 0)
|
|
261
|
-
|
|
575
|
+
if (parts.length > 0) {
|
|
576
|
+
const section = parts.join("\n\n");
|
|
577
|
+
// @delivery-surface: runtime-turn:cascade-blocker
|
|
578
|
+
blockerParts.push(section);
|
|
579
|
+
// #1446 item 1: proves the cascade section reached `blockerParts` â
|
|
580
|
+
// i.e. it was QUEUED for persistence into the turn-end advisory â not
|
|
581
|
+
// that it reached the agent. The counters alone (cascade_result,
|
|
582
|
+
// cascade_turn_end) never confirmed even that much, only computation.
|
|
583
|
+
// Actual delivery happens later, via consumeTurnEndFindings/
|
|
584
|
+
// peekTurnEndFindings, and can still be suppressed after this point
|
|
585
|
+
// (e.g. allFilesDeleted, cross-turn dedup, or the session ending
|
|
586
|
+
// before the next turn_end drains it) â this record does not prove
|
|
587
|
+
// the agent ever saw the text.
|
|
588
|
+
logCascade({
|
|
589
|
+
phase: "cascade_injected",
|
|
590
|
+
filePath: cascadeLogFilePath,
|
|
591
|
+
neighborCount: injectedNeighborCount,
|
|
592
|
+
diagnosticCount: injectedDiagnosticCount,
|
|
593
|
+
metadata: {
|
|
594
|
+
sectionChars: section.length,
|
|
595
|
+
testSuggestionCount,
|
|
596
|
+
suppressedByOwnership,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
}
|
|
262
600
|
logCascade({
|
|
263
601
|
phase: "cascade_turn_end",
|
|
264
|
-
filePath:
|
|
602
|
+
filePath: cascadeLogFilePath,
|
|
265
603
|
neighborCount: cascadeResults.reduce((s, r) => s + r.neighbors.length, 0),
|
|
266
604
|
diagnosticCount: cascadeResults.reduce((s, r) => s + r.neighbors.reduce((ns, n) => ns + n.diagnostics.length, 0), 0),
|
|
267
605
|
metadata: {
|
|
@@ -271,8 +609,9 @@ export async function handleTurnEnd(deps) {
|
|
|
271
609
|
});
|
|
272
610
|
}
|
|
273
611
|
// #1023: surface an HONEST note whenever a cascade run could not compute
|
|
274
|
-
// downstream impact (degraded/over-cap graph, missing node,
|
|
275
|
-
//
|
|
612
|
+
// downstream impact (degraded/over-cap graph, missing node, a thrown compute,
|
|
613
|
+
// or a deliberately budget-truncated neighbor set) â never a silent all-clear
|
|
614
|
+
// (#533). This goes to the ADVISORY tier,
|
|
276
615
|
// NOT the blocker tier: in an over-cap monorepo the graph is `skipped` on
|
|
277
616
|
// every edit, so a blocker would fire hard and never clear turn state every
|
|
278
617
|
// turn (over-escalation â the mirror of the silent-all-clear bug). Advisory
|
|
@@ -309,12 +648,28 @@ export async function handleTurnEnd(deps) {
|
|
|
309
648
|
const more = files.length > 5 ? ` (+${files.length - 5} more)` : "";
|
|
310
649
|
lines.push(` âĸ ${detail}: ${shown}${more}`);
|
|
311
650
|
}
|
|
312
|
-
const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath)))
|
|
651
|
+
const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath)))
|
|
652
|
+
.size;
|
|
313
653
|
const reasons = [...byDetail.keys()].join("; ");
|
|
314
654
|
return `${frame.lead(fileCount, reasons)}\n${lines.join("\n")}`;
|
|
315
655
|
};
|
|
316
|
-
|
|
317
|
-
|
|
656
|
+
// #1445: `excluded_by_role` (test files excluded from the graph BY DESIGN,
|
|
657
|
+
// #260) is never agent-facing â it is not a graph failure, and #1080
|
|
658
|
+
// already excludes test-role files from every neighbor surface, so "a
|
|
659
|
+
// clean result does not cover them" would itself be a false claim. It
|
|
660
|
+
// stays visible in the `cascade_indeterminate` log below (metadata-only,
|
|
661
|
+
// info-level) so the log can tell an intentional exclusion from a real
|
|
662
|
+
// graph gap, but it never reaches `buildAdvisory`/the agent.
|
|
663
|
+
const graphRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason !== "lsp_binding_rejected" &&
|
|
664
|
+
r.indeterminate?.reason !== "excluded_by_role" &&
|
|
665
|
+
r.indeterminate?.reason !== "budget_truncated" &&
|
|
666
|
+
r.indeterminate?.budget === undefined);
|
|
667
|
+
const bindingRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "lsp_binding_rejected" &&
|
|
668
|
+
r.indeterminate?.budget === undefined);
|
|
669
|
+
// Budget coverage can be merged into a graph or binding marker, so its
|
|
670
|
+
// advisory bucket follows the evidence rather than replacing that reason.
|
|
671
|
+
const budgetRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "budget_truncated" ||
|
|
672
|
+
r.indeterminate?.budget !== undefined);
|
|
318
673
|
// Factual/informational phrasing â the advisory tier wraps this with an
|
|
319
674
|
// "âšī¸ Advisory â no action required this turn:" label, so an imperative
|
|
320
675
|
// ("review dependents manually") would contradict it. The #533 substance
|
|
@@ -327,6 +682,7 @@ export async function handleTurnEnd(deps) {
|
|
|
327
682
|
? "changed file not in the review graph"
|
|
328
683
|
: "review graph unavailable",
|
|
329
684
|
});
|
|
685
|
+
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
330
686
|
if (graphAdvisory)
|
|
331
687
|
advisoryParts.push(graphAdvisory);
|
|
332
688
|
const bindingAdvisory = buildAdvisory(bindingRuns, {
|
|
@@ -335,15 +691,57 @@ export async function handleTurnEnd(deps) {
|
|
|
335
691
|
`cascade result does not cover them.`,
|
|
336
692
|
fallbackDetail: () => "cascade diagnostics withheld (binding rejected)",
|
|
337
693
|
});
|
|
694
|
+
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
338
695
|
if (bindingAdvisory)
|
|
339
696
|
advisoryParts.push(bindingAdvisory);
|
|
697
|
+
const budgetAdvisory = buildAdvisory(budgetRuns, {
|
|
698
|
+
lead: (fileCount, reasons) => `Cascade checked the selected neighbors for ${fileCount} edited file(s) this turn, ` +
|
|
699
|
+
`but some eligible dependents were not checked because the cascade budget ` +
|
|
700
|
+
`was exhausted (${reasons}); a clean cascade result does not cover them.`,
|
|
701
|
+
fallbackDetail: (r) => {
|
|
702
|
+
const budget = r.indeterminate?.budget;
|
|
703
|
+
if (!budget)
|
|
704
|
+
return "cascade budget omitted eligible dependents";
|
|
705
|
+
const detail = `cascade budget checked ${budget.selectedCount} of ${budget.eligibleCount} eligible dependents (${budget.truncatedCount} omitted)`;
|
|
706
|
+
return budget.transitiveTruncated
|
|
707
|
+
? `${detail}; transitive expansion was capped before all eligible dependents were enumerated`
|
|
708
|
+
: detail;
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
712
|
+
if (budgetAdvisory)
|
|
713
|
+
advisoryParts.push(budgetAdvisory);
|
|
340
714
|
const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
|
|
715
|
+
// #1550: attribute each reason to the file that PRODUCED it. This record
|
|
716
|
+
// used to stamp `filePath: files[0] ?? cwd` â the turn's first EDITED file
|
|
717
|
+
// â and a bare `reasons` array with no file association. The two sets are
|
|
718
|
+
// disjoint: a run can be carried across turns (#1443), and an edited file
|
|
719
|
+
// can skip the graph entirely (markdown/JSON return `non_code` before
|
|
720
|
+
// computeImpactCascade ever runs). So the log blamed a file that could not
|
|
721
|
+
// have produced the reason â a markdown file credited with `missing_node`,
|
|
722
|
+
// a non-test source file credited with `excluded_by_role` â and the defect
|
|
723
|
+
// read as "concentrated on test files" only because the first edited file
|
|
724
|
+
// of a turn usually is one. `fileCount` and the agent-facing advisory
|
|
725
|
+
// already keyed off `r.filePath`; only this record's labels did not.
|
|
726
|
+
const byFile = indeterminateRuns.map((r) => ({
|
|
727
|
+
file: toRunnerDisplayPath(cwd, r.filePath),
|
|
728
|
+
reason: r.indeterminate?.reason,
|
|
729
|
+
...(r.indeterminate?.detail && { detail: r.indeterminate.detail }),
|
|
730
|
+
...(r.indeterminate?.budget && { budget: r.indeterminate.budget }),
|
|
731
|
+
...(r.indeterminate?.diagnostic && {
|
|
732
|
+
diagnostic: r.indeterminate.diagnostic,
|
|
733
|
+
}),
|
|
734
|
+
}));
|
|
341
735
|
logCascade({
|
|
342
736
|
phase: "cascade_indeterminate",
|
|
343
|
-
|
|
737
|
+
// The first indeterminate run's own file. `files[0] ?? cwd` survives only
|
|
738
|
+
// as a last resort for a run with no path at all.
|
|
739
|
+
filePath: indeterminateRuns[0]?.filePath ?? files[0] ?? cwd,
|
|
344
740
|
metadata: {
|
|
345
741
|
fileCount,
|
|
346
742
|
reasons: indeterminateRuns.map((r) => r.indeterminate?.reason),
|
|
743
|
+
byFile: byFile.slice(0, 20),
|
|
744
|
+
...(byFile.length > 20 && { byFileTruncated: byFile.length - 20 }),
|
|
347
745
|
},
|
|
348
746
|
});
|
|
349
747
|
}
|
|
@@ -385,25 +783,44 @@ export async function handleTurnEnd(deps) {
|
|
|
385
783
|
// run tool checks every turn. Also back off after a timeout/kill so every
|
|
386
784
|
// agent turn does not spend 30s launching another heavyweight knip process.
|
|
387
785
|
const prevKnip = cacheManager.readCache("knip", cwd);
|
|
786
|
+
// An availability failure is NOT a hard knip failure: knip never ran, so
|
|
787
|
+
// there is nothing to back off from, and backing off would make an
|
|
788
|
+
// expiring probe verdict permanent again (#1467).
|
|
388
789
|
const previousFailedHard = prevKnip &&
|
|
389
790
|
!prevKnip.data.success &&
|
|
791
|
+
!prevKnip.data.failureKind &&
|
|
390
792
|
/(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prevKnip.data.summary);
|
|
391
793
|
if (previousFailedHard) {
|
|
392
794
|
dbg(`turn_end: skipping knip after recent failure: ${prevKnip.data.summary}`);
|
|
393
795
|
knipMeta = { skipped: true, reason: prevKnip.data.summary };
|
|
394
796
|
}
|
|
395
797
|
else {
|
|
396
|
-
const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns()
|
|
397
|
-
|
|
798
|
+
const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns(), {
|
|
799
|
+
projectSeq: runtime.projectSeq,
|
|
800
|
+
});
|
|
801
|
+
// Never overwrite a good scan with a failure (#925, #1467): the last
|
|
802
|
+
// good result stays until a new successful scan replaces it.
|
|
803
|
+
const knipWouldPoison = wouldPoisonCache(prevKnip, knipResult);
|
|
804
|
+
if (knipWouldPoison) {
|
|
805
|
+
dbg(`turn_end: keeping last good knip cache; this run failed: ${knipResult.summary}`);
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
cacheManager.writeCache("knip", knipResult, cwd);
|
|
809
|
+
}
|
|
398
810
|
knipMeta = {
|
|
811
|
+
execution: knipResult.execution ?? "executed",
|
|
399
812
|
success: knipResult.success,
|
|
400
813
|
totalIssues: knipResult.issues.length,
|
|
401
814
|
newIssues: 0,
|
|
402
815
|
blockerIssues: 0,
|
|
403
816
|
...(!knipResult.success && { reason: knipResult.summary }),
|
|
817
|
+
...(knipResult.failureKind && { failureKind: knipResult.failureKind }),
|
|
818
|
+
...(knipWouldPoison && { cacheKept: true }),
|
|
404
819
|
};
|
|
405
820
|
if (knipResult.success && knipResult.issues.length > 0) {
|
|
406
|
-
|
|
821
|
+
// Deliberately excludes the line number â see stableFindingKey's
|
|
822
|
+
// doc comment (#1483: mirrors the dead-code fix in #1477).
|
|
823
|
+
const issueKey = (i) => stableFindingKey(i.type, i.file, i.name, i.package);
|
|
407
824
|
const prevKeys = new Set((prevKnip?.data?.issues ?? []).map(issueKey));
|
|
408
825
|
const modifiedSet = new Set(files.map((f) => resolveRunnerPath(cwd, f)));
|
|
409
826
|
const newIssues = knipResult.issues.filter((issue) => {
|
|
@@ -435,6 +852,7 @@ export async function handleTurnEnd(deps) {
|
|
|
435
852
|
if (firstPath) {
|
|
436
853
|
report += ` First location: ${firstPath}\n`;
|
|
437
854
|
}
|
|
855
|
+
// @delivery-surface: runtime-turn:knip-blocker
|
|
438
856
|
blockerParts.push(report);
|
|
439
857
|
}
|
|
440
858
|
// Turn-end injects only this turn's HIGH-CONFIDENCE, ATTRIBUTABLE
|
|
@@ -454,6 +872,7 @@ export async function handleTurnEnd(deps) {
|
|
|
454
872
|
: "(unknown)";
|
|
455
873
|
report += ` ${display}${issue.line ? `:${issue.line}` : ""} â ${issue.name}\n`;
|
|
456
874
|
}
|
|
875
|
+
// @delivery-surface: runtime-turn:knip-advisory
|
|
457
876
|
advisoryParts.push(report);
|
|
458
877
|
}
|
|
459
878
|
}
|
|
@@ -467,25 +886,206 @@ export async function handleTurnEnd(deps) {
|
|
|
467
886
|
durationMs: Date.now() - t2,
|
|
468
887
|
metadata: knipMeta,
|
|
469
888
|
});
|
|
889
|
+
// Cross-file dead-code (#127) for non-JS/TS languages, on knip's contract:
|
|
890
|
+
// re-scan only when this turn touched a file the client owns, then inject the
|
|
891
|
+
// ATTRIBUTABLE delta â symbols in those files that became unused because of
|
|
892
|
+
// the edit. The project-wide list is deliberately NOT injected per turn (the
|
|
893
|
+
// same reasoning as knip above) and stays available via lens_diagnostics.
|
|
894
|
+
// MUST run before the projectDiagnosticsDelta write below, or a dead-code-only
|
|
895
|
+
// turn would persist nothing.
|
|
896
|
+
const tDeadCode = Date.now();
|
|
897
|
+
const deadCodeMeta = {};
|
|
898
|
+
if (runtime.isStartupScanInFlight("dead-code")) {
|
|
899
|
+
dbg("turn_end: skipping dead-code (startup scan still in flight)");
|
|
900
|
+
deadCodeMeta.skipped = true;
|
|
901
|
+
deadCodeMeta.reason = "startup_scan_in_flight";
|
|
902
|
+
}
|
|
903
|
+
else if (deadCodeClients.length === 0) {
|
|
904
|
+
deadCodeMeta.reason = "no_clients";
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
// The modified-file set costs a resolveRunnerPath per file, and that walks
|
|
908
|
+
// every ancestor to the filesystem root on a miss. Build it lazily, only
|
|
909
|
+
// once a client has actually claimed this project, so an all-JS repo with
|
|
910
|
+
// no dead-code client pays nothing per turn. Knip does the same.
|
|
911
|
+
let modifiedSet = null;
|
|
912
|
+
const modifiedFiles = () => (modifiedSet ??= new Set(files.map((f) => resolveRunnerPath(cwd, f))));
|
|
913
|
+
let newIssueTotal = 0;
|
|
914
|
+
const reasons = [];
|
|
915
|
+
// A malformed client or deps object must never abort turn_end. Before the
|
|
916
|
+
// per-turn delta this block only read a cache; now it iterates and awaits,
|
|
917
|
+
// so the whole thing needs the guard, not just `client.analyze`.
|
|
918
|
+
try {
|
|
919
|
+
for (const client of deadCodeClients) {
|
|
920
|
+
if (!client.detect(cwd)) {
|
|
921
|
+
reasons.push(`${client.id}:not_detected`);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (![...modifiedFiles()].some((f) => client.owns(f))) {
|
|
925
|
+
reasons.push(`${client.id}:no_owned_files`);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const cacheKey = `dead-code-${client.id}`;
|
|
929
|
+
const prev = cacheManager.readCache(cacheKey, cwd);
|
|
930
|
+
// Back off after a timeout/kill so an unresponsive scanner cannot cost
|
|
931
|
+
// every later turn its full analysis budget (mirrors knip).
|
|
932
|
+
if (prev &&
|
|
933
|
+
!prev.data.success &&
|
|
934
|
+
/(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prev.data.summary)) {
|
|
935
|
+
dbg(`turn_end: skipping dead-code after failure: ${prev.data.summary}`);
|
|
936
|
+
deadCodeMeta.skipped = true;
|
|
937
|
+
reasons.push(`${client.id}:backoff:${prev.data.summary}`);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const startMs = Date.now();
|
|
941
|
+
try {
|
|
942
|
+
const result = await client.analyze(cwd);
|
|
943
|
+
const durationMs = Date.now() - startMs;
|
|
944
|
+
// Never overwrite a good scan with a failure (#925, #1467): a
|
|
945
|
+
// vulture timeout on one .py turn would otherwise evict the
|
|
946
|
+
// session_start scan, and the backoff above would then latch
|
|
947
|
+
// off the poisoned record.
|
|
948
|
+
if (wouldPoisonCache(prev, result)) {
|
|
949
|
+
dbg(`turn_end: keeping last good dead-code(${client.id}) cache; this run failed: ${result.summary}`);
|
|
950
|
+
deadCodeMeta.cacheKept = true;
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
cacheManager.writeCache(cacheKey, result, cwd, {
|
|
954
|
+
scanDurationMs: durationMs,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
// One event per cross-file scan (AGENTS.md) â the per-turn scan is
|
|
958
|
+
// now the primary path, so dead-code.log must see it too.
|
|
959
|
+
logDeadCodeScan({
|
|
960
|
+
language: client.language,
|
|
961
|
+
success: result.success,
|
|
962
|
+
cached: false,
|
|
963
|
+
unusedExports: result.unusedExports.length,
|
|
964
|
+
unusedFiles: result.unusedFiles.length,
|
|
965
|
+
unusedDeps: result.unusedDeps.length,
|
|
966
|
+
unlistedDeps: result.unlistedDeps.length,
|
|
967
|
+
durationMs: result.durationMs ?? durationMs,
|
|
968
|
+
...(!result.success && { reason: result.summary }),
|
|
969
|
+
});
|
|
970
|
+
deadCodeMeta.success = result.success;
|
|
971
|
+
if (!result.success) {
|
|
972
|
+
reasons.push(`${client.id}:scan_failed:${result.summary}`);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
deadCodeMeta.totalIssues =
|
|
976
|
+
(deadCodeMeta.totalIssues ?? 0) + deadCodeIssues(result).length;
|
|
977
|
+
// No baseline means every finding looks new. Report nothing rather
|
|
978
|
+
// than blame the edit for the whole project's pre-existing debt.
|
|
979
|
+
if (!prev?.data.success) {
|
|
980
|
+
reasons.push(`${client.id}:no_previous_scan`);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
const prevKeys = new Set(deadCodeIssues(prev.data).map(deadCodeIssueKey));
|
|
984
|
+
const modified = modifiedFiles();
|
|
985
|
+
const newIssues = deadCodeIssues(result).filter((issue) => {
|
|
986
|
+
if (prevKeys.has(deadCodeIssueKey(issue)))
|
|
987
|
+
return false;
|
|
988
|
+
if (!issue.file)
|
|
989
|
+
return false;
|
|
990
|
+
return modified.has(resolveRunnerPath(cwd, issue.file));
|
|
991
|
+
});
|
|
992
|
+
if (newIssues.length === 0) {
|
|
993
|
+
reasons.push(`${client.id}:clean`);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
newIssueTotal += newIssues.length;
|
|
997
|
+
projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
|
|
998
|
+
projectDiagnosticsSources.add("dead-code");
|
|
999
|
+
// @delivery-surface: runtime-turn:dead-code-advisory
|
|
1000
|
+
advisoryParts.push(formatDeadCodeDelta(newIssues, result.language));
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
dbg(`turn_end: dead-code(${client.id}) failed: ${err}`);
|
|
1004
|
+
reasons.push(`${client.id}:threw`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
catch (err) {
|
|
1009
|
+
dbg(`turn_end: dead-code block failed: ${err}`);
|
|
1010
|
+
reasons.push("block_threw");
|
|
1011
|
+
}
|
|
1012
|
+
deadCodeMeta.newIssues = newIssueTotal;
|
|
1013
|
+
if (reasons.length > 0)
|
|
1014
|
+
deadCodeMeta.reason = reasons.join(",");
|
|
1015
|
+
}
|
|
1016
|
+
logLatency({
|
|
1017
|
+
type: "phase",
|
|
1018
|
+
toolName: "turn_end",
|
|
1019
|
+
filePath: cwd,
|
|
1020
|
+
phase: "dead-code",
|
|
1021
|
+
durationMs: Date.now() - tDeadCode,
|
|
1022
|
+
metadata: deadCodeMeta,
|
|
1023
|
+
});
|
|
1024
|
+
// #1617: running total of findings this turn's advisory/blocker sections
|
|
1025
|
+
// dropped because an agent/user marked them false-positive/won't-fix â
|
|
1026
|
+
// the #1616 suppressed-bucket rule applied to turn_end's own reporting
|
|
1027
|
+
// lanes, so a mark's effect stays visible even though the finding itself
|
|
1028
|
+
// no longer appears above. Review-round F4 (#1625): kept per-lane, not
|
|
1029
|
+
// just a bare total, so the eventual trace says WHICH lane's marks did
|
|
1030
|
+
// the suppressing.
|
|
1031
|
+
let dispositionSuppressedTotal = 0;
|
|
1032
|
+
const dispositionSuppressedByLane = {};
|
|
1033
|
+
function recordDispositionSuppressed(lane, count) {
|
|
1034
|
+
if (count <= 0)
|
|
1035
|
+
return;
|
|
1036
|
+
dispositionSuppressedTotal += count;
|
|
1037
|
+
dispositionSuppressedByLane[lane] =
|
|
1038
|
+
(dispositionSuppressedByLane[lane] ?? 0) + count;
|
|
1039
|
+
}
|
|
470
1040
|
// govulncheck â surface session_start-cached Go CVE findings as advisory.
|
|
471
1041
|
// No per-turn re-run in this slice; the cache refreshes at next session_start.
|
|
472
1042
|
const govCacheEntry = cacheManager.readCache("govulncheck", cwd);
|
|
473
|
-
|
|
474
|
-
|
|
1043
|
+
// #1622: govulncheck renders a call site as `file:line`, and the cache is a
|
|
1044
|
+
// session_start snapshot â the same stale-line shape as gitleaks, one tier
|
|
1045
|
+
// lower. A CVE is pinned by go.mod, NOT by the call site, so neither an edit
|
|
1046
|
+
// nor a deletion may drop it: `onMissing: "demote"` routes a vanished traced
|
|
1047
|
+
// file into the same arm as an edited one. This gate only ever decides
|
|
1048
|
+
// whether the cited LINE is still worth printing. (Review round H1: the first
|
|
1049
|
+
// cut let a deleted trace file drop the CVE, contradicting this comment, and
|
|
1050
|
+
// `citedPath` reads only the FIRST filename frame â so one deleted file in a
|
|
1051
|
+
// long trace silently killed a CVE that go.mod still pins.)
|
|
1052
|
+
const govGate = gateFindingsByPathFreshness({
|
|
1053
|
+
store: "govulncheck",
|
|
1054
|
+
findings: govCacheEntry?.data?.findings ?? [],
|
|
1055
|
+
cwd,
|
|
1056
|
+
scannedAt: govCacheEntry?.data?.scannedAt,
|
|
1057
|
+
citedPath: (finding) => finding.trace.find((t) => t.filename)?.filename,
|
|
1058
|
+
onMissing: "demote",
|
|
1059
|
+
});
|
|
1060
|
+
const govStale = new Set(govGate.stale);
|
|
1061
|
+
// #1625 review round: the #1622 freshness gate runs FIRST â the disposition
|
|
1062
|
+
// filter's anchor is derived from each finding's post-demotion identity
|
|
1063
|
+
// (this array is already the gate's live+stale partition, never the raw
|
|
1064
|
+
// pre-gate cache). #1627's own post-gate guard (`if (govFindings.length)`)
|
|
1065
|
+
// and this round's disposition guard are the SAME guard â compose them,
|
|
1066
|
+
// never fall back to a raw-cache-length check (that would print the header
|
|
1067
|
+
// with zero rows beneath it whenever either filter empties the list).
|
|
1068
|
+
const govFiltered = filterFindingsByDisposition([...govGate.live, ...govGate.stale], cwd, (f) => govulncheckFindingToProjectDiagnostic(cwd, f));
|
|
1069
|
+
recordDispositionSuppressed("govulncheck", govFiltered.suppressed);
|
|
1070
|
+
const govFindings = govFiltered.kept;
|
|
1071
|
+
if (govFindings.length) {
|
|
1072
|
+
const findings = govFindings.slice(0, 5);
|
|
475
1073
|
let report = "đĄī¸ Go CVEs reachable from this code (govulncheck) â upgrade where possible:\n";
|
|
476
1074
|
for (const f of findings) {
|
|
477
1075
|
const callSite = f.trace.find((t) => t.filename);
|
|
1076
|
+
const stale = govStale.has(f);
|
|
478
1077
|
const where = callSite?.filename
|
|
479
|
-
? `${toRunnerDisplayPath(cwd, callSite.filename)}${callSite.line ? `:${callSite.line}` : ""}`
|
|
1078
|
+
? `${toRunnerDisplayPath(cwd, callSite.filename)}${!stale && callSite.line ? `:${callSite.line}` : ""}${stale ? ` ${STALE_LINE_MARKER}` : ""}`
|
|
480
1079
|
: (f.module ?? f.packageName ?? "(module)");
|
|
481
1080
|
const fix = f.fixedVersion
|
|
482
1081
|
? ` â upgrade to ${f.fixedVersion} or later`
|
|
483
1082
|
: " â no fix yet, track upstream";
|
|
484
1083
|
report += ` ${f.osv} (${where})${fix}\n`;
|
|
485
1084
|
}
|
|
486
|
-
if (
|
|
487
|
-
report += ` âĻ and ${
|
|
1085
|
+
if (govFindings.length > findings.length) {
|
|
1086
|
+
report += ` âĻ and ${govFindings.length - findings.length} more\n`;
|
|
488
1087
|
}
|
|
1088
|
+
// @delivery-surface: runtime-turn:govulncheck-advisory
|
|
489
1089
|
advisoryParts.push(report);
|
|
490
1090
|
}
|
|
491
1091
|
const trivyCacheEntry = cacheManager.readCache("trivy", cwd);
|
|
@@ -495,13 +1095,93 @@ export async function handleTurnEnd(deps) {
|
|
|
495
1095
|
// location so a committed/hardcoded secret is reported ONCE (with combined
|
|
496
1096
|
// provenance) â a blocker, since credentials need rotation before merge.
|
|
497
1097
|
const gitleaksData = cacheManager.readCache("gitleaks", cwd)?.data;
|
|
1098
|
+
const trivySecretsData = trivyCacheEntry?.data;
|
|
1099
|
+
// #1461 slice 1 (#1460): the gitleaks cache is TTL-only, so a finding for a
|
|
1100
|
+
// file deleted after the scan is still served as a đ´ blocker for the rest
|
|
1101
|
+
// of the 30-minute window â the live case, and 119 of 126 findings in
|
|
1102
|
+
// pi-lens's own cache. This read is the single agent-facing consumer of that
|
|
1103
|
+
// store (session_start's read only decides whether to re-scan; the
|
|
1104
|
+
// project-diagnostics path re-scans fresh and reconciles at load), so the
|
|
1105
|
+
// drop belongs here, before the findings enter the shared secret pipeline.
|
|
1106
|
+
// #1622 extends that gate from existence to freshness, and adds trivy
|
|
1107
|
+
// secrets â the sibling store with the identical shape. A cited file edited
|
|
1108
|
+
// after the scan keeps its finding but loses its line number: the credential
|
|
1109
|
+
// may still be there, just not where the snapshot says. Dropping instead
|
|
1110
|
+
// would let any edit â malicious or accidental â mute a real secret.
|
|
1111
|
+
const gitleaksGate = gateFindingsByPathFreshness({
|
|
1112
|
+
store: "gitleaks",
|
|
1113
|
+
findings: gitleaksData?.findings ?? [],
|
|
1114
|
+
cwd,
|
|
1115
|
+
scannedAt: gitleaksData?.scannedAt,
|
|
1116
|
+
citedPath: (finding) => finding.file,
|
|
1117
|
+
});
|
|
1118
|
+
const trivySecretsGate = gateFindingsByPathFreshness({
|
|
1119
|
+
store: "trivy-secrets",
|
|
1120
|
+
findings: trivySecretsData?.secrets ?? [],
|
|
1121
|
+
cwd,
|
|
1122
|
+
scannedAt: trivySecretsData?.scannedAt,
|
|
1123
|
+
citedPath: (finding) => finding.file,
|
|
1124
|
+
});
|
|
1125
|
+
// #1617: THE bug this issue exists for â gitleaks findings never passed
|
|
1126
|
+
// through `applyDispositions`, so an agent-marked false-positive/won't-fix
|
|
1127
|
+
// re-reported as a đ´ STOP blocker on every turn. Filter through the SAME
|
|
1128
|
+
// `gitleaksFindingToProjectDiagnostic` identity `lens_diagnostics
|
|
1129
|
+
// mode=full` surfaces (tool="gitleaks", rule="gitleaks:<ruleId>", the
|
|
1130
|
+
// exact "Potential secret: âĻ" message) so a mark made against what the
|
|
1131
|
+
// agent was shown is honored here too.
|
|
1132
|
+
//
|
|
1133
|
+
// #1625 review round: filtered AFTER the #1622 freshness gate above â the
|
|
1134
|
+
// anchor is derived from each finding's post-demotion identity, never the
|
|
1135
|
+
// raw pre-gate cache. Applied to BOTH `gitleaksGate.live` AND
|
|
1136
|
+
// `gitleaksGate.stale`: `staleSecretEntries` below derives from the stale
|
|
1137
|
+
// arm, and an fp-marked finding that later goes stale must not reappear
|
|
1138
|
+
// there â a suppression escape (and a double count against
|
|
1139
|
+
// `dispositionSuppressedTotal`) that a live-only filter would have missed.
|
|
1140
|
+
//
|
|
1141
|
+
// #1628: trivy-secret findings get the SAME treatment, now that
|
|
1142
|
+
// `trivySecretFindingToProjectDiagnostic` (project-diagnostics/runner-
|
|
1143
|
+
// adapters/trivy.ts) gives them a `lens_diagnostics`-surfaced identity
|
|
1144
|
+
// (tool="trivy", rule="trivy-secret:<ruleId>") to anchor a mark against â
|
|
1145
|
+
// same pattern as gitleaks above, applied to both the live and stale arms
|
|
1146
|
+
// for the same reason.
|
|
1147
|
+
//
|
|
1148
|
+
// ast-grep secret findings need no filtering here â they already went
|
|
1149
|
+
// through dispatch's applyDispositions before reaching
|
|
1150
|
+
// `peekActionableWarnings()`.
|
|
1151
|
+
const gitleaksLiveFiltered = filterFindingsByDisposition(gitleaksGate.live, cwd, (f) => gitleaksFindingToProjectDiagnostic(cwd, f));
|
|
1152
|
+
const gitleaksStaleFiltered = filterFindingsByDisposition(gitleaksGate.stale, cwd, (f) => gitleaksFindingToProjectDiagnostic(cwd, f));
|
|
1153
|
+
recordDispositionSuppressed("gitleaks", gitleaksLiveFiltered.suppressed + gitleaksStaleFiltered.suppressed);
|
|
1154
|
+
const trivySecretsLiveFiltered = filterFindingsByDisposition(trivySecretsGate.live, cwd, (f) => trivySecretFindingToProjectDiagnostic(cwd, f));
|
|
1155
|
+
const trivySecretsStaleFiltered = filterFindingsByDisposition(trivySecretsGate.stale, cwd, (f) => trivySecretFindingToProjectDiagnostic(cwd, f));
|
|
1156
|
+
recordDispositionSuppressed("trivy-secrets", trivySecretsLiveFiltered.suppressed + trivySecretsStaleFiltered.suppressed);
|
|
498
1157
|
const astSecretWarnings = runtime
|
|
499
1158
|
.peekActionableWarnings()
|
|
500
1159
|
.filter(isSecretWarning);
|
|
501
1160
|
const sessionSecrets = dedupeSecretFindings([
|
|
502
|
-
...fromGitleaks(
|
|
503
|
-
...fromTrivySecrets(
|
|
1161
|
+
...fromGitleaks(gitleaksLiveFiltered.kept),
|
|
1162
|
+
...fromTrivySecrets(trivySecretsLiveFiltered.kept),
|
|
504
1163
|
]);
|
|
1164
|
+
// Demoted secrets are addressed by FILE, never by line â the line is the one
|
|
1165
|
+
// field the edit invalidated. Rule id and source survive it and must be
|
|
1166
|
+
// carried through (review round M1): an agent triages an `aws-access-token`
|
|
1167
|
+
// differently from a low-confidence `generic-api-key`, and cannot do that
|
|
1168
|
+
// from a bare path. Deduped on file+rule+source so a file with twenty stale
|
|
1169
|
+
// hits of one rule is named once.
|
|
1170
|
+
const staleSecretEntries = [
|
|
1171
|
+
...gitleaksStaleFiltered.kept.map((f) => ({
|
|
1172
|
+
file: toRunnerDisplayPath(cwd, f.file),
|
|
1173
|
+
rule: f.ruleId,
|
|
1174
|
+
source: "gitleaks",
|
|
1175
|
+
})),
|
|
1176
|
+
...trivySecretsStaleFiltered.kept.map((f) => ({
|
|
1177
|
+
file: toRunnerDisplayPath(cwd, f.file),
|
|
1178
|
+
rule: f.ruleId,
|
|
1179
|
+
source: "trivy",
|
|
1180
|
+
})),
|
|
1181
|
+
];
|
|
1182
|
+
const staleSecrets = [
|
|
1183
|
+
...new Map(staleSecretEntries.map((e) => [`${e.file}|${e.rule}|${e.source}`, e])).values(),
|
|
1184
|
+
];
|
|
505
1185
|
// Locations already surfaced as session-scan secret blockers â used to enrich
|
|
506
1186
|
// provenance where ast-grep agrees and to suppress the duplicate ast-grep copy
|
|
507
1187
|
// from the actionable-warnings advisory below.
|
|
@@ -522,14 +1202,48 @@ export async function handleTurnEnd(deps) {
|
|
|
522
1202
|
if (enriched.length > shown.length) {
|
|
523
1203
|
report += ` âĻ and ${enriched.length - shown.length} more\n`;
|
|
524
1204
|
}
|
|
1205
|
+
// @delivery-surface: runtime-turn:secrets-gitleaks,runtime-turn:secrets-trivy
|
|
525
1206
|
blockerParts.push(report);
|
|
526
1207
|
}
|
|
1208
|
+
if (staleSecrets.length) {
|
|
1209
|
+
// Its OWN tier, never `advisoryParts` (review round M2). The advisory tier
|
|
1210
|
+
// is labelled "no action required this turn", which would sit directly
|
|
1211
|
+
// above copy telling the agent to re-scan â a section that contradicts its
|
|
1212
|
+
// own heading. This preamble is imperative because the action is real: the
|
|
1213
|
+
// finding is unverified, not dismissed.
|
|
1214
|
+
const shown = staleSecrets.slice(0, 5);
|
|
1215
|
+
let report = `đ ACTION NEEDED â secrets were flagged in files that changed after the scan. ${STALE_LINE_MARKER}\n` +
|
|
1216
|
+
"The cached line numbers are no longer trustworthy, so they are withheld. Re-run a secrets scan to confirm or clear these:\n";
|
|
1217
|
+
for (const entry of shown) {
|
|
1218
|
+
report += ` ${entry.file} â ${entry.rule} [${entry.source}]\n`;
|
|
1219
|
+
}
|
|
1220
|
+
if (staleSecrets.length > shown.length) {
|
|
1221
|
+
report += ` âĻ and ${staleSecrets.length - shown.length} more\n`;
|
|
1222
|
+
}
|
|
1223
|
+
// @delivery-surface: runtime-turn:stale-secrets-tier
|
|
1224
|
+
staleSecretParts.push(report);
|
|
1225
|
+
}
|
|
527
1226
|
// trivy â surface session_start-cached dependency CVEs (#131, Phase 1).
|
|
528
1227
|
// CRITICAL is a blocker (a known-exploitable CVE in a shipped dep is real
|
|
529
1228
|
// production risk); HIGH/MEDIUM/LOW are advisory. The agent gets the upgrade
|
|
530
1229
|
// target as a hint and decides â we never auto-edit lockfiles.
|
|
531
|
-
|
|
532
|
-
|
|
1230
|
+
//
|
|
1231
|
+
// #1634: these three trivy reports (critical blocker, non-critical
|
|
1232
|
+
// advisory, license advisory below) name a PACKAGE, not a file:line â there
|
|
1233
|
+
// is no cited path for `gateFindingsByPathFreshness` to stat, so unlike the
|
|
1234
|
+
// secrets/govulncheck stores above this store cannot be freshness-GATED.
|
|
1235
|
+
// It is the delivery gate's explicit-label escape hatch instead
|
|
1236
|
+
// (`clients/finding-delivery-gate.ts`, surfaces `runtime-turn:trivy-*`):
|
|
1237
|
+
// the session_start cache can be arbitrarily old, so its age is stated
|
|
1238
|
+
// plainly rather than presenting a CRITICAL blocker as if it were current.
|
|
1239
|
+
// This runs on top of (not instead of) #1625's disposition filter below â
|
|
1240
|
+
// a suppressed finding never reaches this render at all, so the two only
|
|
1241
|
+
// ever compose.
|
|
1242
|
+
const trivyAgeLabel = formatCacheAgeLabel(trivyCacheEntry?.data?.scannedAt);
|
|
1243
|
+
const trivyFindingsFiltered = filterFindingsByDisposition(trivyCacheEntry?.data?.findings ?? [], cwd, (f) => trivyFindingToProjectDiagnostic(cwd, f));
|
|
1244
|
+
recordDispositionSuppressed("trivy", trivyFindingsFiltered.suppressed);
|
|
1245
|
+
if (trivyFindingsFiltered.kept.length) {
|
|
1246
|
+
const all = trivyFindingsFiltered.kept;
|
|
533
1247
|
const critical = all.filter((f) => f.severity === "CRITICAL");
|
|
534
1248
|
const advisory = all.filter((f) => f.severity !== "CRITICAL");
|
|
535
1249
|
const fmt = (f) => {
|
|
@@ -543,32 +1257,35 @@ export async function handleTurnEnd(deps) {
|
|
|
543
1257
|
};
|
|
544
1258
|
if (critical.length) {
|
|
545
1259
|
const shown = critical.slice(0, 5);
|
|
546
|
-
let report =
|
|
1260
|
+
let report = `đ´ STOP â CRITICAL dependency CVEs (trivy, ${trivyAgeLabel}). Upgrade before shipping:\n`;
|
|
547
1261
|
for (const f of shown)
|
|
548
1262
|
report += fmt(f);
|
|
549
1263
|
if (critical.length > shown.length) {
|
|
550
1264
|
report += ` âĻ and ${critical.length - shown.length} more\n`;
|
|
551
1265
|
}
|
|
1266
|
+
// @delivery-surface: runtime-turn:trivy-critical-blocker
|
|
552
1267
|
blockerParts.push(report);
|
|
553
1268
|
}
|
|
554
1269
|
if (advisory.length) {
|
|
555
1270
|
const shown = advisory.slice(0, 5);
|
|
556
|
-
let report =
|
|
1271
|
+
let report = `đĄī¸ Dependency CVEs (trivy, ${trivyAgeLabel}) â upgrade where possible:\n`;
|
|
557
1272
|
for (const f of shown)
|
|
558
1273
|
report += fmt(f);
|
|
559
1274
|
if (advisory.length > shown.length) {
|
|
560
1275
|
report += ` âĻ and ${advisory.length - shown.length} more\n`;
|
|
561
1276
|
}
|
|
1277
|
+
// @delivery-surface: runtime-turn:trivy-cve-advisory
|
|
562
1278
|
advisoryParts.push(report);
|
|
563
1279
|
}
|
|
564
1280
|
}
|
|
565
1281
|
// trivy â dependency license risk (#131 Mode 4). Advisory only: a copyleft /
|
|
566
1282
|
// restricted license in a proprietary tree is a compliance signal, not a
|
|
567
|
-
// build break. Surfaced from the same cached `trivy fs` pass
|
|
1283
|
+
// build break. Surfaced from the same cached `trivy fs` pass â same #1634
|
|
1284
|
+
// explicit-label rationale as the CVE reports above (no cited path to gate).
|
|
568
1285
|
const licenses = trivyCacheEntry?.data?.licenses ?? [];
|
|
569
1286
|
if (licenses.length) {
|
|
570
1287
|
const shown = licenses.slice(0, 5);
|
|
571
|
-
let report =
|
|
1288
|
+
let report = `đ Dependency license risk (trivy, ${trivyAgeLabel}) â review for compliance:\n`;
|
|
572
1289
|
for (const l of shown) {
|
|
573
1290
|
const cat = l.category ? `, ${l.category}` : "";
|
|
574
1291
|
report += ` ${l.pkgName} â ${l.license} (${l.severity}${cat})\n`;
|
|
@@ -576,11 +1293,30 @@ export async function handleTurnEnd(deps) {
|
|
|
576
1293
|
if (licenses.length > shown.length) {
|
|
577
1294
|
report += ` âĻ and ${licenses.length - shown.length} more\n`;
|
|
578
1295
|
}
|
|
1296
|
+
// @delivery-surface: runtime-turn:trivy-license-advisory
|
|
579
1297
|
advisoryParts.push(report);
|
|
580
1298
|
}
|
|
1299
|
+
// #1616 suppressed-bucket rule: surface the running disposition-drop total
|
|
1300
|
+
// as its own advisory line so a mark's effect is visible, not a silent
|
|
1301
|
+
// absence â trace, not a vanish.
|
|
1302
|
+
if (dispositionSuppressedTotal > 0) {
|
|
1303
|
+
// Review-round F4 (#1625): per-lane attribution, e.g.
|
|
1304
|
+
// "gitleaks 2, govulncheck 1" â not just a bare total.
|
|
1305
|
+
const byLane = Object.entries(dispositionSuppressedByLane)
|
|
1306
|
+
.map(([lane, count]) => `${lane} ${count}`)
|
|
1307
|
+
.join(", ");
|
|
1308
|
+
// @delivery-surface: runtime-turn:disposition-suppressed-notice
|
|
1309
|
+
advisoryParts.push(`suppressed by disposition: ${dispositionSuppressedTotal} finding(s) ` +
|
|
1310
|
+
`dropped from this turn's gitleaks/govulncheck/trivy sections (${byLane}) ` +
|
|
1311
|
+
"(marked false-positive or won't-fix).");
|
|
1312
|
+
}
|
|
581
1313
|
const t3 = Date.now();
|
|
582
1314
|
let madgeStats;
|
|
583
|
-
|
|
1315
|
+
// Off by default (#766): this pass only writes debug output, and user-facing
|
|
1316
|
+
// madge diagnostics come from the session-start `madge` cache + the
|
|
1317
|
+
// `lens_diagnostics` extractor. Enabled with `--lens-turn-end-madge` /
|
|
1318
|
+
// `turnEnd.madge.enabled=true` for those who want the per-edit circular note.
|
|
1319
|
+
if (getFlag("lens-turn-end-madge") && (await depChecker.ensureAvailable())) {
|
|
584
1320
|
const madgeFiles = cacheManager.getFilesForMadge(cwd);
|
|
585
1321
|
if (madgeFiles.length > 0) {
|
|
586
1322
|
dbg(`turn_end: madge checking ${madgeFiles.length} file(s) for circular deps`);
|
|
@@ -616,7 +1352,9 @@ export async function handleTurnEnd(deps) {
|
|
|
616
1352
|
filePath: cwd,
|
|
617
1353
|
phase: "madge",
|
|
618
1354
|
durationMs: Date.now() - t3,
|
|
619
|
-
|
|
1355
|
+
// A ~0ms entry with no metadata is indistinguishable from "ran and was
|
|
1356
|
+
// fast" when re-analyzing the #766 tail â mark the skipped case.
|
|
1357
|
+
metadata: madgeStats ?? { skipped: true },
|
|
620
1358
|
});
|
|
621
1359
|
// --- Test runner: fire once per turn after all edits are done ---
|
|
622
1360
|
// Runs for each unique test target across modified files; results appear
|
|
@@ -668,8 +1406,38 @@ export async function handleTurnEnd(deps) {
|
|
|
668
1406
|
dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking)`);
|
|
669
1407
|
const firedAtTurn = runtime.turnIndex;
|
|
670
1408
|
const firedSessionId = runtime.telemetrySessionId;
|
|
1409
|
+
const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
1410
|
+
const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
|
|
1411
|
+
const provenanceFiles = [
|
|
1412
|
+
...candidates.map((candidate) => ({
|
|
1413
|
+
path: candidate.abs,
|
|
1414
|
+
role: "source",
|
|
1415
|
+
})),
|
|
1416
|
+
...targets.map((target) => ({
|
|
1417
|
+
path: target.testFile,
|
|
1418
|
+
role: "test",
|
|
1419
|
+
})),
|
|
1420
|
+
];
|
|
1421
|
+
const launchedFrom = snapshotAdvisoryProvenance({
|
|
1422
|
+
cwd,
|
|
1423
|
+
runtime,
|
|
1424
|
+
generation: testRunGeneration,
|
|
1425
|
+
files: provenanceFiles,
|
|
1426
|
+
});
|
|
1427
|
+
cacheManager.writeCache("test-runner-findings", { ...(priorTestCache ?? { content: "" }), testRunGeneration }, cwd);
|
|
671
1428
|
Promise.allSettled(targets.map((t) => testRunnerClient.runTestFileAsync(t.testFile, cwd, t.runner, t.config)))
|
|
672
1429
|
.then((results) => {
|
|
1430
|
+
const publishedAgainst = snapshotAdvisoryProvenance({
|
|
1431
|
+
cwd,
|
|
1432
|
+
runtime,
|
|
1433
|
+
generation: testRunGeneration,
|
|
1434
|
+
files: provenanceFiles,
|
|
1435
|
+
});
|
|
1436
|
+
const superseded = launchedFrom.revision.sessionId !== publishedAgainst.revision.sessionId ||
|
|
1437
|
+
launchedFrom.revision.projectSeq !== publishedAgainst.revision.projectSeq ||
|
|
1438
|
+
launchedFrom.revision.turnIndex !== publishedAgainst.revision.turnIndex ||
|
|
1439
|
+
launchedFrom.files.some((file, index) => publishedAgainst.files[index]?.sha256 !== file.sha256 ||
|
|
1440
|
+
publishedAgainst.files[index]?.path !== file.path);
|
|
673
1441
|
// #628: the turn advancing while tests ran no longer means the
|
|
674
1442
|
// results are thrown away â a late result is still real
|
|
675
1443
|
// information about what's currently broken. It's tagged `stale`
|
|
@@ -686,25 +1454,85 @@ export async function handleTurnEnd(deps) {
|
|
|
686
1454
|
resultValues.push(r.value);
|
|
687
1455
|
const { file, runner, passed, failed, duration, error } = r.value;
|
|
688
1456
|
const shortFile = path.basename(file);
|
|
1457
|
+
// #1479: `(0ms)` used to be printed for a run nobody
|
|
1458
|
+
// timed â a payload with no suite timestamps, an
|
|
1459
|
+
// unrecognised summary line, or an empty result â and
|
|
1460
|
+
// that is the same string a genuinely sub-millisecond
|
|
1461
|
+
// run produces. A reader could not tell "measured 0"
|
|
1462
|
+
// from "not measured", which is the confusion #1452 was
|
|
1463
|
+
// reported for. `duration` is now absent when it was
|
|
1464
|
+
// never measured, and this line says which one it has.
|
|
1465
|
+
//
|
|
1466
|
+
// #1480: the test is `formatRunDurationMs`, not an
|
|
1467
|
+
// inline comparison. The "absent = unmeasured" contract
|
|
1468
|
+
// was being re-derived at every site that read a
|
|
1469
|
+
// duration, and a site that gets it slightly wrong â
|
|
1470
|
+
// treating a measured `0` as absent â puts the bug back
|
|
1471
|
+
// without touching this comment.
|
|
1472
|
+
const elapsed = formatRunDurationMs(duration);
|
|
1473
|
+
// Lifted out of the template below for the same reason
|
|
1474
|
+
// `elapsed` is: the pair read as a nested ternary, which
|
|
1475
|
+
// this line only got flagged for because #1479 touched it.
|
|
1476
|
+
const verdict = failed > 0 ? "FAIL" : "PASS";
|
|
689
1477
|
const summary = error && passed === 0 && failed === 0
|
|
690
1478
|
? `error: ${error}`
|
|
691
|
-
: `${
|
|
1479
|
+
: `${verdict} ${passed}p/${failed}f (${elapsed})`;
|
|
692
1480
|
dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} â ${summary}`);
|
|
693
|
-
|
|
1481
|
+
// #1524: also fires on `error` alone, not just `failed > 0`.
|
|
1482
|
+
// A runner-error result (the suite never started â spawn/
|
|
1483
|
+
// config failure) has `failed === 0` by construction, so
|
|
1484
|
+
// gating on `failed > 0` alone dropped it silently: the
|
|
1485
|
+
// agent got no context at all, and the empty `failures`
|
|
1486
|
+
// array below sent this result down the "all tests
|
|
1487
|
+
// passed" branch, clearing any prior real test-failure
|
|
1488
|
+
// git-guard blocker. `formatResult` already renders the
|
|
1489
|
+
// error-only case as "Could not run tests: ...".
|
|
1490
|
+
if (failed > 0 || error) {
|
|
694
1491
|
const formatted = testRunnerClient.formatResult(r.value);
|
|
695
1492
|
if (formatted)
|
|
696
1493
|
failures.push(formatted);
|
|
697
1494
|
}
|
|
698
1495
|
}
|
|
699
1496
|
if (failures.length > 0) {
|
|
1497
|
+
const currentGeneration = cacheManager.readCache("test-runner-findings", cwd)?.data?.testRunGeneration;
|
|
1498
|
+
if (currentGeneration !== undefined &&
|
|
1499
|
+
currentGeneration > testRunGeneration) {
|
|
1500
|
+
dbg(`turn_end: test generation ${testRunGeneration} superseded by ${currentGeneration}`);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
700
1503
|
const content = stale
|
|
701
1504
|
? `[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")}`
|
|
702
1505
|
: failures.join("\n\n");
|
|
703
|
-
cacheManager.writeCache("test-runner-findings", {
|
|
1506
|
+
cacheManager.writeCache("test-runner-findings", {
|
|
1507
|
+
content,
|
|
1508
|
+
stale,
|
|
1509
|
+
results: resultValues,
|
|
1510
|
+
testRunGeneration,
|
|
1511
|
+
launchedFrom,
|
|
1512
|
+
publishedAgainst,
|
|
1513
|
+
provenance: publishedAgainst,
|
|
1514
|
+
superseded,
|
|
1515
|
+
}, cwd);
|
|
704
1516
|
if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1517
|
+
// #1524: `&& !value.error` â a runner-error result has
|
|
1518
|
+
// `failed === 0` (the suite never ran, so nothing could
|
|
1519
|
+
// fail), but it is not a pass. Without the filter it
|
|
1520
|
+
// would clear a prior real test-failure git-guard
|
|
1521
|
+
// blocker on the strength of a suite that never
|
|
1522
|
+
// started. And the call itself is skipped when this
|
|
1523
|
+
// list is empty rather than passed as `[]`:
|
|
1524
|
+
// `clearGitGuardTestFailure`'s own empty-array
|
|
1525
|
+
// fallback treats "no files named" as "clear every
|
|
1526
|
+
// blocked file", so an all-error batch (one go file,
|
|
1527
|
+
// runner-error, zero clean files) would otherwise
|
|
1528
|
+
// clear every blocker through that fallback instead
|
|
1529
|
+
// of clearing none.
|
|
1530
|
+
const cleanFiles = resultValues
|
|
1531
|
+
.filter((value) => value.failed === 0 && !value.error)
|
|
1532
|
+
.map((value) => value.file);
|
|
1533
|
+
if (cleanFiles.length > 0) {
|
|
1534
|
+
clearGitGuardTestFailure(cacheManager, cwd, runtime, cleanFiles);
|
|
1535
|
+
}
|
|
708
1536
|
mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, resultValues
|
|
709
1537
|
.filter((value) => value.failed > 0)
|
|
710
1538
|
.map((value) => value.file));
|
|
@@ -712,7 +1540,8 @@ export async function handleTurnEnd(deps) {
|
|
|
712
1540
|
dbg(`turn_end: ${failures.length} test failure(s) cached for next context injection${stale ? " (stale â turn advanced while tests ran)" : ""}`);
|
|
713
1541
|
}
|
|
714
1542
|
else if (results.length > 0) {
|
|
715
|
-
if (getFlag("lens-guard") &&
|
|
1543
|
+
if (getFlag("lens-guard") &&
|
|
1544
|
+
firedSessionId === runtime.telemetrySessionId) {
|
|
716
1545
|
clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues.map((value) => value.file));
|
|
717
1546
|
}
|
|
718
1547
|
dbg(`turn_end: all tests passed${stale ? " (stale â turn advanced while tests ran)" : ""}`);
|
|
@@ -745,6 +1574,7 @@ export async function handleTurnEnd(deps) {
|
|
|
745
1574
|
// into an authoritative-looking clean result for the rest of the file.
|
|
746
1575
|
// Keep the limitation visible and require a complete graph for this
|
|
747
1576
|
// user-facing impact surface (#1070).
|
|
1577
|
+
// @delivery-surface: runtime-turn:call-graph-advisory
|
|
748
1578
|
advisoryParts.push("Call-graph impact was not emitted because call-graph extraction coverage is incomplete; " +
|
|
749
1579
|
"the affected files may have unreported callers.");
|
|
750
1580
|
}
|
|
@@ -762,7 +1592,8 @@ export async function handleTurnEnd(deps) {
|
|
|
762
1592
|
const changedFileKey = normalizeMapKey(resolveRunnerPath(cwd, filePath));
|
|
763
1593
|
const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => {
|
|
764
1594
|
const graphFilePath = parseSymbolKey(k).filePath;
|
|
765
|
-
return normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) ===
|
|
1595
|
+
return (normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) ===
|
|
1596
|
+
changedFileKey);
|
|
766
1597
|
});
|
|
767
1598
|
for (const calleeKey of fileCallerKeys.slice(0, 3)) {
|
|
768
1599
|
// #1080: drop KNOWN test-role callers BEFORE both the human advisory
|
|
@@ -786,6 +1617,7 @@ export async function handleTurnEnd(deps) {
|
|
|
786
1617
|
}
|
|
787
1618
|
}
|
|
788
1619
|
if (impactLines.length > 0) {
|
|
1620
|
+
// @delivery-surface: runtime-turn:call-graph-advisory
|
|
789
1621
|
advisoryParts.push(`đ Call-graph impact (changed symbols have callers):\n${impactLines.join("\n")}`);
|
|
790
1622
|
}
|
|
791
1623
|
if (impactFindings.length > 0) {
|
|
@@ -854,6 +1686,7 @@ export async function handleTurnEnd(deps) {
|
|
|
854
1686
|
writeActionableWarningsReport(cacheManager, cwd, report);
|
|
855
1687
|
appendActionableWarningsHistory(cwd, report);
|
|
856
1688
|
const advisory = formatActionableWarningsAdvisory(report);
|
|
1689
|
+
// @delivery-surface: runtime-turn:actionable-warnings-advisory
|
|
857
1690
|
if (advisory)
|
|
858
1691
|
advisoryParts.push(advisory);
|
|
859
1692
|
logActionableWarningsEvent({
|
|
@@ -903,6 +1736,7 @@ export async function handleTurnEnd(deps) {
|
|
|
903
1736
|
writeCodeQualityWarningsReport(cacheManager, cwd, qualityReport);
|
|
904
1737
|
appendCodeQualityWarningsHistory(cwd, qualityReport);
|
|
905
1738
|
const advisory = formatCodeQualityWarningsAdvisory(qualityReport);
|
|
1739
|
+
// @delivery-surface: runtime-turn:code-quality-warnings-advisory
|
|
906
1740
|
if (advisory)
|
|
907
1741
|
advisoryParts.push(advisory);
|
|
908
1742
|
logLatency({
|
|
@@ -928,29 +1762,15 @@ export async function handleTurnEnd(deps) {
|
|
|
928
1762
|
},
|
|
929
1763
|
});
|
|
930
1764
|
}
|
|
931
|
-
// Cross-file dead-code (#127): surface the cached session_start scan as an
|
|
932
|
-
// advisory (project-wide unused symbols are slow to compute, so we read the
|
|
933
|
-
// cache rather than re-scanning every turn â the analogue of knip's cache
|
|
934
|
-
// for non-JS/TS languages). Merged across languages for polyglot repos.
|
|
935
|
-
try {
|
|
936
|
-
const deadCodeResults = [];
|
|
937
|
-
for (const client of deadCodeClients) {
|
|
938
|
-
if (!client.detect(cwd))
|
|
939
|
-
continue;
|
|
940
|
-
const cached = cacheManager.readCache(`dead-code-${client.id}`, cwd);
|
|
941
|
-
if (cached?.data)
|
|
942
|
-
deadCodeResults.push(cached.data);
|
|
943
|
-
}
|
|
944
|
-
const advisory = formatDeadCodeAdvisory(deadCodeResults);
|
|
945
|
-
if (advisory)
|
|
946
|
-
advisoryParts.push(advisory);
|
|
947
|
-
}
|
|
948
|
-
catch (err) {
|
|
949
|
-
dbg(`turn_end: dead-code advisory failed: ${err}`);
|
|
950
|
-
}
|
|
951
1765
|
cacheManager.incrementTurnCycle(cwd, currentOwner);
|
|
952
1766
|
const labeledAdvisoryParts = advisoryParts.map((p) => `âšī¸ Advisory â no action required this turn:\n${p}`);
|
|
953
|
-
|
|
1767
|
+
// Stale-secret parts sit between the two tiers and are NOT relabelled â they
|
|
1768
|
+
// ship the imperative preamble they were built with (#1622 review M2).
|
|
1769
|
+
const findingParts = [
|
|
1770
|
+
...blockerParts,
|
|
1771
|
+
...staleSecretParts,
|
|
1772
|
+
...labeledAdvisoryParts,
|
|
1773
|
+
];
|
|
954
1774
|
if (findingParts.length > 0) {
|
|
955
1775
|
dbg(`turn_end: ${blockerParts.length} blocker section(s), ${advisoryParts.length} advisory section(s) found, persisting for next context`);
|
|
956
1776
|
const content = capTurnEndMessage(findingParts.join("\n\n"));
|
|
@@ -975,7 +1795,12 @@ export async function handleTurnEnd(deps) {
|
|
|
975
1795
|
blockingFiles: blockerParts.length > 0 ? existingGuard.affectedFiles : undefined,
|
|
976
1796
|
projectSeqStart: runtime.turnStartProjectSeq,
|
|
977
1797
|
projectSeqEnd: runtime.projectSeq,
|
|
978
|
-
fileSeqByPath: Object.fromEntries(runtime
|
|
1798
|
+
fileSeqByPath: Object.fromEntries(runtime
|
|
1799
|
+
.getFileSeqEntries()
|
|
1800
|
+
.map(([filePath, seq]) => [
|
|
1801
|
+
normalizeMapKey(path.resolve(filePath)),
|
|
1802
|
+
seq,
|
|
1803
|
+
])),
|
|
979
1804
|
fileContentHashes: {},
|
|
980
1805
|
consumed: false,
|
|
981
1806
|
});
|
|
@@ -1022,7 +1847,29 @@ export async function handleTurnEnd(deps) {
|
|
|
1022
1847
|
});
|
|
1023
1848
|
}
|
|
1024
1849
|
else {
|
|
1025
|
-
|
|
1850
|
+
const allAffectedFiles = [
|
|
1851
|
+
...files.map((file) => resolveRunnerPath(cwd, file)),
|
|
1852
|
+
...cascadeResults.flatMap((result) => result.neighbors
|
|
1853
|
+
.filter((neighbor) => neighbor.diagnostics.length > 0)
|
|
1854
|
+
.map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
|
|
1855
|
+
];
|
|
1856
|
+
const affectedFiles = [...new Set(allAffectedFiles)].slice(0, MAX_ADVISORY_AFFECTED_FILES);
|
|
1857
|
+
const affectedFilesTruncated = new Set(allAffectedFiles).size > affectedFiles.length;
|
|
1858
|
+
cacheManager.writeCache("turn-end-findings", {
|
|
1859
|
+
content,
|
|
1860
|
+
affectedFiles,
|
|
1861
|
+
affectedFilesTruncated,
|
|
1862
|
+
provenance: snapshotAdvisoryProvenance({
|
|
1863
|
+
cwd,
|
|
1864
|
+
runtime,
|
|
1865
|
+
generation: 0,
|
|
1866
|
+
files: affectedFiles.map((file) => ({
|
|
1867
|
+
path: file,
|
|
1868
|
+
role: "affected",
|
|
1869
|
+
})),
|
|
1870
|
+
truncated: affectedFilesTruncated,
|
|
1871
|
+
}),
|
|
1872
|
+
}, cwd);
|
|
1026
1873
|
}
|
|
1027
1874
|
cacheManager.writeCache("turn-end-findings-last", {
|
|
1028
1875
|
signature,
|
|
@@ -1042,7 +1889,13 @@ export async function handleTurnEnd(deps) {
|
|
|
1042
1889
|
}
|
|
1043
1890
|
if (blockerParts.length === 0) {
|
|
1044
1891
|
cacheManager.clearTurnState(cwd, currentOwner);
|
|
1045
|
-
|
|
1892
|
+
// `staleSecretParts` counts here too (#1622 review M2): clearing the
|
|
1893
|
+
// findings record while a stale secret is still unverified would drop the
|
|
1894
|
+
// only surviving trace of it.
|
|
1895
|
+
if (getFlag("lens-guard") &&
|
|
1896
|
+
advisoryParts.length === 0 &&
|
|
1897
|
+
staleSecretParts.length === 0 &&
|
|
1898
|
+
!runtime.gitGuardHasBlockers) {
|
|
1046
1899
|
const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
1047
1900
|
if (guardRecord?.sessionId === runtime.telemetrySessionId &&
|
|
1048
1901
|
guardRecord.testFailures !== true) {
|
|
@@ -1058,10 +1911,18 @@ export async function handleTurnEnd(deps) {
|
|
|
1058
1911
|
toolName: "turn_end",
|
|
1059
1912
|
filePath: cwd,
|
|
1060
1913
|
durationMs: Date.now() - turnEndStart,
|
|
1061
|
-
|
|
1914
|
+
// #1622 review M2: a pending stale secret is NOT a clean turn. It gets its
|
|
1915
|
+
// own result rather than being promoted to `blockers_found`, which would
|
|
1916
|
+
// undo the demotion the freshness gate just made.
|
|
1917
|
+
result: blockerParts.length > 0
|
|
1918
|
+
? "blockers_found"
|
|
1919
|
+
: staleSecretParts.length > 0
|
|
1920
|
+
? "stale_secrets_pending"
|
|
1921
|
+
: "clean",
|
|
1062
1922
|
metadata: {
|
|
1063
1923
|
fileCount: files.length,
|
|
1064
1924
|
blockerSections: blockerParts.length,
|
|
1925
|
+
staleSecretSections: staleSecretParts.length,
|
|
1065
1926
|
advisorySections: advisoryParts.length,
|
|
1066
1927
|
},
|
|
1067
1928
|
});
|