@herbertgao/pi-extensions 2026.8.6 â 2026.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +320 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +79 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +27 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +66 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +5 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +174 -100
- package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +10 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +5 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +140 -97
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +63 -28
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +86 -38
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +76 -9
- package/node_modules/@narumitw/pi-btw/README.md +120 -111
- package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-btw/package.json +10 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +999 -0
- package/node_modules/pi-lens/README.md +117 -3
- package/node_modules/pi-lens/config/ruff/core.toml +42 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
- package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +381 -13
- package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +129 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +206 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
- package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
- package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +84 -14
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +25 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +32 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +47 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +127 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +188 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +21 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +316 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
- package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
- package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +87 -8
- package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +9 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
- package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +128 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1660 -164
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2423 -151
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
- package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
- package/node_modules/pi-lens/dist/clients/module-report.js +22 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
- package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +23 -9
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +8 -3
- package/node_modules/pi-lens/dist/clients/read-guard.js +253 -10
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +352 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +373 -9
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +620 -67
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +61407 -48577
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +476 -57
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/docs/agent-guide.md +6 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +19 -5
- package/node_modules/pi-lens/docs/globalconfig.md +3 -0
- package/node_modules/pi-lens/docs/language-coverage.md +71 -5
- package/node_modules/pi-lens/docs/mcp.md +2 -2
- package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
- package/node_modules/pi-lens/docs/settings.md +19 -1
- package/node_modules/pi-lens/docs/usage.md +1 -0
- package/node_modules/pi-lens/package.json +27 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
- package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
- package/node_modules/pi-mcp-adapter/README.md +47 -5
- package/node_modules/pi-mcp-adapter/cli.js +163 -1
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +25 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
- package/node_modules/pi-mcp-adapter/index.ts +219 -32
- package/node_modules/pi-mcp-adapter/init.ts +36 -4
- package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
- package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +3 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
- package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
- package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +2 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
- package/node_modules/pi-mcp-adapter/utils.ts +70 -0
- package/node_modules/pi-web-access/CHANGELOG.md +37 -0
- package/node_modules/pi-web-access/README.md +32 -13
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +40 -5
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +60 -61
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +4 -2
- package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
- package/node_modules/pi-web-access/parallel.ts +2 -2
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +44 -22
- package/node_modules/pi-web-access/tavily.ts +16 -4
- package/node_modules/pi-web-access/utils.ts +87 -0
- package/node_modules/pi-web-access/valyu.ts +199 -0
- package/package.json +14 -14
|
@@ -3,17 +3,24 @@ 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 { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, } from "./dead-code-client.js";
|
|
15
|
+
import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, stableFindingKey, } from "./dead-code-client.js";
|
|
13
16
|
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
14
17
|
import { PROJECT_DIAGNOSTICS_CACHE_VERSION, writeProjectDiagnosticsDeltaReport, } from "./project-diagnostics/cache.js";
|
|
15
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";
|
|
16
22
|
import { knipIssuesToProjectDiagnostics } from "./project-diagnostics/runner-adapters/knip.js";
|
|
23
|
+
import { applyDispositionsMultiFile } from "./diagnostic-dispositions.js";
|
|
17
24
|
import { logLatency } from "./latency-logger.js";
|
|
18
25
|
import { getLspBudgetIdleTimeoutMs, shouldShortenLspIdleTimeout, } from "./lsp-budget.js";
|
|
19
26
|
import { updateHeartbeat } from "./instance-registry.js";
|
|
@@ -21,7 +28,50 @@ import { emitLensTurnFindings } from "./lens-events.js";
|
|
|
21
28
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
22
29
|
import { isSubagentSession } from "./subagent-mode.js";
|
|
23
30
|
import { formatRunDurationMs } from "./run-duration.js";
|
|
24
|
-
import { MAX_ADVISORY_AFFECTED_FILES,
|
|
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 { incrementDegradationCount } from "./degradation-ledger.js";
|
|
38
|
+
import { degradeDemotedFindingBody, formatRetirementNote, } from "./demoted-finding-render.js";
|
|
39
|
+
import { STALE_LINE_MARKER } from "./stale-marker.js";
|
|
40
|
+
import { getWidgetBlockingFilesForSweep, markWidgetFileBlockersStale, } from "./widget-state.js";
|
|
41
|
+
/**
|
|
42
|
+
* #1617: turn_end reads gitleaks/govulncheck/trivy straight from their
|
|
43
|
+
* session-scan caches and formats them into advisory/blocker text â a
|
|
44
|
+
* reporting lane parallel to (and, before this fix, entirely bypassing)
|
|
45
|
+
* `dispatcher.ts:924`'s `applyDispositions` filter. An agent-marked
|
|
46
|
+
* false-positive/won't-fix on one of these findings never suppressed it
|
|
47
|
+
* here, so it re-reported on every turn.
|
|
48
|
+
*
|
|
49
|
+
* Filters `findings` through the SAME anchor derivation the dispatch path
|
|
50
|
+
* and `lens_diagnostics mode=full` use (`applyDispositionsMultiFile` in
|
|
51
|
+
* `diagnostic-dispositions.ts`), keyed off each lane's own canonical
|
|
52
|
+
* `ProjectDiagnostic` adapter (`toDiagnostic`) â the exact tool/rule/message
|
|
53
|
+
* identity `lens_diagnostics` already surfaces and `lens_diagnostic_mark`
|
|
54
|
+
* already anchors a mark against, not a second, cloned identity that would
|
|
55
|
+
* silently diverge from what the agent actually marked.
|
|
56
|
+
*
|
|
57
|
+
* Returns the surviving findings plus how many were dropped, so a caller can
|
|
58
|
+
* still surface a "suppressed by disposition: N" trace (the #1616
|
|
59
|
+
* suppressed-bucket rule â a security finding must never vanish with no
|
|
60
|
+
* trace, even when the disposition that dropped it is working as intended).
|
|
61
|
+
*/
|
|
62
|
+
function filterFindingsByDisposition(findings, cwd, toDiagnostic) {
|
|
63
|
+
if (findings.length === 0)
|
|
64
|
+
return { kept: findings, suppressed: 0 };
|
|
65
|
+
const candidates = findings.map((finding) => ({
|
|
66
|
+
finding,
|
|
67
|
+
diagnostic: toDiagnostic(finding),
|
|
68
|
+
}));
|
|
69
|
+
const survivors = new Set(applyDispositionsMultiFile(candidates.map((c) => c.diagnostic), cwd, (d) => d.filePath));
|
|
70
|
+
const kept = candidates
|
|
71
|
+
.filter((c) => survivors.has(c.diagnostic))
|
|
72
|
+
.map((c) => c.finding);
|
|
73
|
+
return { kept, suppressed: findings.length - kept.length };
|
|
74
|
+
}
|
|
25
75
|
/**
|
|
26
76
|
* Would writing `next` over `prev` throw away a good scan for a failed one?
|
|
27
77
|
*
|
|
@@ -35,6 +85,13 @@ function wouldPoisonCache(prev, next) {
|
|
|
35
85
|
}
|
|
36
86
|
// LSP idle reset scheduling â prevents thrashing by delaying shutdown
|
|
37
87
|
let lspIdleResetTimeout = null;
|
|
88
|
+
// #1618: set while this timer's fire is deferred behind an in-flight
|
|
89
|
+
// workspace sweep (see `scheduleLSPIdleReset`'s `isWorkspaceSweepActive`
|
|
90
|
+
// branch). `cancelLSPIdleReset` must be able to cancel THIS too â otherwise
|
|
91
|
+
// an active-editing turn that cancels idle reset while a sweep is still
|
|
92
|
+
// running would have it silently resurrected once the sweep finishes, even
|
|
93
|
+
// though the session is no longer idle.
|
|
94
|
+
let pendingSweepRearm = null;
|
|
38
95
|
function emitIdleResetReporterWarning(reportErr) {
|
|
39
96
|
try {
|
|
40
97
|
process.emitWarning(`pi-lens LSP idle reset error reporter failed: ${reportErr}`, { code: "PI_LENS_LSP_IDLE_RESET_REPORTER_FAILED" });
|
|
@@ -53,12 +110,40 @@ function reportIdleResetError(onError, err) {
|
|
|
53
110
|
}
|
|
54
111
|
}
|
|
55
112
|
function scheduleLSPIdleReset(resetFn, delayMs, options = {}) {
|
|
56
|
-
// Clear any pending reset to avoid multiple timers
|
|
113
|
+
// Clear any pending reset to avoid multiple timers. #1618: also cancel a
|
|
114
|
+
// rearm still waiting on a prior sweep's hold â otherwise re-scheduling
|
|
115
|
+
// here (this call) leaves that OLD waiter armed too, and the sweep's
|
|
116
|
+
// eventual release would fire a SECOND, independent `scheduleLSPIdleReset`
|
|
117
|
+
// alongside this fresh one.
|
|
57
118
|
if (lspIdleResetTimeout) {
|
|
58
119
|
clearTimeout(lspIdleResetTimeout);
|
|
59
120
|
}
|
|
121
|
+
if (pendingSweepRearm) {
|
|
122
|
+
pendingSweepRearm.cancelled = true;
|
|
123
|
+
pendingSweepRearm = null;
|
|
124
|
+
}
|
|
60
125
|
lspIdleResetTimeout = setTimeout(() => {
|
|
61
126
|
lspIdleResetTimeout = null;
|
|
127
|
+
// #1618: a full workspace sweep (`lens_diagnostics mode=full`) grants
|
|
128
|
+
// itself a wall-clock ceiling that can outlive this timer's delay â this
|
|
129
|
+
// used to fire straight into an in-flight sweep and destroy the very
|
|
130
|
+
// service the sweep was actively touching, mislabeling every file the
|
|
131
|
+
// sweep had not yet reached as budget exhaustion. Defer instead of
|
|
132
|
+
// firing: re-arm a FRESH `delayMs` timer once the sweep releases its
|
|
133
|
+
// hold, rather than resuming a countdown that's already elapsed (which
|
|
134
|
+
// would fire the instant the hold releases) or destroying mid-sweep.
|
|
135
|
+
if (isWorkspaceSweepActive()) {
|
|
136
|
+
const rearmToken = { cancelled: false };
|
|
137
|
+
pendingSweepRearm = rearmToken;
|
|
138
|
+
runWhenWorkspaceSweepIdle(() => {
|
|
139
|
+
if (rearmToken.cancelled)
|
|
140
|
+
return;
|
|
141
|
+
if (pendingSweepRearm === rearmToken)
|
|
142
|
+
pendingSweepRearm = null;
|
|
143
|
+
scheduleLSPIdleReset(resetFn, delayMs, options);
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
62
147
|
try {
|
|
63
148
|
if (options.isCurrentSession && !options.isCurrentSession()) {
|
|
64
149
|
return;
|
|
@@ -77,17 +162,58 @@ function scheduleLSPIdleReset(resetFn, delayMs, options = {}) {
|
|
|
77
162
|
// exit after completing its work, not wait 240 seconds for this to fire)
|
|
78
163
|
lspIdleResetTimeout.unref();
|
|
79
164
|
}
|
|
165
|
+
// #1618 acceptance criterion 6: FULL_SCAN_WALL_CLOCK_MS (the full-sweep wall
|
|
166
|
+
// clock ceiling, `tools/lens-diagnostics.ts`) must stay under EVERY idle
|
|
167
|
+
// reset delay this module can arm â derived, not asserted, so the constants
|
|
168
|
+
// can't drift back into a relationship where a still-running sweep can
|
|
169
|
+
// outlive the timer. The AC1 hold above already makes a mid-sweep fire
|
|
170
|
+
// impossible regardless of this margin; this is defense in depth against a
|
|
171
|
+
// future caller that touches the LSP service outside
|
|
172
|
+
// `runWorkspaceDiagnostics`' hold. `SWEEP_IDLE_SAFETY_MARGIN_MS` is
|
|
173
|
+
// single-sourced from `workspace-sweep-hold.ts`, which also uses it for its
|
|
174
|
+
// own max-hold-age failsafe â one tunable, not two.
|
|
175
|
+
const DEFAULT_LSP_IDLE_RESET_MS = 240_000;
|
|
176
|
+
function sweepDerivedFloorMs() {
|
|
177
|
+
return getFullScanWallClockMs() + SWEEP_IDLE_SAFETY_MARGIN_MS;
|
|
178
|
+
}
|
|
179
|
+
/** The normal (non-subagent, non-budget-pressured) idle-reset delay. */
|
|
180
|
+
function getBaseLspIdleResetMs() {
|
|
181
|
+
return Math.max(DEFAULT_LSP_IDLE_RESET_MS, sweepDerivedFloorMs());
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* #1618 (R4): the subagent-light (#713) and cross-process-budget-pressured
|
|
185
|
+
* (#449) paths used to arm a flat, much SHORTER delay (60s default) than the
|
|
186
|
+
* sweep's own 300s ceiling â a 5:1 inversion covered only by the AC1 hold.
|
|
187
|
+
* Deriving this path too means AC6 ("the sweep's ceiling stays under every
|
|
188
|
+
* idle-reset delay") holds universally, not just for the common path, and an
|
|
189
|
+
* env override to either constant can never invert it (`Math.max` floors at
|
|
190
|
+
* the derived value no matter how small the override pushes the other side).
|
|
191
|
+
*
|
|
192
|
+
* Accepted cost (deliberate, not incidental â see R6 in the PR body): under
|
|
193
|
+
* default settings this now ALSO arms the ~360s derived floor rather than a
|
|
194
|
+
* true 60s teardown, trading some of #713's "release a short-lived
|
|
195
|
+
* subagent's fleet fast" benefit for AC6 holding without exceptions.
|
|
196
|
+
*/
|
|
197
|
+
function getShortenedLspIdleResetMs() {
|
|
198
|
+
return Math.max(getLspBudgetIdleTimeoutMs(), sweepDerivedFloorMs());
|
|
199
|
+
}
|
|
200
|
+
/** The idle-reset delay `handleTurnEnd` actually arms on a file-less turn â
|
|
201
|
+
* exported so tests assert against the REAL computed value instead of a
|
|
202
|
+
* hand-derived literal that can silently drift from this function. */
|
|
203
|
+
export function getEffectiveLspIdleResetMs() {
|
|
204
|
+
return isSubagentSession() || shouldShortenLspIdleTimeout()
|
|
205
|
+
? getShortenedLspIdleResetMs()
|
|
206
|
+
: getBaseLspIdleResetMs();
|
|
207
|
+
}
|
|
80
208
|
export function cancelLSPIdleReset() {
|
|
81
209
|
if (lspIdleResetTimeout) {
|
|
82
210
|
clearTimeout(lspIdleResetTimeout);
|
|
83
211
|
lspIdleResetTimeout = null;
|
|
84
212
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const raw = Number(process.env.PI_LENS_CASCADE_SETTLE_WAIT_MS);
|
|
90
|
-
return Number.isFinite(raw) && raw >= 0 ? raw : 5000;
|
|
213
|
+
if (pendingSweepRearm) {
|
|
214
|
+
pendingSweepRearm.cancelled = true;
|
|
215
|
+
pendingSweepRearm = null;
|
|
216
|
+
}
|
|
91
217
|
}
|
|
92
218
|
function capTurnEndMessage(content) {
|
|
93
219
|
const maxLines = RUNTIME_CONFIG.turnEnd.maxLines;
|
|
@@ -165,14 +291,15 @@ export async function handleTurnEnd(deps) {
|
|
|
165
291
|
cacheManager.clearCache("turn-end-findings", cwd);
|
|
166
292
|
}
|
|
167
293
|
}
|
|
168
|
-
// #713: subagent sessions use a shorter idle reset (60s) â a
|
|
169
|
-
// task agent holding a warm fleet for 4 minutes after its
|
|
170
|
-
// pure waste under fan-out. Classify ONCE here so every
|
|
171
|
-
// path shares the same answer. PI_LENS_SUBAGENT_FULL=1
|
|
172
|
-
// isSubagentSession() returning false.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
294
|
+
// #713: subagent sessions use a shorter idle reset (nominally 60s) â a
|
|
295
|
+
// short-lived task agent holding a warm fleet for 4 minutes after its
|
|
296
|
+
// last turn is pure waste under fan-out. Classify ONCE here so every
|
|
297
|
+
// tick in this call path shares the same answer. PI_LENS_SUBAGENT_FULL=1
|
|
298
|
+
// restores the base delay via isSubagentSession() returning false.
|
|
299
|
+
// #1618: both branches route through `getEffectiveLspIdleResetMs` so
|
|
300
|
+
// AC6's derivation applies universally â see that function's doc for
|
|
301
|
+
// why the "shorter" path is not always literally 60s anymore.
|
|
302
|
+
const idleResetMs = getEffectiveLspIdleResetMs();
|
|
176
303
|
dbg(`turn_end: no modified files, scheduling LSP idle reset (${idleResetMs / 1000}s)`);
|
|
177
304
|
if (!getFlag("no-lsp")) {
|
|
178
305
|
const sessionGeneration = runtime.sessionGeneration;
|
|
@@ -184,8 +311,14 @@ export async function handleTurnEnd(deps) {
|
|
|
184
311
|
resetFormatService();
|
|
185
312
|
return;
|
|
186
313
|
}
|
|
187
|
-
// Cancel any pending idle reset since we're actively working
|
|
188
|
-
|
|
314
|
+
// Cancel any pending idle reset since we're actively working. #1618: also
|
|
315
|
+
// checks `pendingSweepRearm` â a timer deferred behind an in-flight
|
|
316
|
+
// workspace sweep already nulled `lspIdleResetTimeout` (the setTimeout
|
|
317
|
+
// callback clears it before checking the hold), so this guard used to
|
|
318
|
+
// read "nothing pending" and skip the cancel while a rearm was still
|
|
319
|
+
// queued to fire the instant the sweep released its hold â resurrecting
|
|
320
|
+
// idle reset on a session that had since gone back to active editing.
|
|
321
|
+
if (lspIdleResetTimeout || pendingSweepRearm) {
|
|
189
322
|
cancelLSPIdleReset();
|
|
190
323
|
dbg("turn_end: cancelled pending LSP idle reset (active editing)");
|
|
191
324
|
}
|
|
@@ -199,22 +332,117 @@ export async function handleTurnEnd(deps) {
|
|
|
199
332
|
}
|
|
200
333
|
const turnEndStart = Date.now();
|
|
201
334
|
const blockerParts = [];
|
|
335
|
+
/**
|
|
336
|
+
* #1622 review M2: findings the freshness gate demoted. A third tier between
|
|
337
|
+
* blockers and advisories â not a blocker, because the cached coordinate is
|
|
338
|
+
* untrustworthy; not an advisory, because the advisory label reads "no action
|
|
339
|
+
* required this turn" and these DO require a re-scan. Each part carries its
|
|
340
|
+
* own imperative preamble rather than inheriting that label.
|
|
341
|
+
*/
|
|
342
|
+
const staleSecretParts = [];
|
|
202
343
|
const advisoryParts = [];
|
|
203
344
|
const projectDiagnosticsDelta = [];
|
|
204
345
|
const projectDiagnosticsSources = new Set();
|
|
346
|
+
// #1641: past-EOF gate. Runs BEFORE the dependency-drift sweep below â a
|
|
347
|
+
// cheap statSync per cited file is worth paying first so the pricier
|
|
348
|
+
// import-parsing sweep can skip anything already taken out of the
|
|
349
|
+
// authoritative channel this turn (see blocker-past-eof.ts's module doc
|
|
350
|
+
// for the full composition rule with #1631's gate).
|
|
351
|
+
const blockerPastEofStart = Date.now();
|
|
352
|
+
const blockerPastEof = sweepInlineBlockerPastEof(runtime, cwd);
|
|
353
|
+
logLatency({
|
|
354
|
+
type: "phase",
|
|
355
|
+
toolName: "turn_end",
|
|
356
|
+
filePath: cwd,
|
|
357
|
+
phase: "blocker_past_eof_sweep",
|
|
358
|
+
durationMs: Date.now() - blockerPastEofStart,
|
|
359
|
+
metadata: {
|
|
360
|
+
total: blockerPastEof.total,
|
|
361
|
+
checked: blockerPastEof.checked,
|
|
362
|
+
demoted: blockerPastEof.demoted,
|
|
363
|
+
// #1944 review F3: `healed` is gone. Retirement makes the falling edge
|
|
364
|
+
// unreachable on this store, so the field could only ever log zero â
|
|
365
|
+
// see `BlockerPastEofCounts`.
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
// #1631: freshness gate. A cached blocker is a verdict about the file AND
|
|
369
|
+
// everything it imports; before re-serving it, sweep for out-of-band drift of
|
|
370
|
+
// the file or its forward imports and demote drifted entries to a
|
|
371
|
+
// `[stale â re-run to confirm]` advisory instead of re-asserting them at full
|
|
372
|
+
// authority (#1419 demote-not-drop).
|
|
373
|
+
const blockerFreshnessStart = Date.now();
|
|
374
|
+
// #1790: widen the sweep's population with widget-store rows a cache-served
|
|
375
|
+
// replay populated without ever touching RuntimeCoordinator's inline-blocker
|
|
376
|
+
// map â see blocker-freshness.ts's `WidgetSweepBlockerEntry` doc for why this
|
|
377
|
+
// is injected here rather than imported by blocker-freshness.ts itself.
|
|
378
|
+
const blockerFreshness = await sweepInlineBlockerFreshness(runtime, cwd, {
|
|
379
|
+
additionalEntries: getWidgetBlockingFilesForSweep().map((row) => ({
|
|
380
|
+
filePath: row.filePath,
|
|
381
|
+
recordedAtMs: row.recordedAtMs,
|
|
382
|
+
demote: () => markWidgetFileBlockersStale(row.filePath, "dependency-drift"),
|
|
383
|
+
})),
|
|
384
|
+
});
|
|
385
|
+
logLatency({
|
|
386
|
+
type: "phase",
|
|
387
|
+
toolName: "turn_end",
|
|
388
|
+
filePath: cwd,
|
|
389
|
+
phase: "blocker_freshness_sweep",
|
|
390
|
+
durationMs: Date.now() - blockerFreshnessStart,
|
|
391
|
+
metadata: {
|
|
392
|
+
total: blockerFreshness.total,
|
|
393
|
+
kept: blockerFreshness.kept,
|
|
394
|
+
revalidated: blockerFreshness.revalidated,
|
|
395
|
+
alreadyStale: blockerFreshness.alreadyStale,
|
|
396
|
+
truncatedImports: blockerFreshness.truncatedImports,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
205
399
|
// Re-surface inline blockers from this turn that the agent didn't fix.
|
|
206
400
|
// These were shown inline during write/edit but the agent moved on without resolving them.
|
|
207
401
|
const unresolvedBlockers = runtime.getInlineBlockersSnapshot();
|
|
208
|
-
|
|
402
|
+
/** #1944: past-EOF demotions retired after their single degraded delivery. */
|
|
403
|
+
let demotedFindingsRetired = 0;
|
|
404
|
+
for (const { filePath: bPath, summary, stale } of unresolvedBlockers) {
|
|
209
405
|
const displayPath = toRunnerDisplayPath(cwd, bPath);
|
|
210
|
-
|
|
406
|
+
if (stale) {
|
|
407
|
+
// #1631: demoted â out of the authoritative blocker channel and into the
|
|
408
|
+
// advisory channel with a stale marker, so the agent is told to re-run
|
|
409
|
+
// rather than pressured by a verdict that may already be resolved.
|
|
410
|
+
//
|
|
411
|
+
// #1944: the CHANNEL change is not enough. Until this call the advisory
|
|
412
|
+
// embedded the blocker body verbatim, so the agent read "đ´ STOP â 11
|
|
413
|
+
// issue(s) must be fixed" with dead line numbers under a hedge line it
|
|
414
|
+
// ignored. Degrade the body itself, and â when the file shrank past the
|
|
415
|
+
// cited lines, so no re-run can ever confirm it â retire the record
|
|
416
|
+
// after this ONE delivery instead of re-serving it for the rest of the
|
|
417
|
+
// session.
|
|
418
|
+
const deadLines = blockerPastEof.deadLinesByPath.get(bPath) ?? [];
|
|
419
|
+
const degraded = degradeDemotedFindingBody(summary, { deadLines });
|
|
420
|
+
const retired = runtime.retireDemotedPastEofBlocker(bPath, deadLines);
|
|
421
|
+
if (retired) {
|
|
422
|
+
demotedFindingsRetired += 1;
|
|
423
|
+
// Bounded by the ledger's own per-kind/subject tally, and the subject
|
|
424
|
+
// keeps the discriminating identity (which store, which file).
|
|
425
|
+
incrementDegradationCount({
|
|
426
|
+
kind: "demoted-finding-retired",
|
|
427
|
+
subject: `inline-blocker:${displayPath}`,
|
|
428
|
+
reason: `file shrank past cited line(s) ${deadLines.join(", ")}; retired after one degraded delivery`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
// @delivery-surface: runtime-turn:unresolved-inline-blocker
|
|
432
|
+
advisoryParts.push(`${STALE_LINE_MARKER} ${displayPath}:\n${degraded.body}` +
|
|
433
|
+
(retired ? `\n${formatRetirementNote(deadLines)}` : ""));
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// @delivery-surface: runtime-turn:unresolved-inline-blocker
|
|
437
|
+
blockerParts.push(`Unresolved from this turn â ${displayPath}:\n${summary}`);
|
|
438
|
+
}
|
|
211
439
|
}
|
|
212
440
|
// Drain the deferred cascade computes kicked off this turn (#450). They ran
|
|
213
441
|
// concurrently off the write hot path; wait a bounded time for them here so
|
|
214
442
|
// their runs are available to the merge below. A compute still in flight at
|
|
215
443
|
// the cap is carried over to the next turn_end (never dropped).
|
|
216
444
|
const cascadeSettleStart = Date.now();
|
|
217
|
-
const { settled, timedOut } = await runtime.settleCascadeRuns(cascadeSettleWaitMs());
|
|
445
|
+
const { settled, timedOut } = await runtime.settleCascadeRuns(cascadeSettleWaitMs(), { trackTurnEndClock: true });
|
|
218
446
|
logLatency({
|
|
219
447
|
type: "phase",
|
|
220
448
|
toolName: "turn_end",
|
|
@@ -260,7 +488,10 @@ export async function handleTurnEnd(deps) {
|
|
|
260
488
|
if (changedSince.length > 0) {
|
|
261
489
|
const changedSet = new Set(changedSince);
|
|
262
490
|
const primaryKey = normalizeMapKey(path.resolve(run.filePath));
|
|
263
|
-
const neighborKeys =
|
|
491
|
+
const neighborKeys = [
|
|
492
|
+
...(run.result?.neighbors ?? []).map((n) => n.filePath),
|
|
493
|
+
...(run.selectedNeighborPaths ?? []),
|
|
494
|
+
].map((filePath) => normalizeMapKey(path.resolve(filePath)));
|
|
264
495
|
const supersededByOwnFile = changedSet.has(primaryKey) ||
|
|
265
496
|
neighborKeys.some((k) => changedSet.has(k));
|
|
266
497
|
if (supersededByOwnFile) {
|
|
@@ -286,6 +517,15 @@ export async function handleTurnEnd(deps) {
|
|
|
286
517
|
return true;
|
|
287
518
|
});
|
|
288
519
|
const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
|
|
520
|
+
// #1550 class sweep: every cascade record below summarises `cascadeResults`
|
|
521
|
+
// â runs, which carry their own paths and can be carried across turns
|
|
522
|
+
// (#1443) â so labelling them with the turn's first EDITED file is the same
|
|
523
|
+
// mis-attribution the `cascade_indeterminate` fix removes. On a read-only
|
|
524
|
+
// drain turn `files` is empty and the old `?? cwd` fallback stamped a bare
|
|
525
|
+
// DIRECTORY as the record's file. These three are turn-level AGGREGATES (no
|
|
526
|
+
// per-file cause is claimed), so one label suffices; the edited file and cwd
|
|
527
|
+
// stay as fallbacks.
|
|
528
|
+
const cascadeLogFilePath = cascadeResults[0]?.filePath ?? files[0] ?? cwd;
|
|
289
529
|
if (cascadeResults.length > 0) {
|
|
290
530
|
const seen = new Map();
|
|
291
531
|
for (const result of cascadeResults) {
|
|
@@ -339,7 +579,7 @@ export async function handleTurnEnd(deps) {
|
|
|
339
579
|
// recorded on the same phase rather than only logging on a hit.
|
|
340
580
|
logCascade({
|
|
341
581
|
phase: "cascade_test_targets",
|
|
342
|
-
filePath:
|
|
582
|
+
filePath: cascadeLogFilePath,
|
|
343
583
|
neighborCount: uniqueNeighborFiles.length,
|
|
344
584
|
metadata: {
|
|
345
585
|
neighborFiles: uniqueNeighborFiles.slice(0, 10),
|
|
@@ -362,6 +602,7 @@ export async function handleTurnEnd(deps) {
|
|
|
362
602
|
}
|
|
363
603
|
if (parts.length > 0) {
|
|
364
604
|
const section = parts.join("\n\n");
|
|
605
|
+
// @delivery-surface: runtime-turn:cascade-blocker
|
|
365
606
|
blockerParts.push(section);
|
|
366
607
|
// #1446 item 1: proves the cascade section reached `blockerParts` â
|
|
367
608
|
// i.e. it was QUEUED for persistence into the turn-end advisory â not
|
|
@@ -374,7 +615,7 @@ export async function handleTurnEnd(deps) {
|
|
|
374
615
|
// the agent ever saw the text.
|
|
375
616
|
logCascade({
|
|
376
617
|
phase: "cascade_injected",
|
|
377
|
-
filePath:
|
|
618
|
+
filePath: cascadeLogFilePath,
|
|
378
619
|
neighborCount: injectedNeighborCount,
|
|
379
620
|
diagnosticCount: injectedDiagnosticCount,
|
|
380
621
|
metadata: {
|
|
@@ -386,7 +627,7 @@ export async function handleTurnEnd(deps) {
|
|
|
386
627
|
}
|
|
387
628
|
logCascade({
|
|
388
629
|
phase: "cascade_turn_end",
|
|
389
|
-
filePath:
|
|
630
|
+
filePath: cascadeLogFilePath,
|
|
390
631
|
neighborCount: cascadeResults.reduce((s, r) => s + r.neighbors.length, 0),
|
|
391
632
|
diagnosticCount: cascadeResults.reduce((s, r) => s + r.neighbors.reduce((ns, n) => ns + n.diagnostics.length, 0), 0),
|
|
392
633
|
metadata: {
|
|
@@ -396,8 +637,9 @@ export async function handleTurnEnd(deps) {
|
|
|
396
637
|
});
|
|
397
638
|
}
|
|
398
639
|
// #1023: surface an HONEST note whenever a cascade run could not compute
|
|
399
|
-
// downstream impact (degraded/over-cap graph, missing node,
|
|
400
|
-
//
|
|
640
|
+
// downstream impact (degraded/over-cap graph, missing node, a thrown compute,
|
|
641
|
+
// or a deliberately budget-truncated neighbor set) â never a silent all-clear
|
|
642
|
+
// (#533). This goes to the ADVISORY tier,
|
|
401
643
|
// NOT the blocker tier: in an over-cap monorepo the graph is `skipped` on
|
|
402
644
|
// every edit, so a blocker would fire hard and never clear turn state every
|
|
403
645
|
// turn (over-escalation â the mirror of the silent-all-clear bug). Advisory
|
|
@@ -434,7 +676,8 @@ export async function handleTurnEnd(deps) {
|
|
|
434
676
|
const more = files.length > 5 ? ` (+${files.length - 5} more)` : "";
|
|
435
677
|
lines.push(` âĸ ${detail}: ${shown}${more}`);
|
|
436
678
|
}
|
|
437
|
-
const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath)))
|
|
679
|
+
const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath)))
|
|
680
|
+
.size;
|
|
438
681
|
const reasons = [...byDetail.keys()].join("; ");
|
|
439
682
|
return `${frame.lead(fileCount, reasons)}\n${lines.join("\n")}`;
|
|
440
683
|
};
|
|
@@ -446,8 +689,15 @@ export async function handleTurnEnd(deps) {
|
|
|
446
689
|
// info-level) so the log can tell an intentional exclusion from a real
|
|
447
690
|
// graph gap, but it never reaches `buildAdvisory`/the agent.
|
|
448
691
|
const graphRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason !== "lsp_binding_rejected" &&
|
|
449
|
-
r.indeterminate?.reason !== "excluded_by_role"
|
|
450
|
-
|
|
692
|
+
r.indeterminate?.reason !== "excluded_by_role" &&
|
|
693
|
+
r.indeterminate?.reason !== "budget_truncated" &&
|
|
694
|
+
r.indeterminate?.budget === undefined);
|
|
695
|
+
const bindingRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "lsp_binding_rejected" &&
|
|
696
|
+
r.indeterminate?.budget === undefined);
|
|
697
|
+
// Budget coverage can be merged into a graph or binding marker, so its
|
|
698
|
+
// advisory bucket follows the evidence rather than replacing that reason.
|
|
699
|
+
const budgetRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "budget_truncated" ||
|
|
700
|
+
r.indeterminate?.budget !== undefined);
|
|
451
701
|
// Factual/informational phrasing â the advisory tier wraps this with an
|
|
452
702
|
// "âšī¸ Advisory â no action required this turn:" label, so an imperative
|
|
453
703
|
// ("review dependents manually") would contradict it. The #533 substance
|
|
@@ -460,6 +710,7 @@ export async function handleTurnEnd(deps) {
|
|
|
460
710
|
? "changed file not in the review graph"
|
|
461
711
|
: "review graph unavailable",
|
|
462
712
|
});
|
|
713
|
+
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
463
714
|
if (graphAdvisory)
|
|
464
715
|
advisoryParts.push(graphAdvisory);
|
|
465
716
|
const bindingAdvisory = buildAdvisory(bindingRuns, {
|
|
@@ -468,15 +719,57 @@ export async function handleTurnEnd(deps) {
|
|
|
468
719
|
`cascade result does not cover them.`,
|
|
469
720
|
fallbackDetail: () => "cascade diagnostics withheld (binding rejected)",
|
|
470
721
|
});
|
|
722
|
+
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
471
723
|
if (bindingAdvisory)
|
|
472
724
|
advisoryParts.push(bindingAdvisory);
|
|
725
|
+
const budgetAdvisory = buildAdvisory(budgetRuns, {
|
|
726
|
+
lead: (fileCount, reasons) => `Cascade checked the selected neighbors for ${fileCount} edited file(s) this turn, ` +
|
|
727
|
+
`but some eligible dependents were not checked because the cascade budget ` +
|
|
728
|
+
`was exhausted (${reasons}); a clean cascade result does not cover them.`,
|
|
729
|
+
fallbackDetail: (r) => {
|
|
730
|
+
const budget = r.indeterminate?.budget;
|
|
731
|
+
if (!budget)
|
|
732
|
+
return "cascade budget omitted eligible dependents";
|
|
733
|
+
const detail = `cascade budget checked ${budget.selectedCount} of ${budget.eligibleCount} eligible dependents (${budget.truncatedCount} omitted)`;
|
|
734
|
+
return budget.transitiveTruncated
|
|
735
|
+
? `${detail}; transitive expansion was capped before all eligible dependents were enumerated`
|
|
736
|
+
: detail;
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
740
|
+
if (budgetAdvisory)
|
|
741
|
+
advisoryParts.push(budgetAdvisory);
|
|
473
742
|
const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
|
|
743
|
+
// #1550: attribute each reason to the file that PRODUCED it. This record
|
|
744
|
+
// used to stamp `filePath: files[0] ?? cwd` â the turn's first EDITED file
|
|
745
|
+
// â and a bare `reasons` array with no file association. The two sets are
|
|
746
|
+
// disjoint: a run can be carried across turns (#1443), and an edited file
|
|
747
|
+
// can skip the graph entirely (markdown/JSON return `non_code` before
|
|
748
|
+
// computeImpactCascade ever runs). So the log blamed a file that could not
|
|
749
|
+
// have produced the reason â a markdown file credited with `missing_node`,
|
|
750
|
+
// a non-test source file credited with `excluded_by_role` â and the defect
|
|
751
|
+
// read as "concentrated on test files" only because the first edited file
|
|
752
|
+
// of a turn usually is one. `fileCount` and the agent-facing advisory
|
|
753
|
+
// already keyed off `r.filePath`; only this record's labels did not.
|
|
754
|
+
const byFile = indeterminateRuns.map((r) => ({
|
|
755
|
+
file: toRunnerDisplayPath(cwd, r.filePath),
|
|
756
|
+
reason: r.indeterminate?.reason,
|
|
757
|
+
...(r.indeterminate?.detail && { detail: r.indeterminate.detail }),
|
|
758
|
+
...(r.indeterminate?.budget && { budget: r.indeterminate.budget }),
|
|
759
|
+
...(r.indeterminate?.diagnostic && {
|
|
760
|
+
diagnostic: r.indeterminate.diagnostic,
|
|
761
|
+
}),
|
|
762
|
+
}));
|
|
474
763
|
logCascade({
|
|
475
764
|
phase: "cascade_indeterminate",
|
|
476
|
-
|
|
765
|
+
// The first indeterminate run's own file. `files[0] ?? cwd` survives only
|
|
766
|
+
// as a last resort for a run with no path at all.
|
|
767
|
+
filePath: indeterminateRuns[0]?.filePath ?? files[0] ?? cwd,
|
|
477
768
|
metadata: {
|
|
478
769
|
fileCount,
|
|
479
770
|
reasons: indeterminateRuns.map((r) => r.indeterminate?.reason),
|
|
771
|
+
byFile: byFile.slice(0, 20),
|
|
772
|
+
...(byFile.length > 20 && { byFileTruncated: byFile.length - 20 }),
|
|
480
773
|
},
|
|
481
774
|
});
|
|
482
775
|
}
|
|
@@ -530,7 +823,9 @@ export async function handleTurnEnd(deps) {
|
|
|
530
823
|
knipMeta = { skipped: true, reason: prevKnip.data.summary };
|
|
531
824
|
}
|
|
532
825
|
else {
|
|
533
|
-
const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns()
|
|
826
|
+
const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns(), {
|
|
827
|
+
projectSeq: runtime.projectSeq,
|
|
828
|
+
});
|
|
534
829
|
// Never overwrite a good scan with a failure (#925, #1467): the last
|
|
535
830
|
// good result stays until a new successful scan replaces it.
|
|
536
831
|
const knipWouldPoison = wouldPoisonCache(prevKnip, knipResult);
|
|
@@ -541,6 +836,7 @@ export async function handleTurnEnd(deps) {
|
|
|
541
836
|
cacheManager.writeCache("knip", knipResult, cwd);
|
|
542
837
|
}
|
|
543
838
|
knipMeta = {
|
|
839
|
+
execution: knipResult.execution ?? "executed",
|
|
544
840
|
success: knipResult.success,
|
|
545
841
|
totalIssues: knipResult.issues.length,
|
|
546
842
|
newIssues: 0,
|
|
@@ -550,7 +846,9 @@ export async function handleTurnEnd(deps) {
|
|
|
550
846
|
...(knipWouldPoison && { cacheKept: true }),
|
|
551
847
|
};
|
|
552
848
|
if (knipResult.success && knipResult.issues.length > 0) {
|
|
553
|
-
|
|
849
|
+
// Deliberately excludes the line number â see stableFindingKey's
|
|
850
|
+
// doc comment (#1483: mirrors the dead-code fix in #1477).
|
|
851
|
+
const issueKey = (i) => stableFindingKey(i.type, i.file, i.name, i.package);
|
|
554
852
|
const prevKeys = new Set((prevKnip?.data?.issues ?? []).map(issueKey));
|
|
555
853
|
const modifiedSet = new Set(files.map((f) => resolveRunnerPath(cwd, f)));
|
|
556
854
|
const newIssues = knipResult.issues.filter((issue) => {
|
|
@@ -582,6 +880,7 @@ export async function handleTurnEnd(deps) {
|
|
|
582
880
|
if (firstPath) {
|
|
583
881
|
report += ` First location: ${firstPath}\n`;
|
|
584
882
|
}
|
|
883
|
+
// @delivery-surface: runtime-turn:knip-blocker
|
|
585
884
|
blockerParts.push(report);
|
|
586
885
|
}
|
|
587
886
|
// Turn-end injects only this turn's HIGH-CONFIDENCE, ATTRIBUTABLE
|
|
@@ -601,6 +900,7 @@ export async function handleTurnEnd(deps) {
|
|
|
601
900
|
: "(unknown)";
|
|
602
901
|
report += ` ${display}${issue.line ? `:${issue.line}` : ""} â ${issue.name}\n`;
|
|
603
902
|
}
|
|
903
|
+
// @delivery-surface: runtime-turn:knip-advisory
|
|
604
904
|
advisoryParts.push(report);
|
|
605
905
|
}
|
|
606
906
|
}
|
|
@@ -724,6 +1024,7 @@ export async function handleTurnEnd(deps) {
|
|
|
724
1024
|
newIssueTotal += newIssues.length;
|
|
725
1025
|
projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
|
|
726
1026
|
projectDiagnosticsSources.add("dead-code");
|
|
1027
|
+
// @delivery-surface: runtime-turn:dead-code-advisory
|
|
727
1028
|
advisoryParts.push(formatDeadCodeDelta(newIssues, result.language));
|
|
728
1029
|
}
|
|
729
1030
|
catch (err) {
|
|
@@ -748,25 +1049,71 @@ export async function handleTurnEnd(deps) {
|
|
|
748
1049
|
durationMs: Date.now() - tDeadCode,
|
|
749
1050
|
metadata: deadCodeMeta,
|
|
750
1051
|
});
|
|
1052
|
+
// #1617: running total of findings this turn's advisory/blocker sections
|
|
1053
|
+
// dropped because an agent/user marked them false-positive/won't-fix â
|
|
1054
|
+
// the #1616 suppressed-bucket rule applied to turn_end's own reporting
|
|
1055
|
+
// lanes, so a mark's effect stays visible even though the finding itself
|
|
1056
|
+
// no longer appears above. Review-round F4 (#1625): kept per-lane, not
|
|
1057
|
+
// just a bare total, so the eventual trace says WHICH lane's marks did
|
|
1058
|
+
// the suppressing.
|
|
1059
|
+
let dispositionSuppressedTotal = 0;
|
|
1060
|
+
const dispositionSuppressedByLane = {};
|
|
1061
|
+
function recordDispositionSuppressed(lane, count) {
|
|
1062
|
+
if (count <= 0)
|
|
1063
|
+
return;
|
|
1064
|
+
dispositionSuppressedTotal += count;
|
|
1065
|
+
dispositionSuppressedByLane[lane] =
|
|
1066
|
+
(dispositionSuppressedByLane[lane] ?? 0) + count;
|
|
1067
|
+
}
|
|
751
1068
|
// govulncheck â surface session_start-cached Go CVE findings as advisory.
|
|
752
1069
|
// No per-turn re-run in this slice; the cache refreshes at next session_start.
|
|
753
1070
|
const govCacheEntry = cacheManager.readCache("govulncheck", cwd);
|
|
754
|
-
|
|
755
|
-
|
|
1071
|
+
// #1622: govulncheck renders a call site as `file:line`, and the cache is a
|
|
1072
|
+
// session_start snapshot â the same stale-line shape as gitleaks, one tier
|
|
1073
|
+
// lower. A CVE is pinned by go.mod, NOT by the call site, so neither an edit
|
|
1074
|
+
// nor a deletion may drop it: `onMissing: "demote"` routes a vanished traced
|
|
1075
|
+
// file into the same arm as an edited one. This gate only ever decides
|
|
1076
|
+
// whether the cited LINE is still worth printing. (Review round H1: the first
|
|
1077
|
+
// cut let a deleted trace file drop the CVE, contradicting this comment, and
|
|
1078
|
+
// `citedPath` reads only the FIRST filename frame â so one deleted file in a
|
|
1079
|
+
// long trace silently killed a CVE that go.mod still pins.)
|
|
1080
|
+
const govGate = gateFindingsByPathFreshness({
|
|
1081
|
+
store: "govulncheck",
|
|
1082
|
+
findings: govCacheEntry?.data?.findings ?? [],
|
|
1083
|
+
cwd,
|
|
1084
|
+
scannedAt: govCacheEntry?.data?.scannedAt,
|
|
1085
|
+
citedPath: (finding) => finding.trace.find((t) => t.filename)?.filename,
|
|
1086
|
+
onMissing: "demote",
|
|
1087
|
+
});
|
|
1088
|
+
const govStale = new Set(govGate.stale);
|
|
1089
|
+
// #1625 review round: the #1622 freshness gate runs FIRST â the disposition
|
|
1090
|
+
// filter's anchor is derived from each finding's post-demotion identity
|
|
1091
|
+
// (this array is already the gate's live+stale partition, never the raw
|
|
1092
|
+
// pre-gate cache). #1627's own post-gate guard (`if (govFindings.length)`)
|
|
1093
|
+
// and this round's disposition guard are the SAME guard â compose them,
|
|
1094
|
+
// never fall back to a raw-cache-length check (that would print the header
|
|
1095
|
+
// with zero rows beneath it whenever either filter empties the list).
|
|
1096
|
+
const govFiltered = filterFindingsByDisposition([...govGate.live, ...govGate.stale], cwd, (f) => govulncheckFindingToProjectDiagnostic(cwd, f));
|
|
1097
|
+
recordDispositionSuppressed("govulncheck", govFiltered.suppressed);
|
|
1098
|
+
const govFindings = govFiltered.kept;
|
|
1099
|
+
if (govFindings.length) {
|
|
1100
|
+
const findings = govFindings.slice(0, 5);
|
|
756
1101
|
let report = "đĄī¸ Go CVEs reachable from this code (govulncheck) â upgrade where possible:\n";
|
|
757
1102
|
for (const f of findings) {
|
|
758
1103
|
const callSite = f.trace.find((t) => t.filename);
|
|
1104
|
+
const stale = govStale.has(f);
|
|
759
1105
|
const where = callSite?.filename
|
|
760
|
-
? `${toRunnerDisplayPath(cwd, callSite.filename)}${callSite.line ? `:${callSite.line}` : ""}`
|
|
1106
|
+
? `${toRunnerDisplayPath(cwd, callSite.filename)}${!stale && callSite.line ? `:${callSite.line}` : ""}${stale ? ` ${STALE_LINE_MARKER}` : ""}`
|
|
761
1107
|
: (f.module ?? f.packageName ?? "(module)");
|
|
762
1108
|
const fix = f.fixedVersion
|
|
763
1109
|
? ` â upgrade to ${f.fixedVersion} or later`
|
|
764
1110
|
: " â no fix yet, track upstream";
|
|
765
1111
|
report += ` ${f.osv} (${where})${fix}\n`;
|
|
766
1112
|
}
|
|
767
|
-
if (
|
|
768
|
-
report += ` âĻ and ${
|
|
1113
|
+
if (govFindings.length > findings.length) {
|
|
1114
|
+
report += ` âĻ and ${govFindings.length - findings.length} more\n`;
|
|
769
1115
|
}
|
|
1116
|
+
// @delivery-surface: runtime-turn:govulncheck-advisory
|
|
770
1117
|
advisoryParts.push(report);
|
|
771
1118
|
}
|
|
772
1119
|
const trivyCacheEntry = cacheManager.readCache("trivy", cwd);
|
|
@@ -776,6 +1123,7 @@ export async function handleTurnEnd(deps) {
|
|
|
776
1123
|
// location so a committed/hardcoded secret is reported ONCE (with combined
|
|
777
1124
|
// provenance) â a blocker, since credentials need rotation before merge.
|
|
778
1125
|
const gitleaksData = cacheManager.readCache("gitleaks", cwd)?.data;
|
|
1126
|
+
const trivySecretsData = trivyCacheEntry?.data;
|
|
779
1127
|
// #1461 slice 1 (#1460): the gitleaks cache is TTL-only, so a finding for a
|
|
780
1128
|
// file deleted after the scan is still served as a đ´ blocker for the rest
|
|
781
1129
|
// of the 30-minute window â the live case, and 119 of 126 findings in
|
|
@@ -783,20 +1131,85 @@ export async function handleTurnEnd(deps) {
|
|
|
783
1131
|
// store (session_start's read only decides whether to re-scan; the
|
|
784
1132
|
// project-diagnostics path re-scans fresh and reconciles at load), so the
|
|
785
1133
|
// drop belongs here, before the findings enter the shared secret pipeline.
|
|
786
|
-
//
|
|
787
|
-
|
|
1134
|
+
// #1622 extends that gate from existence to freshness, and adds trivy
|
|
1135
|
+
// secrets â the sibling store with the identical shape. A cited file edited
|
|
1136
|
+
// after the scan keeps its finding but loses its line number: the credential
|
|
1137
|
+
// may still be there, just not where the snapshot says. Dropping instead
|
|
1138
|
+
// would let any edit â malicious or accidental â mute a real secret.
|
|
1139
|
+
const gitleaksGate = gateFindingsByPathFreshness({
|
|
788
1140
|
store: "gitleaks",
|
|
789
1141
|
findings: gitleaksData?.findings ?? [],
|
|
790
1142
|
cwd,
|
|
1143
|
+
scannedAt: gitleaksData?.scannedAt,
|
|
791
1144
|
citedPath: (finding) => finding.file,
|
|
792
1145
|
});
|
|
1146
|
+
const trivySecretsGate = gateFindingsByPathFreshness({
|
|
1147
|
+
store: "trivy-secrets",
|
|
1148
|
+
findings: trivySecretsData?.secrets ?? [],
|
|
1149
|
+
cwd,
|
|
1150
|
+
scannedAt: trivySecretsData?.scannedAt,
|
|
1151
|
+
citedPath: (finding) => finding.file,
|
|
1152
|
+
});
|
|
1153
|
+
// #1617: THE bug this issue exists for â gitleaks findings never passed
|
|
1154
|
+
// through `applyDispositions`, so an agent-marked false-positive/won't-fix
|
|
1155
|
+
// re-reported as a đ´ STOP blocker on every turn. Filter through the SAME
|
|
1156
|
+
// `gitleaksFindingToProjectDiagnostic` identity `lens_diagnostics
|
|
1157
|
+
// mode=full` surfaces (tool="gitleaks", rule="gitleaks:<ruleId>", the
|
|
1158
|
+
// exact "Potential secret: âĻ" message) so a mark made against what the
|
|
1159
|
+
// agent was shown is honored here too.
|
|
1160
|
+
//
|
|
1161
|
+
// #1625 review round: filtered AFTER the #1622 freshness gate above â the
|
|
1162
|
+
// anchor is derived from each finding's post-demotion identity, never the
|
|
1163
|
+
// raw pre-gate cache. Applied to BOTH `gitleaksGate.live` AND
|
|
1164
|
+
// `gitleaksGate.stale`: `staleSecretEntries` below derives from the stale
|
|
1165
|
+
// arm, and an fp-marked finding that later goes stale must not reappear
|
|
1166
|
+
// there â a suppression escape (and a double count against
|
|
1167
|
+
// `dispositionSuppressedTotal`) that a live-only filter would have missed.
|
|
1168
|
+
//
|
|
1169
|
+
// #1628: trivy-secret findings get the SAME treatment, now that
|
|
1170
|
+
// `trivySecretFindingToProjectDiagnostic` (project-diagnostics/runner-
|
|
1171
|
+
// adapters/trivy.ts) gives them a `lens_diagnostics`-surfaced identity
|
|
1172
|
+
// (tool="trivy", rule="trivy-secret:<ruleId>") to anchor a mark against â
|
|
1173
|
+
// same pattern as gitleaks above, applied to both the live and stale arms
|
|
1174
|
+
// for the same reason.
|
|
1175
|
+
//
|
|
1176
|
+
// ast-grep secret findings need no filtering here â they already went
|
|
1177
|
+
// through dispatch's applyDispositions before reaching
|
|
1178
|
+
// `peekActionableWarnings()`.
|
|
1179
|
+
const gitleaksLiveFiltered = filterFindingsByDisposition(gitleaksGate.live, cwd, (f) => gitleaksFindingToProjectDiagnostic(cwd, f));
|
|
1180
|
+
const gitleaksStaleFiltered = filterFindingsByDisposition(gitleaksGate.stale, cwd, (f) => gitleaksFindingToProjectDiagnostic(cwd, f));
|
|
1181
|
+
recordDispositionSuppressed("gitleaks", gitleaksLiveFiltered.suppressed + gitleaksStaleFiltered.suppressed);
|
|
1182
|
+
const trivySecretsLiveFiltered = filterFindingsByDisposition(trivySecretsGate.live, cwd, (f) => trivySecretFindingToProjectDiagnostic(cwd, f));
|
|
1183
|
+
const trivySecretsStaleFiltered = filterFindingsByDisposition(trivySecretsGate.stale, cwd, (f) => trivySecretFindingToProjectDiagnostic(cwd, f));
|
|
1184
|
+
recordDispositionSuppressed("trivy-secrets", trivySecretsLiveFiltered.suppressed + trivySecretsStaleFiltered.suppressed);
|
|
793
1185
|
const astSecretWarnings = runtime
|
|
794
1186
|
.peekActionableWarnings()
|
|
795
1187
|
.filter(isSecretWarning);
|
|
796
1188
|
const sessionSecrets = dedupeSecretFindings([
|
|
797
|
-
...fromGitleaks(
|
|
798
|
-
...fromTrivySecrets(
|
|
1189
|
+
...fromGitleaks(gitleaksLiveFiltered.kept),
|
|
1190
|
+
...fromTrivySecrets(trivySecretsLiveFiltered.kept),
|
|
799
1191
|
]);
|
|
1192
|
+
// Demoted secrets are addressed by FILE, never by line â the line is the one
|
|
1193
|
+
// field the edit invalidated. Rule id and source survive it and must be
|
|
1194
|
+
// carried through (review round M1): an agent triages an `aws-access-token`
|
|
1195
|
+
// differently from a low-confidence `generic-api-key`, and cannot do that
|
|
1196
|
+
// from a bare path. Deduped on file+rule+source so a file with twenty stale
|
|
1197
|
+
// hits of one rule is named once.
|
|
1198
|
+
const staleSecretEntries = [
|
|
1199
|
+
...gitleaksStaleFiltered.kept.map((f) => ({
|
|
1200
|
+
file: toRunnerDisplayPath(cwd, f.file),
|
|
1201
|
+
rule: f.ruleId,
|
|
1202
|
+
source: "gitleaks",
|
|
1203
|
+
})),
|
|
1204
|
+
...trivySecretsStaleFiltered.kept.map((f) => ({
|
|
1205
|
+
file: toRunnerDisplayPath(cwd, f.file),
|
|
1206
|
+
rule: f.ruleId,
|
|
1207
|
+
source: "trivy",
|
|
1208
|
+
})),
|
|
1209
|
+
];
|
|
1210
|
+
const staleSecrets = [
|
|
1211
|
+
...new Map(staleSecretEntries.map((e) => [`${e.file}|${e.rule}|${e.source}`, e])).values(),
|
|
1212
|
+
];
|
|
800
1213
|
// Locations already surfaced as session-scan secret blockers â used to enrich
|
|
801
1214
|
// provenance where ast-grep agrees and to suppress the duplicate ast-grep copy
|
|
802
1215
|
// from the actionable-warnings advisory below.
|
|
@@ -817,14 +1230,48 @@ export async function handleTurnEnd(deps) {
|
|
|
817
1230
|
if (enriched.length > shown.length) {
|
|
818
1231
|
report += ` âĻ and ${enriched.length - shown.length} more\n`;
|
|
819
1232
|
}
|
|
1233
|
+
// @delivery-surface: runtime-turn:secrets-gitleaks,runtime-turn:secrets-trivy
|
|
820
1234
|
blockerParts.push(report);
|
|
821
1235
|
}
|
|
1236
|
+
if (staleSecrets.length) {
|
|
1237
|
+
// Its OWN tier, never `advisoryParts` (review round M2). The advisory tier
|
|
1238
|
+
// is labelled "no action required this turn", which would sit directly
|
|
1239
|
+
// above copy telling the agent to re-scan â a section that contradicts its
|
|
1240
|
+
// own heading. This preamble is imperative because the action is real: the
|
|
1241
|
+
// finding is unverified, not dismissed.
|
|
1242
|
+
const shown = staleSecrets.slice(0, 5);
|
|
1243
|
+
let report = `đ ACTION NEEDED â secrets were flagged in files that changed after the scan. ${STALE_LINE_MARKER}\n` +
|
|
1244
|
+
"The cached line numbers are no longer trustworthy, so they are withheld. Re-run a secrets scan to confirm or clear these:\n";
|
|
1245
|
+
for (const entry of shown) {
|
|
1246
|
+
report += ` ${entry.file} â ${entry.rule} [${entry.source}]\n`;
|
|
1247
|
+
}
|
|
1248
|
+
if (staleSecrets.length > shown.length) {
|
|
1249
|
+
report += ` âĻ and ${staleSecrets.length - shown.length} more\n`;
|
|
1250
|
+
}
|
|
1251
|
+
// @delivery-surface: runtime-turn:stale-secrets-tier
|
|
1252
|
+
staleSecretParts.push(report);
|
|
1253
|
+
}
|
|
822
1254
|
// trivy â surface session_start-cached dependency CVEs (#131, Phase 1).
|
|
823
1255
|
// CRITICAL is a blocker (a known-exploitable CVE in a shipped dep is real
|
|
824
1256
|
// production risk); HIGH/MEDIUM/LOW are advisory. The agent gets the upgrade
|
|
825
1257
|
// target as a hint and decides â we never auto-edit lockfiles.
|
|
826
|
-
|
|
827
|
-
|
|
1258
|
+
//
|
|
1259
|
+
// #1634: these three trivy reports (critical blocker, non-critical
|
|
1260
|
+
// advisory, license advisory below) name a PACKAGE, not a file:line â there
|
|
1261
|
+
// is no cited path for `gateFindingsByPathFreshness` to stat, so unlike the
|
|
1262
|
+
// secrets/govulncheck stores above this store cannot be freshness-GATED.
|
|
1263
|
+
// It is the delivery gate's explicit-label escape hatch instead
|
|
1264
|
+
// (`clients/finding-delivery-gate.ts`, surfaces `runtime-turn:trivy-*`):
|
|
1265
|
+
// the session_start cache can be arbitrarily old, so its age is stated
|
|
1266
|
+
// plainly rather than presenting a CRITICAL blocker as if it were current.
|
|
1267
|
+
// This runs on top of (not instead of) #1625's disposition filter below â
|
|
1268
|
+
// a suppressed finding never reaches this render at all, so the two only
|
|
1269
|
+
// ever compose.
|
|
1270
|
+
const trivyAgeLabel = formatCacheAgeLabel(trivyCacheEntry?.data?.scannedAt);
|
|
1271
|
+
const trivyFindingsFiltered = filterFindingsByDisposition(trivyCacheEntry?.data?.findings ?? [], cwd, (f) => trivyFindingToProjectDiagnostic(cwd, f));
|
|
1272
|
+
recordDispositionSuppressed("trivy", trivyFindingsFiltered.suppressed);
|
|
1273
|
+
if (trivyFindingsFiltered.kept.length) {
|
|
1274
|
+
const all = trivyFindingsFiltered.kept;
|
|
828
1275
|
const critical = all.filter((f) => f.severity === "CRITICAL");
|
|
829
1276
|
const advisory = all.filter((f) => f.severity !== "CRITICAL");
|
|
830
1277
|
const fmt = (f) => {
|
|
@@ -838,32 +1285,35 @@ export async function handleTurnEnd(deps) {
|
|
|
838
1285
|
};
|
|
839
1286
|
if (critical.length) {
|
|
840
1287
|
const shown = critical.slice(0, 5);
|
|
841
|
-
let report =
|
|
1288
|
+
let report = `đ´ STOP â CRITICAL dependency CVEs (trivy, ${trivyAgeLabel}). Upgrade before shipping:\n`;
|
|
842
1289
|
for (const f of shown)
|
|
843
1290
|
report += fmt(f);
|
|
844
1291
|
if (critical.length > shown.length) {
|
|
845
1292
|
report += ` âĻ and ${critical.length - shown.length} more\n`;
|
|
846
1293
|
}
|
|
1294
|
+
// @delivery-surface: runtime-turn:trivy-critical-blocker
|
|
847
1295
|
blockerParts.push(report);
|
|
848
1296
|
}
|
|
849
1297
|
if (advisory.length) {
|
|
850
1298
|
const shown = advisory.slice(0, 5);
|
|
851
|
-
let report =
|
|
1299
|
+
let report = `đĄī¸ Dependency CVEs (trivy, ${trivyAgeLabel}) â upgrade where possible:\n`;
|
|
852
1300
|
for (const f of shown)
|
|
853
1301
|
report += fmt(f);
|
|
854
1302
|
if (advisory.length > shown.length) {
|
|
855
1303
|
report += ` âĻ and ${advisory.length - shown.length} more\n`;
|
|
856
1304
|
}
|
|
1305
|
+
// @delivery-surface: runtime-turn:trivy-cve-advisory
|
|
857
1306
|
advisoryParts.push(report);
|
|
858
1307
|
}
|
|
859
1308
|
}
|
|
860
1309
|
// trivy â dependency license risk (#131 Mode 4). Advisory only: a copyleft /
|
|
861
1310
|
// restricted license in a proprietary tree is a compliance signal, not a
|
|
862
|
-
// build break. Surfaced from the same cached `trivy fs` pass
|
|
1311
|
+
// build break. Surfaced from the same cached `trivy fs` pass â same #1634
|
|
1312
|
+
// explicit-label rationale as the CVE reports above (no cited path to gate).
|
|
863
1313
|
const licenses = trivyCacheEntry?.data?.licenses ?? [];
|
|
864
1314
|
if (licenses.length) {
|
|
865
1315
|
const shown = licenses.slice(0, 5);
|
|
866
|
-
let report =
|
|
1316
|
+
let report = `đ Dependency license risk (trivy, ${trivyAgeLabel}) â review for compliance:\n`;
|
|
867
1317
|
for (const l of shown) {
|
|
868
1318
|
const cat = l.category ? `, ${l.category}` : "";
|
|
869
1319
|
report += ` ${l.pkgName} â ${l.license} (${l.severity}${cat})\n`;
|
|
@@ -871,11 +1321,30 @@ export async function handleTurnEnd(deps) {
|
|
|
871
1321
|
if (licenses.length > shown.length) {
|
|
872
1322
|
report += ` âĻ and ${licenses.length - shown.length} more\n`;
|
|
873
1323
|
}
|
|
1324
|
+
// @delivery-surface: runtime-turn:trivy-license-advisory
|
|
874
1325
|
advisoryParts.push(report);
|
|
875
1326
|
}
|
|
1327
|
+
// #1616 suppressed-bucket rule: surface the running disposition-drop total
|
|
1328
|
+
// as its own advisory line so a mark's effect is visible, not a silent
|
|
1329
|
+
// absence â trace, not a vanish.
|
|
1330
|
+
if (dispositionSuppressedTotal > 0) {
|
|
1331
|
+
// Review-round F4 (#1625): per-lane attribution, e.g.
|
|
1332
|
+
// "gitleaks 2, govulncheck 1" â not just a bare total.
|
|
1333
|
+
const byLane = Object.entries(dispositionSuppressedByLane)
|
|
1334
|
+
.map(([lane, count]) => `${lane} ${count}`)
|
|
1335
|
+
.join(", ");
|
|
1336
|
+
// @delivery-surface: runtime-turn:disposition-suppressed-notice
|
|
1337
|
+
advisoryParts.push(`suppressed by disposition: ${dispositionSuppressedTotal} finding(s) ` +
|
|
1338
|
+
`dropped from this turn's gitleaks/govulncheck/trivy sections (${byLane}) ` +
|
|
1339
|
+
"(marked false-positive or won't-fix).");
|
|
1340
|
+
}
|
|
876
1341
|
const t3 = Date.now();
|
|
877
1342
|
let madgeStats;
|
|
878
|
-
|
|
1343
|
+
// Off by default (#766): this pass only writes debug output, and user-facing
|
|
1344
|
+
// madge diagnostics come from the session-start `madge` cache + the
|
|
1345
|
+
// `lens_diagnostics` extractor. Enabled with `--lens-turn-end-madge` /
|
|
1346
|
+
// `turnEnd.madge.enabled=true` for those who want the per-edit circular note.
|
|
1347
|
+
if (getFlag("lens-turn-end-madge") && (await depChecker.ensureAvailable())) {
|
|
879
1348
|
const madgeFiles = cacheManager.getFilesForMadge(cwd);
|
|
880
1349
|
if (madgeFiles.length > 0) {
|
|
881
1350
|
dbg(`turn_end: madge checking ${madgeFiles.length} file(s) for circular deps`);
|
|
@@ -911,7 +1380,9 @@ export async function handleTurnEnd(deps) {
|
|
|
911
1380
|
filePath: cwd,
|
|
912
1381
|
phase: "madge",
|
|
913
1382
|
durationMs: Date.now() - t3,
|
|
914
|
-
|
|
1383
|
+
// A ~0ms entry with no metadata is indistinguishable from "ran and was
|
|
1384
|
+
// fast" when re-analyzing the #766 tail â mark the skipped case.
|
|
1385
|
+
metadata: madgeStats ?? { skipped: true },
|
|
915
1386
|
});
|
|
916
1387
|
// --- Test runner: fire once per turn after all edits are done ---
|
|
917
1388
|
// Runs for each unique test target across modified files; results appear
|
|
@@ -966,8 +1437,14 @@ export async function handleTurnEnd(deps) {
|
|
|
966
1437
|
const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
967
1438
|
const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
|
|
968
1439
|
const provenanceFiles = [
|
|
969
|
-
...candidates.map((candidate) => ({
|
|
970
|
-
|
|
1440
|
+
...candidates.map((candidate) => ({
|
|
1441
|
+
path: candidate.abs,
|
|
1442
|
+
role: "source",
|
|
1443
|
+
})),
|
|
1444
|
+
...targets.map((target) => ({
|
|
1445
|
+
path: target.testFile,
|
|
1446
|
+
role: "test",
|
|
1447
|
+
})),
|
|
971
1448
|
];
|
|
972
1449
|
const launchedFrom = snapshotAdvisoryProvenance({
|
|
973
1450
|
cwd,
|
|
@@ -1029,7 +1506,16 @@ export async function handleTurnEnd(deps) {
|
|
|
1029
1506
|
? `error: ${error}`
|
|
1030
1507
|
: `${verdict} ${passed}p/${failed}f (${elapsed})`;
|
|
1031
1508
|
dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} â ${summary}`);
|
|
1032
|
-
|
|
1509
|
+
// #1524: also fires on `error` alone, not just `failed > 0`.
|
|
1510
|
+
// A runner-error result (the suite never started â spawn/
|
|
1511
|
+
// config failure) has `failed === 0` by construction, so
|
|
1512
|
+
// gating on `failed > 0` alone dropped it silently: the
|
|
1513
|
+
// agent got no context at all, and the empty `failures`
|
|
1514
|
+
// array below sent this result down the "all tests
|
|
1515
|
+
// passed" branch, clearing any prior real test-failure
|
|
1516
|
+
// git-guard blocker. `formatResult` already renders the
|
|
1517
|
+
// error-only case as "Could not run tests: ...".
|
|
1518
|
+
if (failed > 0 || error) {
|
|
1033
1519
|
const formatted = testRunnerClient.formatResult(r.value);
|
|
1034
1520
|
if (formatted)
|
|
1035
1521
|
failures.push(formatted);
|
|
@@ -1037,7 +1523,8 @@ export async function handleTurnEnd(deps) {
|
|
|
1037
1523
|
}
|
|
1038
1524
|
if (failures.length > 0) {
|
|
1039
1525
|
const currentGeneration = cacheManager.readCache("test-runner-findings", cwd)?.data?.testRunGeneration;
|
|
1040
|
-
if (currentGeneration !== undefined &&
|
|
1526
|
+
if (currentGeneration !== undefined &&
|
|
1527
|
+
currentGeneration > testRunGeneration) {
|
|
1041
1528
|
dbg(`turn_end: test generation ${testRunGeneration} superseded by ${currentGeneration}`);
|
|
1042
1529
|
return;
|
|
1043
1530
|
}
|
|
@@ -1055,9 +1542,25 @@ export async function handleTurnEnd(deps) {
|
|
|
1055
1542
|
superseded,
|
|
1056
1543
|
}, cwd);
|
|
1057
1544
|
if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1545
|
+
// #1524: `&& !value.error` â a runner-error result has
|
|
1546
|
+
// `failed === 0` (the suite never ran, so nothing could
|
|
1547
|
+
// fail), but it is not a pass. Without the filter it
|
|
1548
|
+
// would clear a prior real test-failure git-guard
|
|
1549
|
+
// blocker on the strength of a suite that never
|
|
1550
|
+
// started. And the call itself is skipped when this
|
|
1551
|
+
// list is empty rather than passed as `[]`:
|
|
1552
|
+
// `clearGitGuardTestFailure`'s own empty-array
|
|
1553
|
+
// fallback treats "no files named" as "clear every
|
|
1554
|
+
// blocked file", so an all-error batch (one go file,
|
|
1555
|
+
// runner-error, zero clean files) would otherwise
|
|
1556
|
+
// clear every blocker through that fallback instead
|
|
1557
|
+
// of clearing none.
|
|
1558
|
+
const cleanFiles = resultValues
|
|
1559
|
+
.filter((value) => value.failed === 0 && !value.error)
|
|
1560
|
+
.map((value) => value.file);
|
|
1561
|
+
if (cleanFiles.length > 0) {
|
|
1562
|
+
clearGitGuardTestFailure(cacheManager, cwd, runtime, cleanFiles);
|
|
1563
|
+
}
|
|
1061
1564
|
mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, resultValues
|
|
1062
1565
|
.filter((value) => value.failed > 0)
|
|
1063
1566
|
.map((value) => value.file));
|
|
@@ -1065,7 +1568,8 @@ export async function handleTurnEnd(deps) {
|
|
|
1065
1568
|
dbg(`turn_end: ${failures.length} test failure(s) cached for next context injection${stale ? " (stale â turn advanced while tests ran)" : ""}`);
|
|
1066
1569
|
}
|
|
1067
1570
|
else if (results.length > 0) {
|
|
1068
|
-
if (getFlag("lens-guard") &&
|
|
1571
|
+
if (getFlag("lens-guard") &&
|
|
1572
|
+
firedSessionId === runtime.telemetrySessionId) {
|
|
1069
1573
|
clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues.map((value) => value.file));
|
|
1070
1574
|
}
|
|
1071
1575
|
dbg(`turn_end: all tests passed${stale ? " (stale â turn advanced while tests ran)" : ""}`);
|
|
@@ -1098,6 +1602,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1098
1602
|
// into an authoritative-looking clean result for the rest of the file.
|
|
1099
1603
|
// Keep the limitation visible and require a complete graph for this
|
|
1100
1604
|
// user-facing impact surface (#1070).
|
|
1605
|
+
// @delivery-surface: runtime-turn:call-graph-advisory
|
|
1101
1606
|
advisoryParts.push("Call-graph impact was not emitted because call-graph extraction coverage is incomplete; " +
|
|
1102
1607
|
"the affected files may have unreported callers.");
|
|
1103
1608
|
}
|
|
@@ -1115,7 +1620,8 @@ export async function handleTurnEnd(deps) {
|
|
|
1115
1620
|
const changedFileKey = normalizeMapKey(resolveRunnerPath(cwd, filePath));
|
|
1116
1621
|
const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => {
|
|
1117
1622
|
const graphFilePath = parseSymbolKey(k).filePath;
|
|
1118
|
-
return normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) ===
|
|
1623
|
+
return (normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) ===
|
|
1624
|
+
changedFileKey);
|
|
1119
1625
|
});
|
|
1120
1626
|
for (const calleeKey of fileCallerKeys.slice(0, 3)) {
|
|
1121
1627
|
// #1080: drop KNOWN test-role callers BEFORE both the human advisory
|
|
@@ -1139,6 +1645,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1139
1645
|
}
|
|
1140
1646
|
}
|
|
1141
1647
|
if (impactLines.length > 0) {
|
|
1648
|
+
// @delivery-surface: runtime-turn:call-graph-advisory
|
|
1142
1649
|
advisoryParts.push(`đ Call-graph impact (changed symbols have callers):\n${impactLines.join("\n")}`);
|
|
1143
1650
|
}
|
|
1144
1651
|
if (impactFindings.length > 0) {
|
|
@@ -1207,6 +1714,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1207
1714
|
writeActionableWarningsReport(cacheManager, cwd, report);
|
|
1208
1715
|
appendActionableWarningsHistory(cwd, report);
|
|
1209
1716
|
const advisory = formatActionableWarningsAdvisory(report);
|
|
1717
|
+
// @delivery-surface: runtime-turn:actionable-warnings-advisory
|
|
1210
1718
|
if (advisory)
|
|
1211
1719
|
advisoryParts.push(advisory);
|
|
1212
1720
|
logActionableWarningsEvent({
|
|
@@ -1256,6 +1764,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1256
1764
|
writeCodeQualityWarningsReport(cacheManager, cwd, qualityReport);
|
|
1257
1765
|
appendCodeQualityWarningsHistory(cwd, qualityReport);
|
|
1258
1766
|
const advisory = formatCodeQualityWarningsAdvisory(qualityReport);
|
|
1767
|
+
// @delivery-surface: runtime-turn:code-quality-warnings-advisory
|
|
1259
1768
|
if (advisory)
|
|
1260
1769
|
advisoryParts.push(advisory);
|
|
1261
1770
|
logLatency({
|
|
@@ -1283,7 +1792,13 @@ export async function handleTurnEnd(deps) {
|
|
|
1283
1792
|
}
|
|
1284
1793
|
cacheManager.incrementTurnCycle(cwd, currentOwner);
|
|
1285
1794
|
const labeledAdvisoryParts = advisoryParts.map((p) => `âšī¸ Advisory â no action required this turn:\n${p}`);
|
|
1286
|
-
|
|
1795
|
+
// Stale-secret parts sit between the two tiers and are NOT relabelled â they
|
|
1796
|
+
// ship the imperative preamble they were built with (#1622 review M2).
|
|
1797
|
+
const findingParts = [
|
|
1798
|
+
...blockerParts,
|
|
1799
|
+
...staleSecretParts,
|
|
1800
|
+
...labeledAdvisoryParts,
|
|
1801
|
+
];
|
|
1287
1802
|
if (findingParts.length > 0) {
|
|
1288
1803
|
dbg(`turn_end: ${blockerParts.length} blocker section(s), ${advisoryParts.length} advisory section(s) found, persisting for next context`);
|
|
1289
1804
|
const content = capTurnEndMessage(findingParts.join("\n\n"));
|
|
@@ -1308,7 +1823,12 @@ export async function handleTurnEnd(deps) {
|
|
|
1308
1823
|
blockingFiles: blockerParts.length > 0 ? existingGuard.affectedFiles : undefined,
|
|
1309
1824
|
projectSeqStart: runtime.turnStartProjectSeq,
|
|
1310
1825
|
projectSeqEnd: runtime.projectSeq,
|
|
1311
|
-
fileSeqByPath: Object.fromEntries(runtime
|
|
1826
|
+
fileSeqByPath: Object.fromEntries(runtime
|
|
1827
|
+
.getFileSeqEntries()
|
|
1828
|
+
.map(([filePath, seq]) => [
|
|
1829
|
+
normalizeMapKey(path.resolve(filePath)),
|
|
1830
|
+
seq,
|
|
1831
|
+
])),
|
|
1312
1832
|
fileContentHashes: {},
|
|
1313
1833
|
consumed: false,
|
|
1314
1834
|
});
|
|
@@ -1361,8 +1881,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1361
1881
|
.filter((neighbor) => neighbor.diagnostics.length > 0)
|
|
1362
1882
|
.map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
|
|
1363
1883
|
];
|
|
1364
|
-
const affectedFiles = [...new Set(allAffectedFiles)]
|
|
1365
|
-
.slice(0, MAX_ADVISORY_AFFECTED_FILES);
|
|
1884
|
+
const affectedFiles = [...new Set(allAffectedFiles)].slice(0, MAX_ADVISORY_AFFECTED_FILES);
|
|
1366
1885
|
const affectedFilesTruncated = new Set(allAffectedFiles).size > affectedFiles.length;
|
|
1367
1886
|
cacheManager.writeCache("turn-end-findings", {
|
|
1368
1887
|
content,
|
|
@@ -1372,7 +1891,10 @@ export async function handleTurnEnd(deps) {
|
|
|
1372
1891
|
cwd,
|
|
1373
1892
|
runtime,
|
|
1374
1893
|
generation: 0,
|
|
1375
|
-
files: affectedFiles.map((file) => ({
|
|
1894
|
+
files: affectedFiles.map((file) => ({
|
|
1895
|
+
path: file,
|
|
1896
|
+
role: "affected",
|
|
1897
|
+
})),
|
|
1376
1898
|
truncated: affectedFilesTruncated,
|
|
1377
1899
|
}),
|
|
1378
1900
|
}, cwd);
|
|
@@ -1395,7 +1917,13 @@ export async function handleTurnEnd(deps) {
|
|
|
1395
1917
|
}
|
|
1396
1918
|
if (blockerParts.length === 0) {
|
|
1397
1919
|
cacheManager.clearTurnState(cwd, currentOwner);
|
|
1398
|
-
|
|
1920
|
+
// `staleSecretParts` counts here too (#1622 review M2): clearing the
|
|
1921
|
+
// findings record while a stale secret is still unverified would drop the
|
|
1922
|
+
// only surviving trace of it.
|
|
1923
|
+
if (getFlag("lens-guard") &&
|
|
1924
|
+
advisoryParts.length === 0 &&
|
|
1925
|
+
staleSecretParts.length === 0 &&
|
|
1926
|
+
!runtime.gitGuardHasBlockers) {
|
|
1399
1927
|
const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
1400
1928
|
if (guardRecord?.sessionId === runtime.telemetrySessionId &&
|
|
1401
1929
|
guardRecord.testFailures !== true) {
|
|
@@ -1406,16 +1934,41 @@ export async function handleTurnEnd(deps) {
|
|
|
1406
1934
|
runtime.fixedThisTurn.clear();
|
|
1407
1935
|
runtime.clearActionableWarnings();
|
|
1408
1936
|
runtime.clearCodeQualityWarnings();
|
|
1937
|
+
if (demotedFindingsRetired > 0) {
|
|
1938
|
+
// #1944: the retired payload must not survive as a SUPPRESSION key.
|
|
1939
|
+
// `turn-end-findings-last` holds a content signature used to silence a
|
|
1940
|
+
// duplicate re-prompt; live evidence found the demoted payload still on
|
|
1941
|
+
// disk 80+ minutes after the file shrank. It is not a delivery source â
|
|
1942
|
+
// `runtime-context.ts` never reads it, and the read at the top of this
|
|
1943
|
+
// function is gated on the current `sessionId`, so it cannot resurrect
|
|
1944
|
+
// the finding in a later session. It CAN, however, silence a genuinely
|
|
1945
|
+
// new report of content the store no longer holds. Drop it with the
|
|
1946
|
+
// record it describes.
|
|
1947
|
+
cacheManager.clearCache("turn-end-findings-last", cwd);
|
|
1948
|
+
}
|
|
1409
1949
|
logLatency({
|
|
1410
1950
|
type: "tool_result",
|
|
1411
1951
|
toolName: "turn_end",
|
|
1412
1952
|
filePath: cwd,
|
|
1413
1953
|
durationMs: Date.now() - turnEndStart,
|
|
1414
|
-
|
|
1954
|
+
// #1622 review M2: a pending stale secret is NOT a clean turn. It gets its
|
|
1955
|
+
// own result rather than being promoted to `blockers_found`, which would
|
|
1956
|
+
// undo the demotion the freshness gate just made.
|
|
1957
|
+
result: blockerParts.length > 0
|
|
1958
|
+
? "blockers_found"
|
|
1959
|
+
: staleSecretParts.length > 0
|
|
1960
|
+
? "stale_secrets_pending"
|
|
1961
|
+
: "clean",
|
|
1415
1962
|
metadata: {
|
|
1416
1963
|
fileCount: files.length,
|
|
1417
1964
|
blockerSections: blockerParts.length,
|
|
1965
|
+
staleSecretSections: staleSecretParts.length,
|
|
1418
1966
|
advisorySections: advisoryParts.length,
|
|
1967
|
+
// #1944 AC3: an empty advisory section on its own cannot say whether
|
|
1968
|
+
// the turn had nothing to report or dropped something. This counter
|
|
1969
|
+
// answers that from latency.log even when the payload is empty, and
|
|
1970
|
+
// the payload itself carries the retirement note when it is not.
|
|
1971
|
+
demotedFindingsRetired,
|
|
1419
1972
|
},
|
|
1420
1973
|
});
|
|
1421
1974
|
resetFormatService();
|