@herbertgao/pi-extensions 2026.9.15 → 2026.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
- package/node_modules/pi-lens/CHANGELOG.md +498 -0
- package/node_modules/pi-lens/README.md +45 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
- package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
- package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
- package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
- package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
- package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
- package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
- package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
- package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
- package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
- package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
- package/node_modules/pi-lens/dist/index.js +5695 -3733
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
- package/node_modules/pi-lens/dist/mcp/server.js +33 -21
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
- package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
- package/node_modules/pi-lens/docs/adr/README.md +26 -0
- package/node_modules/pi-lens/docs/agent-tools.md +18 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
- package/node_modules/pi-lens/docs/configuration.md +55 -1
- package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
- package/node_modules/pi-lens/docs/environment-variables.md +61 -4
- package/node_modules/pi-lens/docs/features.md +2 -2
- package/node_modules/pi-lens/docs/globalconfig.md +6 -1
- package/node_modules/pi-lens/docs/language-coverage.md +17 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
- package/node_modules/pi-lens/docs/settings.md +12 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
- package/node_modules/pi-lens/package.json +5 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
- package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
- package/node_modules/pi-mcp-adapter/README.md +12 -5
- package/node_modules/pi-mcp-adapter/config.ts +31 -8
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +37 -20
- package/node_modules/pi-mcp-adapter/init.ts +14 -15
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +3 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
- package/node_modules/pi-mcp-adapter/types.ts +2 -2
- package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
- package/node_modules/pi-mcp-adapter/utils.ts +11 -3
- package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
- package/node_modules/pi-next-cue/LICENSE +21 -0
- package/node_modules/pi-next-cue/README.md +81 -0
- package/node_modules/pi-next-cue/index.ts +358 -0
- package/node_modules/pi-next-cue/package.json +28 -0
- package/package.json +9 -6
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { buildActionableWarningsReport, formatActionableWarningsAdvisory, publishActionableWarningsReport, writeDeferredActionableWarningsReport, } from "./actionable-warnings.js";
|
|
4
4
|
import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
|
|
5
5
|
import { appendCodeQualityWarningsHistory, buildCodeQualityWarningsReport, formatCodeQualityWarningsAdvisory, writeCodeQualityWarningsReport, } from "./code-quality-warnings.js";
|
|
6
|
-
import { clearGitGuardTestFailure, mergeGitGuardTestFailure, writeGitGuardRecord, } from "./git-guard.js";
|
|
6
|
+
import { clearGitGuardTestFailure, mergeGitGuardTestFailure, resyncGitGuardAfterInlinePolicy, writeGitGuardRecord, } from "./git-guard.js";
|
|
7
7
|
import { cascadeSettleWaitMs } from "./cascade-budget.js";
|
|
8
8
|
import { logCascade } from "./cascade-logger.js";
|
|
9
9
|
import { normalizeMapKey } from "./path-utils.js";
|
|
@@ -13,17 +13,15 @@ import { getKnipIgnorePatterns } from "./file-utils.js";
|
|
|
13
13
|
import { formatCacheAgeLabel } from "./finding-delivery-gate.js";
|
|
14
14
|
import { getFullScanWallClockMs, isWorkspaceSweepActive, runWhenWorkspaceSweepIdle, SWEEP_IDLE_SAFETY_MARGIN_MS, } from "./lsp/workspace-sweep-hold.js";
|
|
15
15
|
import { isTestRoleCollateral } from "./collateral-test-role.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { isSecretWarning, secretLocationKey } from "./secret-findings.js";
|
|
17
|
+
import { govulncheckLane } from "./turn-end/lanes/govulncheck.js";
|
|
18
|
+
import { secretsLane } from "./turn-end/lanes/secrets.js";
|
|
18
19
|
import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, stableFindingKey, } from "./dead-code-client.js";
|
|
19
20
|
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
20
21
|
import { PROJECT_DIAGNOSTICS_CACHE_VERSION, writeProjectDiagnosticsDeltaReport, } from "./project-diagnostics/cache.js";
|
|
21
22
|
import { deadCodeIssueToProjectDiagnostic } from "./project-diagnostics/runner-adapters/dead-code.js";
|
|
22
|
-
import {
|
|
23
|
-
import { govulncheckFindingToProjectDiagnostic } from "./project-diagnostics/runner-adapters/govulncheck.js";
|
|
24
|
-
import { trivyFindingToProjectDiagnostic, trivySecretFindingToProjectDiagnostic, } from "./project-diagnostics/runner-adapters/trivy.js";
|
|
23
|
+
import { trivyFindingToProjectDiagnostic } from "./project-diagnostics/runner-adapters/trivy.js";
|
|
25
24
|
import { knipIssuesToProjectDiagnostics } from "./project-diagnostics/runner-adapters/knip.js";
|
|
26
|
-
import { applyDispositionsMultiFile } from "./diagnostic-dispositions.js";
|
|
27
25
|
import { logLatency } from "./latency-logger.js";
|
|
28
26
|
import { getLspBudgetIdleTimeoutMs, shouldShortenLspIdleTimeout, } from "./lsp-budget.js";
|
|
29
27
|
import { updateHeartbeat } from "./instance-registry.js";
|
|
@@ -42,8 +40,9 @@ import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
|
|
|
42
40
|
import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
|
|
43
41
|
import { runPersistentReverify, } from "./persistent-reverify.js";
|
|
44
42
|
import { cascadeCarrySuffix } from "./cascade-format.js";
|
|
45
|
-
import {
|
|
43
|
+
import { applyPushedFindingPolicy, filterFindingsByDisposition, } from "./dispatch/finding-policy.js";
|
|
46
44
|
import { detectFileRole } from "./file-role.js";
|
|
45
|
+
import { applyInlineBlockerPolicy, summarizeInlineBlockerPolicy, } from "./inline-blocker-dispositions.js";
|
|
47
46
|
import { drainPendingRunnerFindings, dropStaleRunnerFindings, pendingRunnerFindingsSize, } from "./dispatch/pending-runner-findings.js";
|
|
48
47
|
// #1631 review V2: moved to its own leaf module so a low-level store
|
|
49
48
|
// (widget-state.ts) can use the marker without importing this orchestrator —
|
|
@@ -283,40 +282,6 @@ export async function runTestTargetsBounded(args) {
|
|
|
283
282
|
stopReason,
|
|
284
283
|
};
|
|
285
284
|
}
|
|
286
|
-
/**
|
|
287
|
-
* #1617: turn_end reads gitleaks/govulncheck/trivy straight from their
|
|
288
|
-
* session-scan caches and formats them into advisory/blocker text — a
|
|
289
|
-
* reporting lane parallel to (and, before this fix, entirely bypassing)
|
|
290
|
-
* `dispatcher.ts:924`'s `applyDispositions` filter. An agent-marked
|
|
291
|
-
* false-positive/won't-fix on one of these findings never suppressed it
|
|
292
|
-
* here, so it re-reported on every turn.
|
|
293
|
-
*
|
|
294
|
-
* Filters `findings` through the SAME anchor derivation the dispatch path
|
|
295
|
-
* and `lens_diagnostics mode=full` use (`applyDispositionsMultiFile` in
|
|
296
|
-
* `diagnostic-dispositions.ts`), keyed off each lane's own canonical
|
|
297
|
-
* `ProjectDiagnostic` adapter (`toDiagnostic`) — the exact tool/rule/message
|
|
298
|
-
* identity `lens_diagnostics` already surfaces and `lens_diagnostic_mark`
|
|
299
|
-
* already anchors a mark against, not a second, cloned identity that would
|
|
300
|
-
* silently diverge from what the agent actually marked.
|
|
301
|
-
*
|
|
302
|
-
* Returns the surviving findings plus how many were dropped, so a caller can
|
|
303
|
-
* still surface a "suppressed by disposition: N" trace (the #1616
|
|
304
|
-
* suppressed-bucket rule — a security finding must never vanish with no
|
|
305
|
-
* trace, even when the disposition that dropped it is working as intended).
|
|
306
|
-
*/
|
|
307
|
-
function filterFindingsByDisposition(findings, cwd, toDiagnostic) {
|
|
308
|
-
if (findings.length === 0)
|
|
309
|
-
return { kept: findings, suppressed: 0 };
|
|
310
|
-
const candidates = findings.map((finding) => ({
|
|
311
|
-
finding,
|
|
312
|
-
diagnostic: toDiagnostic(finding),
|
|
313
|
-
}));
|
|
314
|
-
const survivors = new Set(applyDispositionsMultiFile(candidates.map((c) => c.diagnostic), cwd, (d) => d.filePath));
|
|
315
|
-
const kept = candidates
|
|
316
|
-
.filter((c) => survivors.has(c.diagnostic))
|
|
317
|
-
.map((c) => c.finding);
|
|
318
|
-
return { kept, suppressed: findings.length - kept.length };
|
|
319
|
-
}
|
|
320
285
|
/**
|
|
321
286
|
* Would writing `next` over `prev` throw away a good scan for a failed one?
|
|
322
287
|
*
|
|
@@ -746,9 +711,21 @@ export async function handleTurnEnd(deps) {
|
|
|
746
711
|
* branch below.
|
|
747
712
|
*/
|
|
748
713
|
const pendingDependencyDriftDeliveries = [];
|
|
749
|
-
|
|
714
|
+
/** #3246: one bounded record per TURN for the policy pass, never per finding. */
|
|
715
|
+
const inlinePolicyEntries = [];
|
|
716
|
+
/**
|
|
717
|
+
* #3248: files whose EVERY blocker this pass suppressed — the post-policy
|
|
718
|
+
* survivor set, emptied per file. The commit gate is recomputed from it
|
|
719
|
+
* below, once, after the loop.
|
|
720
|
+
*/
|
|
721
|
+
const policySuppressedPaths = [];
|
|
722
|
+
const inlinePolicyStart = Date.now();
|
|
723
|
+
for (const record of unresolvedBlockers) {
|
|
724
|
+
const { filePath: bPath, summary, stale, staleReason } = record;
|
|
750
725
|
const displayPath = toRunnerDisplayPath(cwd, bPath);
|
|
726
|
+
const tally = { displayPath, sources: record.sources };
|
|
751
727
|
if (stale) {
|
|
728
|
+
inlinePolicyEntries.push({ ...tally, stale: true });
|
|
752
729
|
// #1631: demoted — out of the authoritative blocker channel and into the
|
|
753
730
|
// advisory channel with a stale marker, so the agent is told to re-run
|
|
754
731
|
// rather than pressured by a verdict that may already be resolved.
|
|
@@ -812,10 +789,61 @@ export async function handleTurnEnd(deps) {
|
|
|
812
789
|
(retirementNote ? `\n${retirementNote}` : ""));
|
|
813
790
|
}
|
|
814
791
|
else {
|
|
792
|
+
// #3246: the agent may have marked one of these blockers
|
|
793
|
+
// `false-positive` AFTER the record was written, via
|
|
794
|
+
// `lens_diagnostic_mark` — which every other findings surface honors.
|
|
795
|
+
// Re-derive the body from the record's structured diagnostics through
|
|
796
|
+
// the shared `dispatch/finding-policy.ts` stack against the file's
|
|
797
|
+
// CURRENT bytes, exactly as the late-auxiliary lane below does.
|
|
798
|
+
const policy = applyInlineBlockerPolicy(record, cwd);
|
|
799
|
+
inlinePolicyEntries.push({ ...tally, stale: false, outcome: policy });
|
|
800
|
+
if (policy.body === undefined) {
|
|
801
|
+
// Every blocker on this file was suppressed. This is a PUSH
|
|
802
|
+
// surface: silence after a mark is the mark working, not a clean
|
|
803
|
+
// verdict, so the count rides the bounded per-turn record below
|
|
804
|
+
// instead of announcing the suppression on every later turn.
|
|
805
|
+
policySuppressedPaths.push(bPath);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
// #1616 suppressed-bucket rule: a delivery that still has something to
|
|
809
|
+
// say states what it dropped, once per delivery.
|
|
810
|
+
const suppressedNote = policy.suppressed > 0
|
|
811
|
+
? ` (suppressed by disposition: ${policy.suppressed} finding(s))`
|
|
812
|
+
: "";
|
|
815
813
|
// @delivery-surface: runtime-turn:unresolved-inline-blocker
|
|
816
|
-
blockerParts.push(`Unresolved from this turn — ${displayPath}:\n${
|
|
814
|
+
blockerParts.push(`Unresolved from this turn — ${displayPath}${suppressedNote}:\n${policy.body}`);
|
|
817
815
|
}
|
|
818
816
|
}
|
|
817
|
+
// #3248: the post-policy survivor set per file is FINAL here. The commit
|
|
818
|
+
// gate reads a latch (`gitGuardHasBlockers`) before it ever reads the
|
|
819
|
+
// persisted record, and the policy wrote neither — so a file whose every
|
|
820
|
+
// blocker was just suppressed kept blocking `git commit` while the banner
|
|
821
|
+
// above said nothing about it. Recompute the latch from this set; the
|
|
822
|
+
// persisted record is rewritten or cleared further down by the writer that
|
|
823
|
+
// already owns it (`:4287` / `:4379`), from the same survivor set, and that
|
|
824
|
+
// clear is gated on the latch recomputed here.
|
|
825
|
+
const guardLatchBefore = runtime.gitGuardHasBlockers;
|
|
826
|
+
resyncGitGuardAfterInlinePolicy({
|
|
827
|
+
runtime,
|
|
828
|
+
suppressedFilePaths: policySuppressedPaths,
|
|
829
|
+
});
|
|
830
|
+
if (inlinePolicyEntries.length > 0) {
|
|
831
|
+
logLatency({
|
|
832
|
+
type: "phase",
|
|
833
|
+
toolName: "turn_end",
|
|
834
|
+
filePath: cwd,
|
|
835
|
+
phase: "inline_blocker_policy",
|
|
836
|
+
durationMs: Date.now() - inlinePolicyStart,
|
|
837
|
+
metadata: {
|
|
838
|
+
...summarizeInlineBlockerPolicy(inlinePolicyEntries),
|
|
839
|
+
// #3248: one row per TURN for the gate outcome, never per finding
|
|
840
|
+
// — the flip is the event a reader needs to explain why a commit
|
|
841
|
+
// that was blocked is now allowed.
|
|
842
|
+
guardFilesSuppressed: policySuppressedPaths.length,
|
|
843
|
+
guardLatchCleared: guardLatchBefore && !runtime.gitGuardHasBlockers,
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
}
|
|
819
847
|
// Drain the deferred cascade computes kicked off this turn (#450). They ran
|
|
820
848
|
// concurrently off the write hot path; wait a bounded time for them here so
|
|
821
849
|
// their runs are available to the merge below. A compute still in flight at
|
|
@@ -1289,6 +1317,9 @@ export async function handleTurnEnd(deps) {
|
|
|
1289
1317
|
totalIssues: knipResult.issues.length,
|
|
1290
1318
|
newIssues: 0,
|
|
1291
1319
|
blockerIssues: 0,
|
|
1320
|
+
// #3248: bounded per-turn, on the row this lane already writes —
|
|
1321
|
+
// never one record per finding.
|
|
1322
|
+
dispositionSuppressed: 0,
|
|
1292
1323
|
...(!knipResult.success && { reason: knipResult.summary }),
|
|
1293
1324
|
...(knipResult.failureKind && { failureKind: knipResult.failureKind }),
|
|
1294
1325
|
...(knipWouldPoison && { cacheKept: true }),
|
|
@@ -1312,7 +1343,30 @@ export async function handleTurnEnd(deps) {
|
|
|
1312
1343
|
projectDiagnosticsDelta.push(...knipIssuesToProjectDiagnostics(cwd, newIssues));
|
|
1313
1344
|
projectDiagnosticsSources.add("knip");
|
|
1314
1345
|
}
|
|
1315
|
-
|
|
1346
|
+
// #3248: what the agent READS goes through the same stored-
|
|
1347
|
+
// disposition filter every other findings surface applies, keyed off
|
|
1348
|
+
// knip's OWN `ProjectDiagnostic` adapter — the identity
|
|
1349
|
+
// `lens_diagnostics` surfaces and `lens_diagnostic_mark` anchors
|
|
1350
|
+
// against — so a marked finding stops re-reporting here. The
|
|
1351
|
+
// `projectDiagnosticsDelta` push above deliberately keeps the
|
|
1352
|
+
// UNFILTERED set: that record is what the scan found, and its reader
|
|
1353
|
+
// (`lens_diagnostics`) applies dispositions on read, so filtering it
|
|
1354
|
+
// here would apply the same policy twice on one lane.
|
|
1355
|
+
// Paired through `flatMap` rather than indexing the adapter's array:
|
|
1356
|
+
// `knipIssuesToProjectDiagnostics` is a straight `issues.map(...)`
|
|
1357
|
+
// (one diagnostic per issue, never empty), so this keeps the pairing
|
|
1358
|
+
// total while staying honest under `noUncheckedIndexedAccess`.
|
|
1359
|
+
const knipPaired = newIssues.flatMap((issue) => knipIssuesToProjectDiagnostics(cwd, [issue]).map((diagnostic) => ({
|
|
1360
|
+
issue,
|
|
1361
|
+
diagnostic,
|
|
1362
|
+
})));
|
|
1363
|
+
const knipFiltered = filterFindingsByDisposition(knipPaired, cwd, (pair) => pair.diagnostic);
|
|
1364
|
+
const knipDeliverable = {
|
|
1365
|
+
kept: knipFiltered.kept.map((pair) => pair.issue),
|
|
1366
|
+
suppressed: knipFiltered.suppressed,
|
|
1367
|
+
};
|
|
1368
|
+
knipMeta.dispositionSuppressed = knipDeliverable.suppressed;
|
|
1369
|
+
const blockerIssues = knipDeliverable.kept.filter((i) => i.type === "unlisted" || i.type === "bin");
|
|
1316
1370
|
knipMeta.blockerIssues = blockerIssues.length;
|
|
1317
1371
|
if (blockerIssues.length > 0) {
|
|
1318
1372
|
let report = "🔴 New unresolved imports/deps in modified code (Knip):\n";
|
|
@@ -1339,7 +1393,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1339
1393
|
// drown the blockers and burn context every turn); it's available
|
|
1340
1394
|
// on demand via lens_diagnostics. The delta also feeds the session-slop
|
|
1341
1395
|
// record (`projectDiagnosticsDelta`) above.
|
|
1342
|
-
const unusedExportDelta =
|
|
1396
|
+
const unusedExportDelta = knipDeliverable.kept.filter((i) => i.type === "export" || i.type === "enumMember");
|
|
1343
1397
|
if (unusedExportDelta.length > 0) {
|
|
1344
1398
|
let report = "⚠️ Newly unused exports in files you edited — check if callers need updating (Knip):\n";
|
|
1345
1399
|
for (const issue of unusedExportDelta.slice(0, 5)) {
|
|
@@ -1387,6 +1441,8 @@ export async function handleTurnEnd(deps) {
|
|
|
1387
1441
|
let modifiedSet = null;
|
|
1388
1442
|
const modifiedFiles = () => (modifiedSet ??= new Set(files.map((f) => resolveRunnerPath(cwd, f))));
|
|
1389
1443
|
let newIssueTotal = 0;
|
|
1444
|
+
/** #3248: bounded per-turn on this lane's own row, never per finding. */
|
|
1445
|
+
let deadCodeDispositionSuppressed = 0;
|
|
1390
1446
|
const reasons = [];
|
|
1391
1447
|
// A malformed client or deps object must never abort turn_end. Before the
|
|
1392
1448
|
// per-turn delta this block only read a cache; now it iterates and awaits,
|
|
@@ -1472,8 +1528,21 @@ export async function handleTurnEnd(deps) {
|
|
|
1472
1528
|
newIssueTotal += newIssues.length;
|
|
1473
1529
|
projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
|
|
1474
1530
|
projectDiagnosticsSources.add("dead-code");
|
|
1531
|
+
// #3248: the rendered advisory takes the stored-disposition
|
|
1532
|
+
// filter, keyed off this lane's OWN adapter. The delta record
|
|
1533
|
+
// above keeps the unfiltered set — its reader applies the policy
|
|
1534
|
+
// on read, so filtering both would double-apply on one lane.
|
|
1535
|
+
const deadCodeDeliverable = filterFindingsByDisposition(newIssues, cwd, (issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language));
|
|
1536
|
+
deadCodeDispositionSuppressed += deadCodeDeliverable.suppressed;
|
|
1537
|
+
// Every finding on this scan was marked: a PUSH surface stays
|
|
1538
|
+
// silent rather than re-announcing that the mark is working; the
|
|
1539
|
+
// count rides this lane's bounded per-turn row.
|
|
1540
|
+
if (deadCodeDeliverable.kept.length === 0) {
|
|
1541
|
+
reasons.push(`${client.id}:all_disposed`);
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1475
1544
|
// @delivery-surface: runtime-turn:dead-code-advisory
|
|
1476
|
-
advisoryParts.push(formatDeadCodeDelta(
|
|
1545
|
+
advisoryParts.push(formatDeadCodeDelta(deadCodeDeliverable.kept, result.language));
|
|
1477
1546
|
}
|
|
1478
1547
|
catch (err) {
|
|
1479
1548
|
dbg(`turn_end: dead-code(${client.id}) failed: ${err}`);
|
|
@@ -1486,6 +1555,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1486
1555
|
reasons.push("block_threw");
|
|
1487
1556
|
}
|
|
1488
1557
|
deadCodeMeta.newIssues = newIssueTotal;
|
|
1558
|
+
deadCodeMeta.dispositionSuppressed = deadCodeDispositionSuppressed;
|
|
1489
1559
|
if (reasons.length > 0)
|
|
1490
1560
|
deadCodeMeta.reason = reasons.join(",");
|
|
1491
1561
|
}
|
|
@@ -1513,212 +1583,167 @@ export async function handleTurnEnd(deps) {
|
|
|
1513
1583
|
dispositionSuppressedByLane[lane] =
|
|
1514
1584
|
(dispositionSuppressedByLane[lane] ?? 0) + count;
|
|
1515
1585
|
}
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1586
|
+
// #1892: ONE read per scanner store per turn_end. The inline lanes got this
|
|
1587
|
+
// for free by reading each cache into a local; now that a lane reads its own
|
|
1588
|
+
// stores (`TurnEndLaneContext.readScannerCache`), the composer and the lanes
|
|
1589
|
+
// must still share one envelope per store — two reads of one store inside
|
|
1590
|
+
// one delivery is the parallel-store shape this umbrella exists to kill, and
|
|
1591
|
+
// the cache's TTL boundary can fall between them, so the secrets tier and
|
|
1592
|
+
// the CVE tier would disagree about the trivy store they both read.
|
|
1593
|
+
//
|
|
1594
|
+
// #3274: the memo holds the PROMISE of each store's envelope, not the
|
|
1595
|
+
// envelope. The read itself moved to `CacheManager.readCacheAsync`, which
|
|
1596
|
+
// suspends on `fs.promises`, so `bounded()` finally has something to bound:
|
|
1597
|
+
// the sync `readCache` completed during ARGUMENT EVALUATION, before
|
|
1598
|
+
// `bounded()` was ever handed a promise, and a wrapper around it registered
|
|
1599
|
+
// a turn_end budget that could never be spent (probed on #3274 with an
|
|
1600
|
+
// already-aborted signal and `ms: 0` — bounded returned undefined and the
|
|
1601
|
+
// read had already parsed). Memoizing the promise is also what keeps the
|
|
1602
|
+
// one-envelope-per-store rule under concurrency: two lanes that await the
|
|
1603
|
+
// same store share ONE read and ONE TTL boundary even when neither has
|
|
1604
|
+
// settled yet, which a value memo could not do.
|
|
1605
|
+
//
|
|
1606
|
+
// An abandoned read yields null, never a throw and never a late envelope:
|
|
1607
|
+
// `bounded()` resolves `undefined` on the budget or the hook's signal and
|
|
1608
|
+
// records ONE `hook-await-exceeded` row per (hook, label) for the deadline
|
|
1609
|
+
// arm, and the memoized promise is already settled by the time the
|
|
1610
|
+
// abandoned read resolves, so nothing mutates after the delivery composed.
|
|
1611
|
+
// A lane that receives null behaves exactly as it does for a cold cache.
|
|
1612
|
+
const scannerCacheReads = new Map();
|
|
1613
|
+
// Stores this delivery composed WITHOUT because a bound fired, in read
|
|
1614
|
+
// order. `bounded()` records the AWAIT's own row (`hook-await-exceeded`),
|
|
1615
|
+
// but only for the deadline arm and only about the await; the agent-facing
|
|
1616
|
+
// consequence — the secrets tier went out with no gitleaks store behind it,
|
|
1617
|
+
// which looks exactly like a clean scan (AGENTS.md defect shape 10) — has no
|
|
1618
|
+
// record otherwise, on either arm. One row per DELIVERY, below, never one
|
|
1619
|
+
// per store and never on a healthy turn.
|
|
1620
|
+
const scannerStoresUnread = [];
|
|
1621
|
+
function readScannerCache(scanner) {
|
|
1622
|
+
let pending = scannerCacheReads.get(scanner);
|
|
1623
|
+
if (pending === undefined) {
|
|
1624
|
+
// An already-aborted turn does not start the read at all. `bounded()`
|
|
1625
|
+
// abandons the await but cannot cancel work already dispatched, so
|
|
1626
|
+
// without this the cancelled turn still pays six file reads whose
|
|
1627
|
+
// result nothing can use. Checked AFTER the memo lookup: a signal that
|
|
1628
|
+
// fires mid-delivery must not give the second lane a different answer
|
|
1629
|
+
// from the first (that is the TTL-boundary split this memo exists for).
|
|
1630
|
+
// It records nothing, on purpose: an already-cancelled turn is Escape,
|
|
1631
|
+
// which `bounded()` also keeps off the ledger — the row below is for a
|
|
1632
|
+
// delivery that went out degraded, not one the user stopped.
|
|
1633
|
+
pending = deps.signal?.aborted
|
|
1634
|
+
? Promise.resolve(null)
|
|
1635
|
+
: bounded(cacheManager.readCacheAsync(scanner, cwd), {
|
|
1636
|
+
ms: HOOK_WALL_BUDGET_MS.turn_end,
|
|
1637
|
+
signal: deps.signal,
|
|
1638
|
+
hook: "turn_end",
|
|
1639
|
+
label: `readScannerCache:${scanner}`,
|
|
1640
|
+
}).then((entry) => {
|
|
1641
|
+
if (entry === undefined)
|
|
1642
|
+
scannerStoresUnread.push(scanner);
|
|
1643
|
+
return entry ?? null;
|
|
1644
|
+
});
|
|
1645
|
+
scannerCacheReads.set(scanner, pending);
|
|
1646
|
+
}
|
|
1647
|
+
return pending;
|
|
1648
|
+
}
|
|
1649
|
+
const laneCtx = {
|
|
1531
1650
|
cwd,
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1651
|
+
signal: deps.signal,
|
|
1652
|
+
readScannerCache,
|
|
1653
|
+
peekActionableWarnings: () => runtime.peekActionableWarnings(),
|
|
1654
|
+
};
|
|
1655
|
+
// govulncheck — the session_start-cached Go CVE store, delivered as ONE
|
|
1656
|
+
// advisory tier by the govulncheck LANE
|
|
1657
|
+
// (`clients/turn-end/lanes/govulncheck.ts`), which owns every rule this block
|
|
1658
|
+
// used to state inline: the `onMissing: "demote"` freshness declaration and
|
|
1659
|
+
// its first-filename-frame `citedPath` (#1622 H1), the disposition anchor
|
|
1660
|
+
// over BOTH freshness arms (#1694 F1), the stale-line withholding and marker,
|
|
1661
|
+
// the module/package fallback, the fix hint and the display cap. No per-turn
|
|
1662
|
+
// re-run in this slice; the cache refreshes at next session_start. Like every
|
|
1663
|
+
// lane it does NOT gate itself — the freshness pass below is shared.
|
|
1664
|
+
const govSources = await govulncheckLane.collect(laneCtx);
|
|
1665
|
+
const trivyCacheEntry = await readScannerCache("trivy");
|
|
1666
|
+
// The secrets lane (`clients/turn-end/lanes/secrets.ts`) reads the gitleaks
|
|
1667
|
+
// and trivy stores, classifies, and states the freshness policy its rows
|
|
1668
|
+
// need; every rendering and disposition rule for the two secrets tiers lives
|
|
1669
|
+
// there. It does NOT gate itself either.
|
|
1670
|
+
const secretsSources = await secretsLane.collect(laneCtx);
|
|
1671
|
+
// #1892: ONE freshness pass for the three cached scanner stores that cite a
|
|
1672
|
+
// file. Each store keeps its own `scannedAt` and its own `onMissing` — the
|
|
1673
|
+
// gate carries source identity, so gitleaks' older scan cannot demote a
|
|
1674
|
+
// trivy secret its newer scan covers, and govulncheck's `demote` verdict for
|
|
1675
|
+
// a deleted path cannot reach gitleaks, which must drop it. What IS shared
|
|
1676
|
+
// is the filesystem: one `statSync` per unique cited path per delivery, one
|
|
1677
|
+
// stat budget, and one bounded drop/demote record instead of up to six.
|
|
1678
|
+
//
|
|
1679
|
+
// #1461 slice 1 (#1460) on gitleaks: the cache is TTL-only, so a finding for
|
|
1680
|
+
// a file deleted after the scan was served as a 🔴 blocker for the rest of
|
|
1681
|
+
// the 30-minute window — 119 of 126 findings in pi-lens's own cache. This
|
|
1682
|
+
// read is the single agent-facing consumer of that store (session_start's
|
|
1683
|
+
// read only decides whether to re-scan; the project-diagnostics path
|
|
1684
|
+
// re-scans fresh and reconciles at load), so the drop belongs here, before
|
|
1685
|
+
// the findings enter the shared secret pipeline. #1622 extends the gate from
|
|
1686
|
+
// existence to freshness, and adds trivy secrets — the sibling store with
|
|
1687
|
+
// the identical shape. A cited file edited after the scan keeps its finding
|
|
1688
|
+
// but loses its line number: the credential may still be there, just not
|
|
1689
|
+
// where the snapshot says. Dropping instead would let any edit — malicious
|
|
1690
|
+
// or accidental — mute a real secret.
|
|
1691
|
+
if (scannerStoresUnread.length > 0) {
|
|
1692
|
+
logLatency({
|
|
1693
|
+
type: "phase",
|
|
1694
|
+
toolName: "turn_end",
|
|
1695
|
+
filePath: cwd,
|
|
1696
|
+
phase: "scanner_cache_read_abandoned",
|
|
1697
|
+
durationMs: 0,
|
|
1698
|
+
metadata: {
|
|
1699
|
+
stores: scannerStoresUnread.join("+"),
|
|
1700
|
+
aborted: deps.signal?.aborted === true,
|
|
1701
|
+
},
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
const scannerGates = gateFindingsByPathFreshness({
|
|
1705
|
+
cwd,
|
|
1706
|
+
sources: {
|
|
1707
|
+
...govSources,
|
|
1708
|
+
...secretsSources,
|
|
1709
|
+
},
|
|
1535
1710
|
});
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
// never fall back to a raw-cache-length check (that would print the header
|
|
1543
|
-
// with zero rows beneath it whenever either filter empties the list).
|
|
1544
|
-
const govFiltered = filterFindingsByDisposition([...govGate.live, ...govGate.stale], cwd, (f) => govulncheckFindingToProjectDiagnostic(cwd, f));
|
|
1545
|
-
recordDispositionSuppressed("govulncheck", govFiltered.suppressed);
|
|
1546
|
-
const govFindings = govFiltered.kept;
|
|
1547
|
-
if (govFindings.length) {
|
|
1548
|
-
const findings = govFindings.slice(0, 5);
|
|
1549
|
-
let report = "🛡️ Go CVEs reachable from this code (govulncheck) — upgrade where possible:\n";
|
|
1550
|
-
for (const f of findings) {
|
|
1551
|
-
const callSite = f.trace.find((t) => t.filename);
|
|
1552
|
-
const stale = govStale.has(f);
|
|
1553
|
-
const where = callSite?.filename
|
|
1554
|
-
? `${toRunnerDisplayPath(cwd, callSite.filename)}${!stale && callSite.line ? `:${callSite.line}` : ""}${stale ? ` ${STALE_LINE_MARKER}` : ""}`
|
|
1555
|
-
: (f.module ?? f.packageName ?? "(module)");
|
|
1556
|
-
const fix = f.fixedVersion
|
|
1557
|
-
? ` — upgrade to ${f.fixedVersion} or later`
|
|
1558
|
-
: " — no fix yet, track upstream";
|
|
1559
|
-
report += ` ${f.osv} (${where})${fix}\n`;
|
|
1560
|
-
}
|
|
1561
|
-
if (govFindings.length > findings.length) {
|
|
1562
|
-
report += ` … and ${govFindings.length - findings.length} more\n`;
|
|
1563
|
-
}
|
|
1564
|
-
// @delivery-surface: runtime-turn:govulncheck-advisory
|
|
1565
|
-
advisoryParts.push(report);
|
|
1711
|
+
const govGate = scannerGates.govulncheck;
|
|
1712
|
+
const gitleaksGate = scannerGates.gitleaks;
|
|
1713
|
+
const trivySecretsGate = scannerGates["trivy-secrets"];
|
|
1714
|
+
const govDelivery = govulncheckLane.render(govulncheckLane.gate({ govulncheck: govGate }, laneCtx), laneCtx);
|
|
1715
|
+
for (const [store, count] of Object.entries(govDelivery.dispositionSuppressed ?? {})) {
|
|
1716
|
+
recordDispositionSuppressed(store, count);
|
|
1566
1717
|
}
|
|
1567
|
-
|
|
1718
|
+
// @delivery-surface: runtime-turn:govulncheck-advisory
|
|
1719
|
+
advisoryParts.push(...(govDelivery.advisoryParts ?? []));
|
|
1568
1720
|
// Secrets — UNIFIED surfacing (#131 Mode 3). gitleaks, trivy secret, and the
|
|
1569
1721
|
// ast-grep hardcoded-secret rules can each flag the SAME line with different
|
|
1570
1722
|
// rule ids, which the rule-keyed diagnostic dedup can't collapse. Collapse by
|
|
1571
1723
|
// location so a committed/hardcoded secret is reported ONCE (with combined
|
|
1572
1724
|
// provenance) — a blocker, since credentials need rotation before merge.
|
|
1573
|
-
const gitleaksData = cacheManager.readCache("gitleaks", cwd)?.data;
|
|
1574
|
-
const trivySecretsData = trivyCacheEntry?.data;
|
|
1575
|
-
// Gitleaks deliberately scans gitignored local files and nested repositories
|
|
1576
|
-
// so an explicit security audit can still inspect them. The adapter is the
|
|
1577
|
-
// source of truth for whether a finding belongs in a blocking delivery lane;
|
|
1578
|
-
// filter here before freshness handling so demoted findings cannot leak into
|
|
1579
|
-
// either the blocker or stale-secret turn context.
|
|
1580
|
-
const boundedClassification = await bounded(classifyAndFilterFindings(gitleaksData?.findings ?? [], cwd), {
|
|
1581
|
-
ms: HOOK_WALL_BUDGET_MS.turn_end,
|
|
1582
|
-
signal: deps.signal,
|
|
1583
|
-
hook: "turn_end",
|
|
1584
|
-
label: "classifyAndFilterFindings",
|
|
1585
|
-
});
|
|
1586
|
-
const classifiedGitleaksFindings = boundedClassification ?? gitleaksData?.findings ?? [];
|
|
1587
|
-
if (boundedClassification === undefined) {
|
|
1588
|
-
recordDegradationOnce({
|
|
1589
|
-
kind: "gitleaks_classification_timeout",
|
|
1590
|
-
subject: cwd,
|
|
1591
|
-
reason: "gitleaks classification exceeded the turn_end budget; retained raw findings to fail open",
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
const blockingGitleaksFindings = classifiedGitleaksFindings.filter((finding) => gitleaksFindingToProjectDiagnostic(cwd, finding).semantic === "blocking");
|
|
1595
|
-
// #1461 slice 1 (#1460): the gitleaks cache is TTL-only, so a finding for a
|
|
1596
|
-
// file deleted after the scan is still served as a 🔴 blocker for the rest
|
|
1597
|
-
// of the 30-minute window — the live case, and 119 of 126 findings in
|
|
1598
|
-
// pi-lens's own cache. This read is the single agent-facing consumer of that
|
|
1599
|
-
// store (session_start's read only decides whether to re-scan; the
|
|
1600
|
-
// project-diagnostics path re-scans fresh and reconciles at load), so the
|
|
1601
|
-
// drop belongs here, before the findings enter the shared secret pipeline.
|
|
1602
|
-
// #1622 extends that gate from existence to freshness, and adds trivy
|
|
1603
|
-
// secrets — the sibling store with the identical shape. A cited file edited
|
|
1604
|
-
// after the scan keeps its finding but loses its line number: the credential
|
|
1605
|
-
// may still be there, just not where the snapshot says. Dropping instead
|
|
1606
|
-
// would let any edit — malicious or accidental — mute a real secret.
|
|
1607
|
-
const gitleaksGate = gateFindingsByPathFreshness({
|
|
1608
|
-
store: "gitleaks",
|
|
1609
|
-
findings: blockingGitleaksFindings,
|
|
1610
|
-
cwd,
|
|
1611
|
-
scannedAt: gitleaksData?.scannedAt,
|
|
1612
|
-
citedPath: (finding) => finding.file,
|
|
1613
|
-
});
|
|
1614
|
-
const trivySecretsGate = gateFindingsByPathFreshness({
|
|
1615
|
-
store: "trivy-secrets",
|
|
1616
|
-
findings: trivySecretsData?.secrets ?? [],
|
|
1617
|
-
cwd,
|
|
1618
|
-
scannedAt: trivySecretsData?.scannedAt,
|
|
1619
|
-
citedPath: (finding) => finding.file,
|
|
1620
|
-
});
|
|
1621
|
-
// #1617: THE bug this issue exists for — gitleaks findings never passed
|
|
1622
|
-
// through `applyDispositions`, so an agent-marked false-positive/won't-fix
|
|
1623
|
-
// re-reported as a 🔴 STOP blocker on every turn. Filter through the SAME
|
|
1624
|
-
// `gitleaksFindingToProjectDiagnostic` identity `lens_diagnostics
|
|
1625
|
-
// mode=full` surfaces (tool="gitleaks", rule="gitleaks:<ruleId>", the
|
|
1626
|
-
// exact "Potential secret: …" message) so a mark made against what the
|
|
1627
|
-
// agent was shown is honored here too.
|
|
1628
1725
|
//
|
|
1629
|
-
// #
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
// ast-grep secret findings need no filtering here — they already went
|
|
1645
|
-
// through dispatch's applyDispositions before reaching
|
|
1646
|
-
// `peekActionableWarnings()`.
|
|
1647
|
-
const gitleaksLiveFiltered = filterFindingsByDisposition(gitleaksGate.live, cwd, (f) => gitleaksFindingToProjectDiagnostic(cwd, f));
|
|
1648
|
-
const gitleaksStaleFiltered = filterFindingsByDisposition(gitleaksGate.stale, cwd, (f) => gitleaksFindingToProjectDiagnostic(cwd, f));
|
|
1649
|
-
recordDispositionSuppressed("gitleaks", gitleaksLiveFiltered.suppressed + gitleaksStaleFiltered.suppressed);
|
|
1650
|
-
const trivySecretsLiveFiltered = filterFindingsByDisposition(trivySecretsGate.live, cwd, (f) => trivySecretFindingToProjectDiagnostic(cwd, f));
|
|
1651
|
-
const trivySecretsStaleFiltered = filterFindingsByDisposition(trivySecretsGate.stale, cwd, (f) => trivySecretFindingToProjectDiagnostic(cwd, f));
|
|
1652
|
-
recordDispositionSuppressed("trivy-secrets", trivySecretsLiveFiltered.suppressed + trivySecretsStaleFiltered.suppressed);
|
|
1653
|
-
const astSecretWarnings = runtime
|
|
1654
|
-
.peekActionableWarnings()
|
|
1655
|
-
.filter(isSecretWarning);
|
|
1656
|
-
const sessionSecrets = dedupeSecretFindings([
|
|
1657
|
-
...fromGitleaks(gitleaksLiveFiltered.kept),
|
|
1658
|
-
...fromTrivySecrets(trivySecretsLiveFiltered.kept),
|
|
1659
|
-
]);
|
|
1660
|
-
// Demoted secrets are addressed by FILE, never by line — the line is the one
|
|
1661
|
-
// field the edit invalidated. Rule id and source survive it and must be
|
|
1662
|
-
// carried through (review round M1): an agent triages an `aws-access-token`
|
|
1663
|
-
// differently from a low-confidence `generic-api-key`, and cannot do that
|
|
1664
|
-
// from a bare path. Deduped on file+rule+source so a file with twenty stale
|
|
1665
|
-
// hits of one rule is named once.
|
|
1666
|
-
const staleSecretEntries = [
|
|
1667
|
-
...gitleaksStaleFiltered.kept.map((f) => ({
|
|
1668
|
-
file: toRunnerDisplayPath(cwd, f.file),
|
|
1669
|
-
rule: f.ruleId,
|
|
1670
|
-
source: "gitleaks",
|
|
1671
|
-
})),
|
|
1672
|
-
...trivySecretsStaleFiltered.kept.map((f) => ({
|
|
1673
|
-
file: toRunnerDisplayPath(cwd, f.file),
|
|
1674
|
-
rule: f.ruleId,
|
|
1675
|
-
source: "trivy",
|
|
1676
|
-
})),
|
|
1677
|
-
];
|
|
1678
|
-
const staleSecrets = [
|
|
1679
|
-
...new Map(staleSecretEntries.map((e) => [`${e.file}|${e.rule}|${e.source}`, e])).values(),
|
|
1680
|
-
];
|
|
1681
|
-
// Locations already surfaced as session-scan secret blockers — used to enrich
|
|
1682
|
-
// provenance where ast-grep agrees and to suppress the duplicate ast-grep copy
|
|
1683
|
-
// from the actionable-warnings advisory below.
|
|
1684
|
-
const secretBlockedLocations = new Set(sessionSecrets.map((f) => secretLocationKey(f.file, f.line)));
|
|
1685
|
-
if (sessionSecrets.length) {
|
|
1686
|
-
// Fold in ast-grep provenance ONLY where it coincides with a session
|
|
1687
|
-
// secret — don't promote ast-grep-only findings out of their advisory tier.
|
|
1688
|
-
const enriched = dedupeSecretFindings([
|
|
1689
|
-
...sessionSecrets,
|
|
1690
|
-
...fromAstGrepWarnings(astSecretWarnings).filter((a) => secretBlockedLocations.has(secretLocationKey(a.file, a.line))),
|
|
1691
|
-
]);
|
|
1692
|
-
const shown = enriched.slice(0, 5);
|
|
1693
|
-
let report = "🔴 STOP — hardcoded secrets detected. Rotate the credentials and remove them from source:\n";
|
|
1694
|
-
for (const f of shown) {
|
|
1695
|
-
const where = `${toRunnerDisplayPath(cwd, f.file)}:${f.line}`;
|
|
1696
|
-
report += ` ${where} — ${f.rule} [${f.sources.join(" + ")}]${f.description ? `: ${f.description}` : ""}\n`;
|
|
1697
|
-
}
|
|
1698
|
-
if (enriched.length > shown.length) {
|
|
1699
|
-
report += ` … and ${enriched.length - shown.length} more\n`;
|
|
1700
|
-
}
|
|
1701
|
-
// @delivery-surface: runtime-turn:secrets-gitleaks,runtime-turn:secrets-trivy
|
|
1702
|
-
blockerParts.push(report);
|
|
1703
|
-
}
|
|
1704
|
-
if (staleSecrets.length) {
|
|
1705
|
-
// Its OWN tier, never `advisoryParts` (review round M2). The advisory tier
|
|
1706
|
-
// is labelled "no action required this turn", which would sit directly
|
|
1707
|
-
// above copy telling the agent to re-scan — a section that contradicts its
|
|
1708
|
-
// own heading. This preamble is imperative because the action is real: the
|
|
1709
|
-
// finding is unverified, not dismissed.
|
|
1710
|
-
const shown = staleSecrets.slice(0, 5);
|
|
1711
|
-
let report = `🔑 ACTION NEEDED — secrets were flagged in files that changed after the scan. ${STALE_LINE_MARKER}\n` +
|
|
1712
|
-
"The cached line numbers are no longer trustworthy, so they are withheld. Re-run a secrets scan to confirm or clear these:\n";
|
|
1713
|
-
for (const entry of shown) {
|
|
1714
|
-
report += ` ${entry.file} — ${entry.rule} [${entry.source}]\n`;
|
|
1715
|
-
}
|
|
1716
|
-
if (staleSecrets.length > shown.length) {
|
|
1717
|
-
report += ` … and ${staleSecrets.length - shown.length} more\n`;
|
|
1718
|
-
}
|
|
1719
|
-
// @delivery-surface: runtime-turn:stale-secrets-tier
|
|
1720
|
-
staleSecretParts.push(report);
|
|
1726
|
+
// #1892: both tiers are rendered by the secrets LANE
|
|
1727
|
+
// (`clients/turn-end/lanes/secrets.ts`), which owns every rule this block
|
|
1728
|
+
// used to state inline — the two stores' disposition anchors over BOTH
|
|
1729
|
+
// freshness arms (#1617/#1625/#1628), the location dedupe and ast-grep
|
|
1730
|
+
// provenance enrichment, the demoted tier's file+rule+source identity
|
|
1731
|
+
// (#1622 M1) and its own-tier placement (#1622 M2). The composer keeps only
|
|
1732
|
+
// what is not one lane's rule: the gated arms it hands over, the order the
|
|
1733
|
+
// tiers are pushed in, and the per-lane suppression counts that fold into
|
|
1734
|
+
// the one notice below.
|
|
1735
|
+
const secretsDelivery = secretsLane.render(secretsLane.gate({
|
|
1736
|
+
gitleaks: gitleaksGate,
|
|
1737
|
+
"trivy-secrets": trivySecretsGate,
|
|
1738
|
+
}, laneCtx), laneCtx);
|
|
1739
|
+
for (const [store, count] of Object.entries(secretsDelivery.dispositionSuppressed ?? {})) {
|
|
1740
|
+
recordDispositionSuppressed(store, count);
|
|
1721
1741
|
}
|
|
1742
|
+
const secretBlockedLocations = secretsDelivery.deliveredLocationKeys ?? new Set();
|
|
1743
|
+
// @delivery-surface: runtime-turn:secrets-gitleaks,runtime-turn:secrets-trivy
|
|
1744
|
+
blockerParts.push(...(secretsDelivery.blockerParts ?? []));
|
|
1745
|
+
// @delivery-surface: runtime-turn:stale-secrets-tier
|
|
1746
|
+
staleSecretParts.push(...(secretsDelivery.staleSecretParts ?? []));
|
|
1722
1747
|
// trivy — surface session_start-cached dependency CVEs (#131, Phase 1).
|
|
1723
1748
|
// CRITICAL is a blocker (a known-exploitable CVE in a shipped dep is real
|
|
1724
1749
|
// production risk); HIGH/MEDIUM/LOW are advisory. The agent gets the upgrade
|
|
@@ -1828,11 +1853,6 @@ export async function handleTurnEnd(deps) {
|
|
|
1828
1853
|
const depResult = depResults.get(absPath);
|
|
1829
1854
|
if (!depResult)
|
|
1830
1855
|
continue;
|
|
1831
|
-
if (depResult.localSkips && depResult.localSkips > 0) {
|
|
1832
|
-
// Not silent: a skipped LOCAL import means madge couldn't resolve
|
|
1833
|
-
// it into the graph, so a cycle through it would be missed.
|
|
1834
|
-
dbg(`turn_end: madge skipped ${depResult.localSkips} local file(s) resolving ${file} — possible silent cycle-miss`);
|
|
1835
|
-
}
|
|
1836
1856
|
if (depResult.hasCircular && depResult.circular.length > 0) {
|
|
1837
1857
|
// Whole-project circular deps are surfaced in lens_diagnostics via the
|
|
1838
1858
|
// session-start `madge` cache + extractor; this per-file turn-end pass
|
|
@@ -2660,10 +2680,13 @@ export async function handleTurnEnd(deps) {
|
|
|
2660
2680
|
"the affected files may have unreported callers.");
|
|
2661
2681
|
}
|
|
2662
2682
|
else {
|
|
2683
|
+
const callGraphStart = Date.now();
|
|
2663
2684
|
try {
|
|
2664
2685
|
const { impact, formatImpact, parseSymbolKey } = await import("./call-graph.js");
|
|
2665
2686
|
const { callGraphImpactToProjectDiagnostics } = await import("./project-diagnostics/runner-adapters/call-graph-impact.js");
|
|
2666
2687
|
const impactLines = [];
|
|
2688
|
+
/** #3248: bounded per-turn, never per finding. */
|
|
2689
|
+
let callGraphDispositionSuppressed = 0;
|
|
2667
2690
|
const impactFindings = [];
|
|
2668
2691
|
for (const filePath of files.slice(0, 5)) {
|
|
2669
2692
|
// Turn-state files may be cwd-relative while graph keys are absolute,
|
|
@@ -2691,7 +2714,33 @@ export async function handleTurnEnd(deps) {
|
|
|
2691
2714
|
});
|
|
2692
2715
|
if (results.length > 0) {
|
|
2693
2716
|
impactFindings.push({ calleeKey, results });
|
|
2694
|
-
|
|
2717
|
+
// #3248: the rendered line takes the stored-disposition
|
|
2718
|
+
// filter. A result is mapped to its diagnostic by the
|
|
2719
|
+
// lane's OWN adapter, ONE result at a time, so the
|
|
2720
|
+
// identity can never diverge from the one
|
|
2721
|
+
// `lens_diagnostics` surfaces and a mark anchors against
|
|
2722
|
+
// — and a result the adapter does not map (the Review
|
|
2723
|
+
// tier, an unattributable key, a test-role caller) is
|
|
2724
|
+
// kept unconditionally: it is not markable, so nothing
|
|
2725
|
+
// may suppress it. Re-rendered with `formatImpact`, the
|
|
2726
|
+
// same renderer, never a second one.
|
|
2727
|
+
// `flatMap` rather than a push-under-an-`if`: an entry the
|
|
2728
|
+
// adapter does not map simply yields nothing, so the array
|
|
2729
|
+
// is well-typed with no branch whose removal changes no
|
|
2730
|
+
// behaviour (there is nothing here to mutate).
|
|
2731
|
+
const markable = results.flatMap((result) => callGraphImpactToProjectDiagnostics(cwd, [
|
|
2732
|
+
{ calleeKey, results: [result] },
|
|
2733
|
+
]).map((diagnostic) => ({ result, diagnostic })));
|
|
2734
|
+
const filtered = filterFindingsByDisposition(markable, cwd, (entry) => entry.diagnostic);
|
|
2735
|
+
callGraphDispositionSuppressed += filtered.suppressed;
|
|
2736
|
+
const keptEntries = new Set(filtered.kept);
|
|
2737
|
+
const dropped = new Set(markable
|
|
2738
|
+
.filter((entry) => !keptEntries.has(entry))
|
|
2739
|
+
.map((entry) => entry.result));
|
|
2740
|
+
const survivors = dropped.size === 0
|
|
2741
|
+
? results
|
|
2742
|
+
: results.filter((r) => !dropped.has(r));
|
|
2743
|
+
const summary = survivors.length > 0 ? formatImpact(survivors, cwd) : "";
|
|
2695
2744
|
if (summary)
|
|
2696
2745
|
impactLines.push(` ${parseSymbolKey(calleeKey).symbolName ?? calleeKey}: ${summary}`);
|
|
2697
2746
|
}
|
|
@@ -2708,6 +2757,23 @@ export async function handleTurnEnd(deps) {
|
|
|
2708
2757
|
projectDiagnosticsSources.add("call-graph");
|
|
2709
2758
|
}
|
|
2710
2759
|
}
|
|
2760
|
+
// #3248: this lane wrote nothing per turn, so its delivery decision
|
|
2761
|
+
// — including how many callers a stored disposition dropped — was
|
|
2762
|
+
// unobservable. One bounded row per turn, only when the lane ran.
|
|
2763
|
+
if (impactFindings.length > 0 || callGraphDispositionSuppressed > 0) {
|
|
2764
|
+
logLatency({
|
|
2765
|
+
type: "phase",
|
|
2766
|
+
toolName: "turn_end",
|
|
2767
|
+
filePath: cwd,
|
|
2768
|
+
phase: "call_graph_impact",
|
|
2769
|
+
durationMs: Date.now() - callGraphStart,
|
|
2770
|
+
metadata: {
|
|
2771
|
+
callees: impactFindings.length,
|
|
2772
|
+
lines: impactLines.length,
|
|
2773
|
+
dispositionSuppressed: callGraphDispositionSuppressed,
|
|
2774
|
+
},
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2711
2777
|
// Non-fatal — call graph is best-effort
|
|
2712
2778
|
}
|
|
2713
2779
|
catch {
|
|
@@ -2779,14 +2845,7 @@ export async function handleTurnEnd(deps) {
|
|
|
2779
2845
|
turnIndex: runtime.turnIndex,
|
|
2780
2846
|
files,
|
|
2781
2847
|
modifiedRangesByFile,
|
|
2782
|
-
|
|
2783
|
-
// surfaced in the unified secrets blocker above (#131 Mode 3) — the
|
|
2784
|
-
// secret is reported once, not twice.
|
|
2785
|
-
dispatchWarnings: runtime
|
|
2786
|
-
.peekActionableWarnings()
|
|
2787
|
-
.filter((w) => !(isSecretWarning(w) &&
|
|
2788
|
-
typeof w.line === "number" &&
|
|
2789
|
-
secretBlockedLocations.has(secretLocationKey(w.filePath, w.line)))),
|
|
2848
|
+
dispatchWarnings: runtime.peekActionableWarnings(),
|
|
2790
2849
|
includeLspCodeActions: !!getFlag("lens-actionable-warning-actions"),
|
|
2791
2850
|
projectSeqStart: runtime.turnStartProjectSeq,
|
|
2792
2851
|
projectSeqEnd: runtime.projectSeq,
|
|
@@ -2883,7 +2942,36 @@ export async function handleTurnEnd(deps) {
|
|
|
2883
2942
|
// #2521: `cwd` is what resolves the store location the advisory
|
|
2884
2943
|
// names -- it is the SAME cwd `publishActionableWarningsReport`
|
|
2885
2944
|
// just wrote through, so the two cannot disagree.
|
|
2886
|
-
const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd, host)
|
|
2945
|
+
const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd, host, (report) => {
|
|
2946
|
+
let dispositionSuppressed = 0;
|
|
2947
|
+
const files = report.files
|
|
2948
|
+
.map((file) => {
|
|
2949
|
+
const policy = filterFindingsByDisposition(file.warnings, cwd, (warning) => {
|
|
2950
|
+
const { line, column, rule, code } = warning;
|
|
2951
|
+
return {
|
|
2952
|
+
filePath: warning.filePath,
|
|
2953
|
+
severity: warning.severity,
|
|
2954
|
+
semantic: "warning",
|
|
2955
|
+
tool: warning.tool,
|
|
2956
|
+
runner: warning.tool,
|
|
2957
|
+
message: warning.message,
|
|
2958
|
+
source: warning.origin === "lsp" ? "lsp" : "dispatch",
|
|
2959
|
+
...(line === undefined ? {} : { line: warning.line }),
|
|
2960
|
+
...(column === undefined ? {} : { column: warning.column }),
|
|
2961
|
+
...(rule === undefined ? {} : { rule: warning.rule }),
|
|
2962
|
+
...(code === undefined ? {} : { code }),
|
|
2963
|
+
};
|
|
2964
|
+
});
|
|
2965
|
+
dispositionSuppressed += policy.suppressed;
|
|
2966
|
+
const kept = policy.kept.filter((warning) => !(isSecretWarning(warning) &&
|
|
2967
|
+
typeof warning.line === "number" &&
|
|
2968
|
+
secretBlockedLocations.has(secretLocationKey(warning.filePath, warning.line))));
|
|
2969
|
+
return kept.length > 0 ? { ...file, warnings: kept } : undefined;
|
|
2970
|
+
})
|
|
2971
|
+
.filter((file) => file !== undefined);
|
|
2972
|
+
recordDispositionSuppressed("actionable-warnings", dispositionSuppressed);
|
|
2973
|
+
return { files, suppressed: dispositionSuppressed };
|
|
2974
|
+
});
|
|
2887
2975
|
// @delivery-surface: runtime-turn:actionable-warnings-advisory
|
|
2888
2976
|
if (advisory)
|
|
2889
2977
|
advisoryParts.push(advisory);
|
|
@@ -2927,11 +3015,46 @@ export async function handleTurnEnd(deps) {
|
|
|
2927
3015
|
}
|
|
2928
3016
|
const t5 = Date.now();
|
|
2929
3017
|
try {
|
|
3018
|
+
// #3248: the code-quality records were filtered at DISPATCH time, before
|
|
3019
|
+
// any `lens_diagnostic_mark`. `lens_diagnostics mode=delta` re-applies
|
|
3020
|
+
// dispositions when it re-serves this same cache
|
|
3021
|
+
// (`tools/lens-diagnostics.ts`'s `visibleWarningFiles`), so without this
|
|
3022
|
+
// the turn-end advisory counted warnings the delta view had already
|
|
3023
|
+
// dropped — the two surfaces disagreed about the same records. Filtered
|
|
3024
|
+
// at the report INPUT so the advisory, the persisted report and the delta
|
|
3025
|
+
// view all agree; the delta view's own filter then re-applies to the same
|
|
3026
|
+
// set and is idempotent. Per file, because the anchor is content-bound.
|
|
3027
|
+
const qualityWarningsByFile = new Map();
|
|
3028
|
+
for (const warning of runtime.peekCodeQualityWarnings()) {
|
|
3029
|
+
const group = qualityWarningsByFile.get(warning.filePath);
|
|
3030
|
+
if (group)
|
|
3031
|
+
group.push(warning);
|
|
3032
|
+
else
|
|
3033
|
+
qualityWarningsByFile.set(warning.filePath, [warning]);
|
|
3034
|
+
}
|
|
3035
|
+
const qualityWarnings = [];
|
|
3036
|
+
let qualityDispositionSuppressed = 0;
|
|
3037
|
+
for (const [filePath, group] of qualityWarningsByFile) {
|
|
3038
|
+
let content;
|
|
3039
|
+
try {
|
|
3040
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
3041
|
+
}
|
|
3042
|
+
catch {
|
|
3043
|
+
content = undefined;
|
|
3044
|
+
}
|
|
3045
|
+
const { kept, suppressed } = applyPushedFindingPolicy(group, {
|
|
3046
|
+
cwd,
|
|
3047
|
+
filePath,
|
|
3048
|
+
content,
|
|
3049
|
+
});
|
|
3050
|
+
qualityWarnings.push(...kept);
|
|
3051
|
+
qualityDispositionSuppressed += suppressed;
|
|
3052
|
+
}
|
|
2930
3053
|
const qualityReport = buildCodeQualityWarningsReport({
|
|
2931
3054
|
cwd,
|
|
2932
3055
|
sessionId: runtime.telemetrySessionId,
|
|
2933
3056
|
turnIndex: runtime.turnIndex,
|
|
2934
|
-
warnings:
|
|
3057
|
+
warnings: qualityWarnings,
|
|
2935
3058
|
modifiedRangesByFile,
|
|
2936
3059
|
projectSeqStart: runtime.turnStartProjectSeq,
|
|
2937
3060
|
projectSeqEnd: runtime.projectSeq,
|
|
@@ -2949,7 +3072,11 @@ export async function handleTurnEnd(deps) {
|
|
|
2949
3072
|
filePath: cwd,
|
|
2950
3073
|
phase: "code_quality_warnings_report",
|
|
2951
3074
|
durationMs: Date.now() - t5,
|
|
2952
|
-
metadata:
|
|
3075
|
+
metadata: {
|
|
3076
|
+
...qualityReport.summary,
|
|
3077
|
+
// #3248: bounded per-turn on this lane's own row.
|
|
3078
|
+
dispositionSuppressed: qualityDispositionSuppressed,
|
|
3079
|
+
},
|
|
2953
3080
|
});
|
|
2954
3081
|
}
|
|
2955
3082
|
catch (err) {
|
|
@@ -2980,6 +3107,8 @@ export async function handleTurnEnd(deps) {
|
|
|
2980
3107
|
let runnerFindingsStale = 0;
|
|
2981
3108
|
let runnerFindingsFailed = 0;
|
|
2982
3109
|
let runnerFindingsDropped = 0;
|
|
3110
|
+
/** #3248: bounded per-turn on this lane's own row, never per finding. */
|
|
3111
|
+
let runnerFindingsDispositionSuppressed = 0;
|
|
2983
3112
|
const runnerFindingsDeliveredIds = [];
|
|
2984
3113
|
for (const pending of pendingRunnerFindings) {
|
|
2985
3114
|
const result = pending.result;
|
|
@@ -2996,12 +3125,15 @@ export async function handleTurnEnd(deps) {
|
|
|
2996
3125
|
const findings = result.diagnostics;
|
|
2997
3126
|
if (findings.length === 0)
|
|
2998
3127
|
continue;
|
|
2999
|
-
const gate = gateFindingsByPathFreshness({
|
|
3000
|
-
store: "late-runner-findings",
|
|
3001
|
-
findings,
|
|
3128
|
+
const { "late-runner-findings": gate } = gateFindingsByPathFreshness({
|
|
3002
3129
|
cwd,
|
|
3003
|
-
|
|
3004
|
-
|
|
3130
|
+
sources: {
|
|
3131
|
+
"late-runner-findings": {
|
|
3132
|
+
findings,
|
|
3133
|
+
scannedAt: pending.markedAtMs,
|
|
3134
|
+
citedPath: (finding) => finding.filePath,
|
|
3135
|
+
},
|
|
3136
|
+
},
|
|
3005
3137
|
});
|
|
3006
3138
|
runnerFindingsStale += gate.stale.length;
|
|
3007
3139
|
if (gate.stale.length > 0) {
|
|
@@ -3014,15 +3146,46 @@ export async function handleTurnEnd(deps) {
|
|
|
3014
3146
|
if (gate.live.length === 0)
|
|
3015
3147
|
continue;
|
|
3016
3148
|
const displayPath = toRunnerDisplayPath(cwd, pending.filePath);
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3149
|
+
// #3248: the survivors are what the agent READS, so they take the same
|
|
3150
|
+
// policy stack the late-AUXILIARY drain below applies — this lane is its
|
|
3151
|
+
// twin (same post-gate `Diagnostic[]`, same rendering) and was the one
|
|
3152
|
+
// push surface still re-reporting a finding the agent had marked. AFTER
|
|
3153
|
+
// the freshness gate, like every other lane: the anchor is derived from
|
|
3154
|
+
// the post-gate identity, never the raw pre-gate set. The file's CURRENT
|
|
3155
|
+
// bytes; an unreadable file fails open inside the helper.
|
|
3156
|
+
let runnerContent;
|
|
3157
|
+
try {
|
|
3158
|
+
runnerContent = fs.readFileSync(pending.filePath, "utf-8");
|
|
3159
|
+
}
|
|
3160
|
+
catch {
|
|
3161
|
+
runnerContent = undefined;
|
|
3162
|
+
}
|
|
3163
|
+
const { kept: runnerKept, suppressed: runnerSuppressedHere } = applyPushedFindingPolicy(gate.live, {
|
|
3164
|
+
cwd,
|
|
3165
|
+
filePath: pending.filePath,
|
|
3166
|
+
content: runnerContent,
|
|
3167
|
+
});
|
|
3168
|
+
runnerFindingsDispositionSuppressed += runnerSuppressedHere;
|
|
3169
|
+
if (runnerKept.length === 0) {
|
|
3170
|
+
// Every late finding was marked. A PUSH surface stays silent rather
|
|
3171
|
+
// than re-announcing that the mark is working; the count rides this
|
|
3172
|
+
// lane's bounded per-turn row below.
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
3175
|
+
const lines = runnerKept.map((finding) => ` ${displayPath}:${finding.line ?? 1}:${finding.column ?? 1} [${finding.rule ?? finding.id}] ${finding.message}`);
|
|
3176
|
+
runnerFindingsDelivered += runnerKept.length;
|
|
3177
|
+
for (const finding of runnerKept) {
|
|
3020
3178
|
if (runnerFindingsDeliveredIds.length < 50) {
|
|
3021
3179
|
runnerFindingsDeliveredIds.push(finding.id);
|
|
3022
3180
|
}
|
|
3023
3181
|
}
|
|
3182
|
+
// #1616 suppressed-bucket rule: a delivery that still has something to
|
|
3183
|
+
// say states what it dropped, once per delivery.
|
|
3184
|
+
const runnerSuppressedNote = runnerSuppressedHere > 0
|
|
3185
|
+
? `; suppressed by disposition: ${runnerSuppressedHere} finding(s)`
|
|
3186
|
+
: "";
|
|
3024
3187
|
// @delivery-surface: runtime-turn:late-runner-findings
|
|
3025
|
-
advisoryParts.push(`⏱️ Late runner diagnostics (${pending.runnerId} completed after the edit):\n${lines.join("\n")}`);
|
|
3188
|
+
advisoryParts.push(`⏱️ Late runner diagnostics (${pending.runnerId} completed after the edit${runnerSuppressedNote}):\n${lines.join("\n")}`);
|
|
3026
3189
|
}
|
|
3027
3190
|
logLatency({
|
|
3028
3191
|
type: "phase",
|
|
@@ -3036,6 +3199,7 @@ export async function handleTurnEnd(deps) {
|
|
|
3036
3199
|
stale: runnerFindingsStale,
|
|
3037
3200
|
failed: runnerFindingsFailed,
|
|
3038
3201
|
dropped: runnerFindingsDropped,
|
|
3202
|
+
dispositionSuppressed: runnerFindingsDispositionSuppressed,
|
|
3039
3203
|
deliveredIds: runnerFindingsDeliveredIds,
|
|
3040
3204
|
},
|
|
3041
3205
|
});
|
|
@@ -3264,12 +3428,15 @@ export async function handleTurnEnd(deps) {
|
|
|
3264
3428
|
// one), so a stale-arm replay would double-report old content.
|
|
3265
3429
|
// Both drops are COUNTED here and in the latency record below —
|
|
3266
3430
|
// never silent (shape 10).
|
|
3267
|
-
const gate = gateFindingsByPathFreshness({
|
|
3268
|
-
store: "late-auxiliary-findings",
|
|
3269
|
-
findings: retained,
|
|
3431
|
+
const { "late-auxiliary-findings": gate } = gateFindingsByPathFreshness({
|
|
3270
3432
|
cwd,
|
|
3271
|
-
|
|
3272
|
-
|
|
3433
|
+
sources: {
|
|
3434
|
+
"late-auxiliary-findings": {
|
|
3435
|
+
findings: retained,
|
|
3436
|
+
scannedAt: pair.markedAtMs,
|
|
3437
|
+
citedPath: () => lateAuxPath,
|
|
3438
|
+
},
|
|
3439
|
+
},
|
|
3273
3440
|
});
|
|
3274
3441
|
lateAuxStale += gate.stale.length;
|
|
3275
3442
|
lateAuxMissing +=
|
|
@@ -3314,15 +3481,15 @@ export async function handleTurnEnd(deps) {
|
|
|
3314
3481
|
// a late pair. Runs AFTER the freshness gate, like the #1625
|
|
3315
3482
|
// govulncheck/secrets filters: the anchor is derived from the
|
|
3316
3483
|
// post-gate identity, never the raw pre-gate set.
|
|
3317
|
-
|
|
3484
|
+
// #3248: the four arguments moved into
|
|
3485
|
+
// `applyPushedFindingPolicy` so the late-RUNNER drain below
|
|
3486
|
+
// cannot make a second copy of them. Same stack, same
|
|
3487
|
+
// identities, same fail-open content rule.
|
|
3488
|
+
const { kept: lateAuxKept, suppressed: lateAuxSuppressedHere } = applyPushedFindingPolicy(gate.live, {
|
|
3318
3489
|
cwd,
|
|
3319
3490
|
filePath: lateAuxPath,
|
|
3320
|
-
content: lateAuxContent
|
|
3321
|
-
// mtime-cached, so a drain of several files costs one stat.
|
|
3322
|
-
policyMap: loadProjectRulePolicyMap(cwd),
|
|
3323
|
-
identities: renderedRuleIdentities,
|
|
3491
|
+
content: lateAuxContent,
|
|
3324
3492
|
});
|
|
3325
|
-
const lateAuxSuppressedHere = gate.live.length - lateAuxKept.length;
|
|
3326
3493
|
lateAuxDispositionSuppressed += lateAuxSuppressedHere;
|
|
3327
3494
|
if (lateAuxKept.length === 0) {
|
|
3328
3495
|
// Every late finding was suppressed. This is a PUSH surface:
|