@herbertgao/pi-extensions 2026.10.1 → 2026.10.3
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 +14 -17
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
- package/node_modules/{remote-pi → @herbertgao/pi-agency}/LICENSE +1 -1
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +1 -1
- package/node_modules/@herbertgao/pi-subagents/package.json +4 -5
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +48 -72
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +4 -1
- package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +9 -8
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +6 -1
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +13 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1159 -0
- package/node_modules/pi-lens/README.md +116 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-2FQ62SPM.js +83 -0
- package/node_modules/pi-lens/dist/chunk-2UMYMRI3.js +118 -0
- package/node_modules/pi-lens/dist/chunk-34OCDWOS.js +48 -0
- package/node_modules/pi-lens/dist/chunk-3FERWMCH.js +144 -0
- package/node_modules/pi-lens/dist/chunk-3JL6IFD7.js +97 -0
- package/node_modules/pi-lens/dist/chunk-3JSEWUWI.js +327 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-3MRXIJ6I.js +79 -0
- package/node_modules/pi-lens/dist/chunk-3P4PF2BX.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-3PNFWVHT.js +201 -0
- package/node_modules/pi-lens/dist/chunk-3TTPKMEF.js +162 -0
- package/node_modules/pi-lens/dist/chunk-5JHXUETW.js +326 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-64LU2OVH.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6B5I6ICN.js +44 -0
- package/node_modules/pi-lens/dist/chunk-6P54TMWL.js +274 -0
- package/node_modules/pi-lens/dist/chunk-6UYA642J.js +145 -0
- package/node_modules/pi-lens/dist/chunk-6XX5HCLK.js +398 -0
- package/node_modules/pi-lens/dist/chunk-7AXO3ZDF.js +30 -0
- package/node_modules/pi-lens/dist/chunk-7BX5DWSA.js +1907 -0
- package/node_modules/pi-lens/dist/chunk-7ELY2CSM.js +902 -0
- package/node_modules/pi-lens/dist/chunk-7M4T5N3V.js +38 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AFZYMAIV.js +756 -0
- package/node_modules/pi-lens/dist/chunk-ALQRYH6O.js +112 -0
- package/node_modules/pi-lens/dist/chunk-AXKPQ6ZI.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-BHWHXMQ2.js +62 -0
- package/node_modules/pi-lens/dist/chunk-BJF25SUG.js +22 -0
- package/node_modules/pi-lens/dist/chunk-BNKLYOOO.js +18 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-CBD2B5CP.js +599 -0
- package/node_modules/pi-lens/dist/chunk-CFYISHUW.js +42 -0
- package/node_modules/pi-lens/dist/chunk-CMU4RMJB.js +30 -0
- package/node_modules/pi-lens/dist/chunk-CULQYFLU.js +14 -0
- package/node_modules/pi-lens/dist/chunk-D65GOCJB.js +289 -0
- package/node_modules/pi-lens/dist/chunk-D6YF5HKX.js +59 -0
- package/node_modules/pi-lens/dist/chunk-DMS52BEP.js +342 -0
- package/node_modules/pi-lens/dist/chunk-ELVWJYRV.js +211 -0
- package/node_modules/pi-lens/dist/chunk-EUT3B22Y.js +27 -0
- package/node_modules/pi-lens/dist/chunk-F5VLZHUW.js +146 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FGALODG3.js +55 -0
- package/node_modules/pi-lens/dist/chunk-G3RLJYA2.js +2358 -0
- package/node_modules/pi-lens/dist/chunk-GAYGGICG.js +32 -0
- package/node_modules/pi-lens/dist/chunk-H2W2AXD3.js +419 -0
- package/node_modules/pi-lens/dist/chunk-H5EV3VMF.js +114 -0
- package/node_modules/pi-lens/dist/chunk-HREZQCT3.js +106 -0
- package/node_modules/pi-lens/dist/chunk-HSIRMK2D.js +517 -0
- package/node_modules/pi-lens/dist/chunk-IMSQR4WO.js +184 -0
- package/node_modules/pi-lens/dist/chunk-IQZXOZ7S.js +4816 -0
- package/node_modules/pi-lens/dist/chunk-IR6XFA5P.js +37 -0
- package/node_modules/pi-lens/dist/chunk-IUKPRZER.js +78 -0
- package/node_modules/pi-lens/dist/chunk-IUR76K6A.js +262 -0
- package/node_modules/pi-lens/dist/chunk-J3FZR3YY.js +98 -0
- package/node_modules/pi-lens/dist/chunk-J6DFZDFA.js +562 -0
- package/node_modules/pi-lens/dist/chunk-J7WJZ6W7.js +50 -0
- package/node_modules/pi-lens/dist/chunk-JONGANYH.js +202 -0
- package/node_modules/pi-lens/dist/chunk-KJCWXUUS.js +425 -0
- package/node_modules/pi-lens/dist/chunk-KJN4IOEJ.js +17 -0
- package/node_modules/pi-lens/dist/chunk-KMGPUKFD.js +25 -0
- package/node_modules/pi-lens/dist/chunk-LD7WFTRX.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNOMA3MP.js +169 -0
- package/node_modules/pi-lens/dist/chunk-MGIWSROG.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-MQA2YBAH.js +308 -0
- package/node_modules/pi-lens/dist/chunk-MTDRLSI7.js +21720 -0
- package/node_modules/pi-lens/dist/chunk-N2IRDBZI.js +2355 -0
- package/node_modules/pi-lens/dist/chunk-N77WFUL7.js +50 -0
- package/node_modules/pi-lens/dist/chunk-N7YNJE5R.js +466 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NFXP47RV.js +999 -0
- package/node_modules/pi-lens/dist/chunk-NJF2X5XF.js +45 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-O7HZX7CA.js +309 -0
- package/node_modules/pi-lens/dist/chunk-OC7LYAU5.js +708 -0
- package/node_modules/pi-lens/dist/chunk-OEX24ECI.js +348 -0
- package/node_modules/pi-lens/dist/chunk-OYOB3ZFG.js +78 -0
- package/node_modules/pi-lens/dist/chunk-OYTMZXRF.js +41 -0
- package/node_modules/pi-lens/dist/chunk-OZJINN4O.js +84 -0
- package/node_modules/pi-lens/dist/chunk-PCHDGUKT.js +23 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RJFED73B.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RTQNRM5X.js +27 -0
- package/node_modules/pi-lens/dist/chunk-S6AMWKPC.js +189 -0
- package/node_modules/pi-lens/dist/chunk-T4QZ6FEH.js +308 -0
- package/node_modules/pi-lens/dist/chunk-T54VJRAD.js +5456 -0
- package/node_modules/pi-lens/dist/chunk-TA5L5KWQ.js +167 -0
- package/node_modules/pi-lens/dist/chunk-TQFBZVCG.js +27 -0
- package/node_modules/pi-lens/dist/chunk-U52SAGRM.js +62 -0
- package/node_modules/pi-lens/dist/chunk-UEAHAMEI.js +5814 -0
- package/node_modules/pi-lens/dist/chunk-UGSMCQ4D.js +327 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-USHG4DLH.js +459 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-V5MEBST3.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-VGEVL5EQ.js +29 -0
- package/node_modules/pi-lens/dist/chunk-VPWKF3GN.js +87 -0
- package/node_modules/pi-lens/dist/chunk-VSMJK3ZJ.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-VYKKMYVT.js +560 -0
- package/node_modules/pi-lens/dist/chunk-VZHJS6DR.js +442 -0
- package/node_modules/pi-lens/dist/chunk-WATC3IQR.js +213 -0
- package/node_modules/pi-lens/dist/chunk-WPE5FPGR.js +54 -0
- package/node_modules/pi-lens/dist/chunk-WSEWKCXU.js +188 -0
- package/node_modules/pi-lens/dist/chunk-WW5BLB74.js +659 -0
- package/node_modules/pi-lens/dist/chunk-XGUL4SLQ.js +406 -0
- package/node_modules/pi-lens/dist/chunk-XHSCVSOL.js +170 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-YLN4K6WD.js +385 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-Z6BB4JLQ.js +478 -0
- package/node_modules/pi-lens/dist/chunk-Z7QQA2KT.js +27 -0
- package/node_modules/pi-lens/dist/chunk-ZETYB3XG.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-ZMOMPX2Z.js +87 -0
- package/node_modules/pi-lens/dist/chunk-ZOEYVXSV.js +149 -0
- package/node_modules/pi-lens/dist/chunk-ZZUFZUQZ.js +12120 -0
- package/node_modules/pi-lens/dist/index.js +39712 -29876
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22688 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -4
- package/node_modules/pi-lens/docs/configuration.md +28 -2
- package/node_modules/pi-lens/docs/custom-rules.md +25 -2
- package/node_modules/pi-lens/docs/environment-variables.md +86 -8
- package/node_modules/pi-lens/docs/features.md +45 -5
- package/node_modules/pi-lens/docs/globalconfig.md +2 -1
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +37 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +160 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +736 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +75 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +189 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/schema/pi-lens-config-v1.json +530 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +56 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/docs/word-index.md +6 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-identity-operator-on-literals-test.yml +12 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-identity-operator-on-literals.yml +20 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +10 -6
- package/node_modules/pi-web-access/ceramic.ts +219 -0
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +846 -588
- package/node_modules/pi-web-access/dist/mcp-cli.js +725 -516
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +34 -2
- package/node_modules/pi-web-access/index.ts +72 -30
- package/node_modules/pi-web-access/openai-search.ts +3 -3
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/page-query.ts +6 -9
- package/node_modules/pi-web-access/parallel-mcp.ts +2 -0
- package/node_modules/pi-web-access/query-rewrite.ts +2 -2
- package/node_modules/pi-web-access/storage.ts +49 -0
- package/node_modules/pi-web-access/summary-review.ts +7 -6
- package/node_modules/pi-web-access/utils.ts +21 -6
- package/node_modules/pi-web-access/web-tool-core.ts +1 -0
- package/package.json +19 -19
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
- package/node_modules/pi-web-access/opencode-session-headers.ts +0 -24
- package/node_modules/remote-pi/README.md +0 -675
- package/node_modules/remote-pi/dist/actions/handlers.d.ts +0 -122
- package/node_modules/remote-pi/dist/actions/handlers.js +0 -160
- package/node_modules/remote-pi/dist/actions/handlers.js.map +0 -1
- package/node_modules/remote-pi/dist/actions/registry.d.ts +0 -39
- package/node_modules/remote-pi/dist/actions/registry.js +0 -58
- package/node_modules/remote-pi/dist/actions/registry.js.map +0 -1
- package/node_modules/remote-pi/dist/bin/supervisord.d.ts +0 -2
- package/node_modules/remote-pi/dist/bin/supervisord.js +0 -86
- package/node_modules/remote-pi/dist/bin/supervisord.js.map +0 -1
- package/node_modules/remote-pi/dist/commands/builtin_mirror.d.ts +0 -58
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js +0 -71
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js.map +0 -1
- package/node_modules/remote-pi/dist/commands/list_commands.d.ts +0 -60
- package/node_modules/remote-pi/dist/commands/list_commands.js +0 -73
- package/node_modules/remote-pi/dist/commands/list_commands.js.map +0 -1
- package/node_modules/remote-pi/dist/config.d.ts +0 -62
- package/node_modules/remote-pi/dist/config.js +0 -115
- package/node_modules/remote-pi/dist/config.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/client.d.ts +0 -20
- package/node_modules/remote-pi/dist/daemon/client.js +0 -131
- package/node_modules/remote-pi/dist/daemon/client.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/control_protocol.d.ts +0 -176
- package/node_modules/remote-pi/dist/daemon/control_protocol.js +0 -63
- package/node_modules/remote-pi/dist/daemon/control_protocol.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/cron_log.d.ts +0 -45
- package/node_modules/remote-pi/dist/daemon/cron_log.js +0 -72
- package/node_modules/remote-pi/dist/daemon/cron_log.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/cron_registry.d.ts +0 -80
- package/node_modules/remote-pi/dist/daemon/cron_registry.js +0 -194
- package/node_modules/remote-pi/dist/daemon/cron_registry.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/id.d.ts +0 -24
- package/node_modules/remote-pi/dist/daemon/id.js +0 -36
- package/node_modules/remote-pi/dist/daemon/id.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/install.d.ts +0 -170
- package/node_modules/remote-pi/dist/daemon/install.js +0 -547
- package/node_modules/remote-pi/dist/daemon/install.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/registry.d.ts +0 -63
- package/node_modules/remote-pi/dist/daemon/registry.js +0 -153
- package/node_modules/remote-pi/dist/daemon/registry.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/rpc_child.d.ts +0 -194
- package/node_modules/remote-pi/dist/daemon/rpc_child.js +0 -389
- package/node_modules/remote-pi/dist/daemon/rpc_child.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/supervisor.d.ts +0 -93
- package/node_modules/remote-pi/dist/daemon/supervisor.js +0 -596
- package/node_modules/remote-pi/dist/daemon/supervisor.js.map +0 -1
- package/node_modules/remote-pi/dist/extension_ui_bridge.d.ts +0 -26
- package/node_modules/remote-pi/dist/extension_ui_bridge.js +0 -341
- package/node_modules/remote-pi/dist/extension_ui_bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/index.d.ts +0 -213
- package/node_modules/remote-pi/dist/index.js +0 -4996
- package/node_modules/remote-pi/dist/index.js.map +0 -1
- package/node_modules/remote-pi/dist/mcp/mesh_result.d.ts +0 -10
- package/node_modules/remote-pi/dist/mcp/mesh_result.js +0 -42
- package/node_modules/remote-pi/dist/mcp/mesh_result.js.map +0 -1
- package/node_modules/remote-pi/dist/mcp/mesh_server.d.ts +0 -16
- package/node_modules/remote-pi/dist/mcp/mesh_server.js +0 -327
- package/node_modules/remote-pi/dist/mcp/mesh_server.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/canonical.d.ts +0 -30
- package/node_modules/remote-pi/dist/mesh/canonical.js +0 -61
- package/node_modules/remote-pi/dist/mesh/canonical.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/client.d.ts +0 -18
- package/node_modules/remote-pi/dist/mesh/client.js +0 -125
- package/node_modules/remote-pi/dist/mesh/client.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/encoding.d.ts +0 -38
- package/node_modules/remote-pi/dist/mesh/encoding.js +0 -216
- package/node_modules/remote-pi/dist/mesh/encoding.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/self_revoke.d.ts +0 -73
- package/node_modules/remote-pi/dist/mesh/self_revoke.js +0 -401
- package/node_modules/remote-pi/dist/mesh/self_revoke.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/siblings.d.ts +0 -49
- package/node_modules/remote-pi/dist/mesh/siblings.js +0 -159
- package/node_modules/remote-pi/dist/mesh/siblings.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/types.d.ts +0 -34
- package/node_modules/remote-pi/dist/mesh/types.js +0 -11
- package/node_modules/remote-pi/dist/mesh/types.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/verify.d.ts +0 -17
- package/node_modules/remote-pi/dist/mesh/verify.js +0 -86
- package/node_modules/remote-pi/dist/mesh/verify.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/crypto.d.ts +0 -8
- package/node_modules/remote-pi/dist/pairing/crypto.js +0 -22
- package/node_modules/remote-pi/dist/pairing/crypto.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/handshake.d.ts +0 -28
- package/node_modules/remote-pi/dist/pairing/handshake.js +0 -113
- package/node_modules/remote-pi/dist/pairing/handshake.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/noise-sha256.d.ts +0 -16
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js +0 -103
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/qr.d.ts +0 -59
- package/node_modules/remote-pi/dist/pairing/qr.js +0 -125
- package/node_modules/remote-pi/dist/pairing/qr.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/storage.d.ts +0 -120
- package/node_modules/remote-pi/dist/pairing/storage.js +0 -604
- package/node_modules/remote-pi/dist/pairing/storage.js.map +0 -1
- package/node_modules/remote-pi/dist/protocol/codec.d.ts +0 -7
- package/node_modules/remote-pi/dist/protocol/codec.js +0 -49
- package/node_modules/remote-pi/dist/protocol/codec.js.map +0 -1
- package/node_modules/remote-pi/dist/protocol/types.d.ts +0 -393
- package/node_modules/remote-pi/dist/protocol/types.js +0 -2
- package/node_modules/remote-pi/dist/protocol/types.js.map +0 -1
- package/node_modules/remote-pi/dist/rooms.d.ts +0 -29
- package/node_modules/remote-pi/dist/rooms.js +0 -57
- package/node_modules/remote-pi/dist/rooms.js.map +0 -1
- package/node_modules/remote-pi/dist/session/address.d.ts +0 -49
- package/node_modules/remote-pi/dist/session/address.js +0 -58
- package/node_modules/remote-pi/dist/session/address.js.map +0 -1
- package/node_modules/remote-pi/dist/session/agent_bridge.d.ts +0 -55
- package/node_modules/remote-pi/dist/session/agent_bridge.js +0 -146
- package/node_modules/remote-pi/dist/session/agent_bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/session/bridge.d.ts +0 -37
- package/node_modules/remote-pi/dist/session/bridge.js +0 -130
- package/node_modules/remote-pi/dist/session/bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/session/broker.d.ts +0 -189
- package/node_modules/remote-pi/dist/session/broker.js +0 -507
- package/node_modules/remote-pi/dist/session/broker.js.map +0 -1
- package/node_modules/remote-pi/dist/session/broker_remote.d.ts +0 -74
- package/node_modules/remote-pi/dist/session/broker_remote.js +0 -583
- package/node_modules/remote-pi/dist/session/broker_remote.js.map +0 -1
- package/node_modules/remote-pi/dist/session/cwd_lock.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/cwd_lock.js +0 -126
- package/node_modules/remote-pi/dist/session/cwd_lock.js.map +0 -1
- package/node_modules/remote-pi/dist/session/envelope.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/envelope.js +0 -117
- package/node_modules/remote-pi/dist/session/envelope.js.map +0 -1
- package/node_modules/remote-pi/dist/session/global_config.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/global_config.js +0 -73
- package/node_modules/remote-pi/dist/session/global_config.js.map +0 -1
- package/node_modules/remote-pi/dist/session/ipc.d.ts +0 -27
- package/node_modules/remote-pi/dist/session/ipc.js +0 -22
- package/node_modules/remote-pi/dist/session/ipc.js.map +0 -1
- package/node_modules/remote-pi/dist/session/leader_election.d.ts +0 -32
- package/node_modules/remote-pi/dist/session/leader_election.js +0 -106
- package/node_modules/remote-pi/dist/session/leader_election.js.map +0 -1
- package/node_modules/remote-pi/dist/session/local_config.d.ts +0 -55
- package/node_modules/remote-pi/dist/session/local_config.js +0 -157
- package/node_modules/remote-pi/dist/session/local_config.js.map +0 -1
- package/node_modules/remote-pi/dist/session/mesh_message.d.ts +0 -20
- package/node_modules/remote-pi/dist/session/mesh_message.js +0 -28
- package/node_modules/remote-pi/dist/session/mesh_message.js.map +0 -1
- package/node_modules/remote-pi/dist/session/mesh_node.d.ts +0 -116
- package/node_modules/remote-pi/dist/session/mesh_node.js +0 -573
- package/node_modules/remote-pi/dist/session/mesh_node.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer.d.ts +0 -139
- package/node_modules/remote-pi/dist/session/peer.js +0 -451
- package/node_modules/remote-pi/dist/session/peer.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer_inventory.d.ts +0 -13
- package/node_modules/remote-pi/dist/session/peer_inventory.js +0 -48
- package/node_modules/remote-pi/dist/session/peer_inventory.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer_limits.d.ts +0 -9
- package/node_modules/remote-pi/dist/session/peer_limits.js +0 -21
- package/node_modules/remote-pi/dist/session/peer_limits.js.map +0 -1
- package/node_modules/remote-pi/dist/session/setup_wizard.d.ts +0 -39
- package/node_modules/remote-pi/dist/session/setup_wizard.js +0 -63
- package/node_modules/remote-pi/dist/session/setup_wizard.js.map +0 -1
- package/node_modules/remote-pi/dist/session/tool_gate.d.ts +0 -5
- package/node_modules/remote-pi/dist/session/tool_gate.js +0 -11
- package/node_modules/remote-pi/dist/session/tool_gate.js.map +0 -1
- package/node_modules/remote-pi/dist/session/tools.d.ts +0 -24
- package/node_modules/remote-pi/dist/session/tools.js +0 -252
- package/node_modules/remote-pi/dist/session/tools.js.map +0 -1
- package/node_modules/remote-pi/dist/session/wizard.d.ts +0 -13
- package/node_modules/remote-pi/dist/session/wizard.js +0 -20
- package/node_modules/remote-pi/dist/session/wizard.js.map +0 -1
- package/node_modules/remote-pi/dist/settings.d.ts +0 -15
- package/node_modules/remote-pi/dist/settings.js +0 -52
- package/node_modules/remote-pi/dist/settings.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/peer_channel.d.ts +0 -37
- package/node_modules/remote-pi/dist/transport/peer_channel.js +0 -98
- package/node_modules/remote-pi/dist/transport/peer_channel.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/pi_forward_client.d.ts +0 -29
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js +0 -62
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/relay_client.d.ts +0 -91
- package/node_modules/remote-pi/dist/transport/relay_client.js +0 -206
- package/node_modules/remote-pi/dist/transport/relay_client.js.map +0 -1
- package/node_modules/remote-pi/dist/ui/footer.d.ts +0 -32
- package/node_modules/remote-pi/dist/ui/footer.js +0 -34
- package/node_modules/remote-pi/dist/ui/footer.js.map +0 -1
- package/node_modules/remote-pi/docs/daemon.md +0 -289
- package/node_modules/remote-pi/package.json +0 -92
- package/node_modules/remote-pi/service-templates/launchd.plist.template +0 -35
- package/node_modules/remote-pi/service-templates/systemd.service.template +0 -19
- package/node_modules/remote-pi/service-templates/task-launcher.vbs.template +0 -10
- package/node_modules/remote-pi/service-templates/task-scheduler.xml.template +0 -38
- package/node_modules/remote-pi/skills/agent-network/SKILL.md +0 -310
|
@@ -1,3767 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { buildActionableWarningsReport, formatActionableWarningsAdvisory, publishActionableWarningsReport, writeDeferredActionableWarningsReport, } from "./actionable-warnings.js";
|
|
4
|
-
import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
|
|
5
|
-
import { appendCodeQualityWarningsHistory, buildCodeQualityWarningsReport, formatCodeQualityWarningsAdvisory, writeCodeQualityWarningsReport, } from "./code-quality-warnings.js";
|
|
6
|
-
import { clearGitGuardTestFailure, mergeGitGuardTestFailure, resyncGitGuardAfterInlinePolicy, writeGitGuardRecord, } from "./git-guard.js";
|
|
7
|
-
import { cascadeSettleWaitMs } from "./cascade-budget.js";
|
|
8
|
-
import { logCascade } from "./cascade-logger.js";
|
|
9
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
10
|
-
import { compareOrdinal } from "./string-utils.js";
|
|
11
|
-
import { resolveRunnerPath, toRunnerDisplayPath, } from "./dispatch/runner-context.js";
|
|
12
|
-
import { getKnipIgnorePatterns } from "./file-utils.js";
|
|
13
|
-
import { formatCacheAgeLabel } from "./finding-delivery-gate.js";
|
|
14
|
-
import { getFullScanWallClockMs, isWorkspaceSweepActive, runWhenWorkspaceSweepIdle, SWEEP_IDLE_SAFETY_MARGIN_MS, } from "./lsp/workspace-sweep-hold.js";
|
|
15
|
-
import { isTestRoleCollateral } from "./collateral-test-role.js";
|
|
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";
|
|
19
|
-
import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, stableFindingKey, } from "./dead-code-client.js";
|
|
20
|
-
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
21
|
-
import { PROJECT_DIAGNOSTICS_CACHE_VERSION, writeProjectDiagnosticsDeltaReport, } from "./project-diagnostics/cache.js";
|
|
22
|
-
import { deadCodeIssueToProjectDiagnostic } from "./project-diagnostics/runner-adapters/dead-code.js";
|
|
23
|
-
import { trivyFindingToProjectDiagnostic } from "./project-diagnostics/runner-adapters/trivy.js";
|
|
24
|
-
import { knipIssuesToProjectDiagnostics } from "./project-diagnostics/runner-adapters/knip.js";
|
|
25
|
-
import { logLatency } from "./latency-logger.js";
|
|
26
|
-
import { getLspBudgetIdleTimeoutMs, shouldShortenLspIdleTimeout, } from "./lsp-budget.js";
|
|
27
|
-
import { updateHeartbeat } from "./instance-registry.js";
|
|
28
|
-
import { emitLensTurnFindings } from "./lens-events.js";
|
|
29
|
-
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
30
|
-
import { isSubagentSession } from "./subagent-mode.js";
|
|
31
|
-
import { formatRunDurationMs } from "./run-duration.js";
|
|
32
|
-
import { isExcludedTestTarget, isRunnerErrorResult, RUNNERS, } from "./test-runner-client.js";
|
|
33
|
-
import { MAX_ADVISORY_AFFECTED_FILES, gateFindingsByPathFreshness, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
|
|
34
|
-
import { DEPENDENCY_DRIFT_MAX_DELIVERIES, sweepInlineBlockerFreshness, } from "./blocker-freshness.js";
|
|
35
|
-
import { sweepInlineBlockerPastEof } from "./blocker-past-eof.js";
|
|
36
|
-
// #2001/#2002: collect-later delivery for slow auxiliary LSP servers.
|
|
37
|
-
import { getLSPService } from "./lsp/index.js";
|
|
38
|
-
import { drainPendingAuxCapEvictedCount, drainPendingAuxiliaryCoverage, isPendingAuxiliaryPastRearmTtl, rearmPendingAuxiliaryCoverage, MAX_LATE_AUX_REARMS, pendingAuxiliaryCoverageSize, } from "./lsp/pending-aux-coverage.js";
|
|
39
|
-
import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
|
|
40
|
-
import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
|
|
41
|
-
import { runPersistentReverify, } from "./persistent-reverify.js";
|
|
42
|
-
import { cascadeCarrySuffix } from "./cascade-format.js";
|
|
43
|
-
import { applyPushedFindingPolicy, filterFindingsByDisposition, } from "./dispatch/finding-policy.js";
|
|
44
|
-
import { detectFileRole } from "./file-role.js";
|
|
45
|
-
import { applyInlineBlockerPolicy, summarizeInlineBlockerPolicy, } from "./inline-blocker-dispositions.js";
|
|
46
|
-
import { drainPendingRunnerFindings, dropStaleRunnerFindings, pendingRunnerFindingsSize, } from "./dispatch/pending-runner-findings.js";
|
|
47
|
-
// #1631 review V2: moved to its own leaf module so a low-level store
|
|
48
|
-
// (widget-state.ts) can use the marker without importing this orchestrator —
|
|
49
|
-
// see clients/stale-marker.ts's doc comment.
|
|
50
|
-
import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
51
|
-
import { mapWithConcurrency } from "./map-with-concurrency.js";
|
|
52
|
-
import { getAmbientAbortSignal } from "./safe-spawn.js";
|
|
53
|
-
import { emitBounded } from "./bounded-telemetry.js";
|
|
54
|
-
import { degradeDemotedFindingBody, formatDeliveryCapNote, formatRetirementNote, } from "./demoted-finding-render.js";
|
|
55
|
-
import { STALE_LINE_MARKER } from "./stale-marker.js";
|
|
56
|
-
import { getActiveSessionId } from "./session-lifecycle.js";
|
|
57
|
-
import { bounded } from "./deadline-utils.js";
|
|
58
|
-
import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
|
|
59
|
-
import { drainRenderedDependencyDriftFilePaths, getWidgetBlockingFilesForSweep, incrementWidgetDependencyDriftDelivery, markWidgetFileBlockersStale, recordRunner, retireWidgetDependencyDriftBlockers, } from "./widget-state.js";
|
|
60
|
-
/** Maximum detailed notify-stall coverage-gap rows emitted in one turn. */
|
|
61
|
-
const LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN = 20;
|
|
62
|
-
/**
|
|
63
|
-
* #2504 — bounds on the per-turn test-runner fan-out.
|
|
64
|
-
*
|
|
65
|
-
* The reported turn fired 59 `vitest.cmd` spawns at once from a bare
|
|
66
|
-
* `Promise.allSettled(targets.map(...))`. Each spawn carries its OWN 60 s
|
|
67
|
-
* timeout (`test-runner-client.ts`), which bounds one target and says nothing
|
|
68
|
-
* about the batch: 59 of them starved the event loop for the whole turn
|
|
69
|
-
* (`cpuCoverageRatio 0.56`, 20 orphan-backstop escalations inside the storm).
|
|
70
|
-
*
|
|
71
|
-
* Three separate bounds, because they fail differently:
|
|
72
|
-
* - CONCURRENCY caps how much CPU the batch can hold at one instant;
|
|
73
|
-
* - TARGET COUNT caps how much work one turn may enqueue at all;
|
|
74
|
-
* - the WALL BUDGET caps how long the batch may keep spawning, and is raced
|
|
75
|
-
* against the ambient abort signal so a cancelled turn stops dispatching
|
|
76
|
-
* immediately rather than at the next natural boundary.
|
|
77
|
-
*
|
|
78
|
-
* #2522: the batch budget shipped at 90 s in #2509 — well over the turn
|
|
79
|
-
* itself, so a slow/hung batch could still hold up the agent for most of a
|
|
80
|
-
* turn before the bound even engaged. Reconciled down to 20 s (one constant,
|
|
81
|
-
* not a second budget alongside it): well under the turn, and still an order
|
|
82
|
-
* of magnitude below the PER-TARGET 60 s cap in `test-runner-client.ts`, so
|
|
83
|
-
* the batch bound is the one that actually fires first for a stuck target.
|
|
84
|
-
*
|
|
85
|
-
* #2522 review round 2: lowering the budget on its own made things WORSE,
|
|
86
|
-
* because the "deferred to the next turn" this comment used to claim did not
|
|
87
|
-
* exist. `runTestTargetsBounded` returned a skipped COUNT and threw the
|
|
88
|
-
* identity of the unrun targets away, so a target slower than the budget
|
|
89
|
-
* produced no result, no cache record and no output — every turn, forever —
|
|
90
|
-
* while a PARTIAL batch fell through to the clean branch and was reported as
|
|
91
|
-
* "all tests passed". The deferral is now a real mechanism:
|
|
92
|
-
*
|
|
93
|
-
* - the batch owns an `AbortController` that is handed to each target's spawn
|
|
94
|
-
* (`TestRunRequest.signal` → `safeSpawnAsync`), so reaching the bound KILLS
|
|
95
|
-
* what is in flight instead of leaving it to burn its own 60 s timeout and
|
|
96
|
-
* then mutate a results array the caller has already consumed;
|
|
97
|
-
* - the killed set plus the never-dispatched set is returned BY IDENTITY as
|
|
98
|
-
* `deferred`, persisted on the `test-runner-findings` record as
|
|
99
|
-
* `deferredTargets`, and dispatched FIRST on the next turn — ahead of
|
|
100
|
-
* failed-first/related/self — under the same `TEST_RUNNER_MAX_TARGETS` cap;
|
|
101
|
-
* - a batch whose `deferred` set is non-empty can never take the clean
|
|
102
|
-
* branch, so an unfinished run is never reported as a green one and never
|
|
103
|
-
* relaxes the `--lens-guard` commit gate.
|
|
104
|
-
*/
|
|
105
|
-
export const TEST_RUNNER_BATCH_CONCURRENCY = 4;
|
|
106
|
-
export const TEST_RUNNER_MAX_TARGETS = 12;
|
|
107
|
-
export const TEST_RUNNER_BATCH_BUDGET_MS = 20_000;
|
|
108
|
-
/**
|
|
109
|
-
* How many consecutive turn-end batches a target may be cut out of before it
|
|
110
|
-
* is retired from turn-end selection altogether.
|
|
111
|
-
*
|
|
112
|
-
* Without this the deferral is a LIVELOCK, and not hypothetically: this
|
|
113
|
-
* repo's own `tests/index-integration.test.ts` — which turn-end resolves
|
|
114
|
-
* through the `related` strategy whenever `clients/runtime-turn.ts` is
|
|
115
|
-
* edited — takes 26.4 s, MORE than the entire 20 s batch budget on its own.
|
|
116
|
-
* Deferring it puts it FIRST in the next batch, where it is cut again at
|
|
117
|
-
* 20 s, and again, every turn, forever. A target that cannot fit the budget
|
|
118
|
-
* is not a scheduling problem retrying can solve; it is reported once per
|
|
119
|
-
* occurrence and dropped, so the turn stops paying 20 s for it.
|
|
120
|
-
*/
|
|
121
|
-
export const TEST_RUNNER_MAX_DEFERRALS = 2;
|
|
122
|
-
/**
|
|
123
|
-
* Ceiling on how many entries either persisted target list may carry.
|
|
124
|
-
*
|
|
125
|
-
* #2522 review round 4, I1: a write may no longer destroy another session's
|
|
126
|
-
* entries, so nothing in the write path prunes them any more — each session
|
|
127
|
-
* that ever cut or retired a target in this project leaves its rows behind, on
|
|
128
|
-
* a record read at every single turn_end. The bound is applied at the one
|
|
129
|
-
* writer, on the whole list, and sheds FOREIGN rows first (the writer orders
|
|
130
|
-
* this session's entries last), so it can never evict the entries this turn
|
|
131
|
-
* depends on. Generous on purpose: it is a backstop against unbounded growth,
|
|
132
|
-
* not a scheduling policy.
|
|
133
|
-
*/
|
|
134
|
-
export const TEST_RUNNER_MAX_PERSISTED_TARGETS = 64;
|
|
135
|
-
/**
|
|
136
|
-
* Union two deferral sets by target identity, keeping the HIGHER attempt count
|
|
137
|
-
* (#2522 review round 3, F3). Two overlapping batches can both be cut on the
|
|
138
|
-
* same target; taking the lower count would let a target trade an attempt for
|
|
139
|
-
* every overlap and never converge on `TEST_RUNNER_MAX_DEFERRALS`.
|
|
140
|
-
*
|
|
141
|
-
* Identity is (session, path), not path alone (#2522 review round 4, I1): two
|
|
142
|
-
* sessions can each owe a run of the same file, and collapsing those into one
|
|
143
|
-
* row makes the surviving row's `sessionId` decide whose deferral is honoured
|
|
144
|
-
* and whose is silently dropped. The path half is keyed through
|
|
145
|
-
* `normalizeMapKey` so `/`- and `\`-separated spellings are one entry
|
|
146
|
-
* (AGENTS.md cross-form-path screen).
|
|
147
|
-
*/
|
|
148
|
-
function deferralEntryKey(entry) {
|
|
149
|
-
return `${entry.sessionId ?? ""}\u0000${normalizeMapKey(path.resolve(entry.testFile))}`;
|
|
150
|
-
}
|
|
151
|
-
function mergeDeferredTargets(existing, incoming) {
|
|
152
|
-
const byKey = new Map();
|
|
153
|
-
for (const entry of [...existing, ...incoming]) {
|
|
154
|
-
const key = deferralEntryKey(entry);
|
|
155
|
-
const prior = byKey.get(key);
|
|
156
|
-
if (prior && (prior.attempts ?? 0) >= (entry.attempts ?? 0))
|
|
157
|
-
continue;
|
|
158
|
-
byKey.set(key, entry);
|
|
159
|
-
}
|
|
160
|
-
return [...byKey.values()];
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Run `run` over `targets` with a concurrency cap, a batch-wide wall budget
|
|
164
|
-
* and an abort-signal race. Reuses `mapWithConcurrency` (the repo's existing
|
|
165
|
-
* worker-pool shape) rather than hand-rolling a second pool.
|
|
166
|
-
*
|
|
167
|
-
* Both bounds are enforced twice on purpose: cooperatively, before each
|
|
168
|
-
* dispatch, so no NEW work starts past the bound; and as a real race against
|
|
169
|
-
* the pool, so an in-flight target cannot hold the batch past its budget.
|
|
170
|
-
* Whatever has settled by then is returned — a partial batch of real results
|
|
171
|
-
* is strictly better than none, and the caller reports the shortfall.
|
|
172
|
-
*/
|
|
173
|
-
export async function runTestTargetsBounded(args) {
|
|
174
|
-
const results = [];
|
|
175
|
-
if (args.targets.length === 0)
|
|
176
|
-
return { results, deferred: [] };
|
|
177
|
-
let stopReason;
|
|
178
|
-
// One controller for the whole batch. Aborting it tree-kills every child the
|
|
179
|
-
// runner has in flight (`safeSpawnAsync` honours the signal it is handed),
|
|
180
|
-
// which is what turns "the bound fired" into "the work actually stopped".
|
|
181
|
-
const batchAbort = new AbortController();
|
|
182
|
-
// Latched at the moment this call returns. Nothing may push into `results`
|
|
183
|
-
// after that: the caller has already consumed the array, and a late push
|
|
184
|
-
// would silently change a batch it has finished reasoning about.
|
|
185
|
-
let closed = false;
|
|
186
|
-
/**
|
|
187
|
-
* #2522 review round 4, I4: "was this target CUT, or did it FINISH?" is a
|
|
188
|
-
* semantic question, and this set is the answer — stamped by the `.then`
|
|
189
|
-
* attached to `run`'s promise AT DISPATCH, which is that promise's FIRST
|
|
190
|
-
* continuation. Nothing but a microtask can run between the promise settling
|
|
191
|
-
* and this stamp, and every path that closes the batch (`setTimeout`, the
|
|
192
|
-
* ambient signal's `abort` event, the pool settling after every mapper has
|
|
193
|
-
* returned) reaches `finish()` from a macrotask or later. So `closed` read
|
|
194
|
-
* here is exactly "the batch was already cut when this value existed".
|
|
195
|
-
*
|
|
196
|
-
* Round 3 read the same flag from the mapper's own `await` continuation,
|
|
197
|
-
* which is several continuations downstream of the settle — a macrotask CAN
|
|
198
|
-
* interleave there — and papered over the resulting misclassification with a
|
|
199
|
-
* `setImmediate` hop plus a `latchedOut` parking array. Both are gone: the
|
|
200
|
-
* hop made the answer depend on queue ordering, which is the thing that kept
|
|
201
|
-
* producing findings (a spawn aborted BEFORE it started resolves
|
|
202
|
-
* synchronously, so the hop folded it back in as a completed run — round 4
|
|
203
|
-
* P3c).
|
|
204
|
-
*/
|
|
205
|
-
const settledBeforeClose = new Set();
|
|
206
|
-
const budgetMs = Math.max(0, args.budgetMs);
|
|
207
|
-
const deadline = Date.now() + budgetMs;
|
|
208
|
-
const shouldStop = () => {
|
|
209
|
-
if (stopReason !== undefined)
|
|
210
|
-
return true;
|
|
211
|
-
if (args.signal?.aborted) {
|
|
212
|
-
stopReason = "abort";
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
if (Date.now() >= deadline) {
|
|
216
|
-
stopReason = "budget";
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
};
|
|
221
|
-
const pool = mapWithConcurrency(args.targets.map((target, index) => ({ target, index })), Math.max(1, args.concurrency), async ({ target, index }) => {
|
|
222
|
-
if (shouldStop())
|
|
223
|
-
return;
|
|
224
|
-
const running = args.run(target, batchAbort.signal);
|
|
225
|
-
// Attached HERE, at dispatch, so it is the promise's first
|
|
226
|
-
// continuation — see `settledBeforeClose`. A value that only exists
|
|
227
|
-
// because the batch was cut (a killed spawn's runner error, or
|
|
228
|
-
// `safeSpawnAsync`'s synchronous "Spawn aborted before start") settles
|
|
229
|
-
// strictly after the close and is therefore never recorded, so the
|
|
230
|
-
// caller defers that target instead of reading it as a completed run.
|
|
231
|
-
const stamp = running.then((value) => {
|
|
232
|
-
if (closed)
|
|
233
|
-
return;
|
|
234
|
-
settledBeforeClose.add(index);
|
|
235
|
-
results.push({ status: "fulfilled", value });
|
|
236
|
-
}, (reason) => {
|
|
237
|
-
if (closed)
|
|
238
|
-
return;
|
|
239
|
-
settledBeforeClose.add(index);
|
|
240
|
-
results.push({ status: "rejected", reason });
|
|
241
|
-
});
|
|
242
|
-
// The stamp swallows both settlements, so this only keeps the pool's
|
|
243
|
-
// concurrency slot held for as long as the work actually runs.
|
|
244
|
-
await stamp;
|
|
245
|
-
});
|
|
246
|
-
// The pool's mapper swallows every throw from an async `run`; a `run` that throws
|
|
247
|
-
// SYNCHRONOUSLY still rejects the pool and closes the whole batch (everything defers,
|
|
248
|
-
// nothing publishes clean). Production runners are async, so this is fail-safe, not reachable.
|
|
249
|
-
void pool.catch(() => { });
|
|
250
|
-
await new Promise((resolve) => {
|
|
251
|
-
let settled = false;
|
|
252
|
-
const finish = (reason) => {
|
|
253
|
-
if (settled)
|
|
254
|
-
return;
|
|
255
|
-
settled = true;
|
|
256
|
-
if (reason !== undefined)
|
|
257
|
-
stopReason ??= reason;
|
|
258
|
-
// Latch BEFORE resolving, so no continuation scheduled by the kill
|
|
259
|
-
// below can slip a result in behind the caller's back.
|
|
260
|
-
closed = true;
|
|
261
|
-
// Kill whatever is still in flight. Without this the pool ran on past
|
|
262
|
-
// the bound: a stuck vitest spawn kept a core busy to its own 60s
|
|
263
|
-
// timeout, long after the turn had moved on.
|
|
264
|
-
batchAbort.abort();
|
|
265
|
-
clearTimeout(timer);
|
|
266
|
-
args.signal?.removeEventListener("abort", onAbort);
|
|
267
|
-
resolve();
|
|
268
|
-
};
|
|
269
|
-
const onAbort = () => finish("abort");
|
|
270
|
-
const timer = setTimeout(() => finish("budget"), budgetMs);
|
|
271
|
-
// A pending batch timer must never hold the process open.
|
|
272
|
-
timer.unref?.();
|
|
273
|
-
if (args.signal?.aborted)
|
|
274
|
-
finish("abort");
|
|
275
|
-
else
|
|
276
|
-
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
277
|
-
void pool.then(() => finish(), () => finish());
|
|
278
|
-
});
|
|
279
|
-
return {
|
|
280
|
-
results,
|
|
281
|
-
deferred: args.targets.filter((_, index) => !settledBeforeClose.has(index)),
|
|
282
|
-
stopReason,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Would writing `next` over `prev` throw away a good scan for a failed one?
|
|
287
|
-
*
|
|
288
|
-
* A failed run carries no findings. Writing it evicts the last good result, and
|
|
289
|
-
* every later reader then serves the failure as the answer — a 194-byte "not
|
|
290
|
-
* available" record replaced 149 KB of real findings in every dogfood project
|
|
291
|
-
* (#925, #1467). Callers keep the previous cache when this returns true.
|
|
292
|
-
*/
|
|
293
|
-
function wouldPoisonCache(prev, next) {
|
|
294
|
-
return !next.success && prev?.data.success === true;
|
|
295
|
-
}
|
|
296
|
-
// LSP idle reset scheduling — prevents thrashing by delaying shutdown
|
|
297
|
-
let lspIdleResetTimeout = null;
|
|
298
|
-
// #1618: set while this timer's fire is deferred behind an in-flight
|
|
299
|
-
// workspace sweep (see `scheduleLSPIdleReset`'s `isWorkspaceSweepActive`
|
|
300
|
-
// branch). `cancelLSPIdleReset` must be able to cancel THIS too — otherwise
|
|
301
|
-
// an active-editing turn that cancels idle reset while a sweep is still
|
|
302
|
-
// running would have it silently resurrected once the sweep finishes, even
|
|
303
|
-
// though the session is no longer idle.
|
|
304
|
-
let pendingSweepRearm = null;
|
|
305
|
-
function emitIdleResetReporterWarning(reportErr) {
|
|
306
|
-
try {
|
|
307
|
-
process.emitWarning(`pi-lens LSP idle reset error reporter failed: ${reportErr}`, { code: "PI_LENS_LSP_IDLE_RESET_REPORTER_FAILED" });
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
// Preserve the detached-timer invariant: this path must never crash.
|
|
311
|
-
void reportErr;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
function reportIdleResetError(onError, err) {
|
|
315
|
-
try {
|
|
316
|
-
onError?.(err);
|
|
317
|
-
}
|
|
318
|
-
catch (reportErr) {
|
|
319
|
-
emitIdleResetReporterWarning(reportErr);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
function scheduleLSPIdleReset(resetFn, delayMs, options = {}) {
|
|
323
|
-
// Clear any pending reset to avoid multiple timers. #1618: also cancel a
|
|
324
|
-
// rearm still waiting on a prior sweep's hold — otherwise re-scheduling
|
|
325
|
-
// here (this call) leaves that OLD waiter armed too, and the sweep's
|
|
326
|
-
// eventual release would fire a SECOND, independent `scheduleLSPIdleReset`
|
|
327
|
-
// alongside this fresh one.
|
|
328
|
-
if (lspIdleResetTimeout) {
|
|
329
|
-
clearTimeout(lspIdleResetTimeout);
|
|
330
|
-
}
|
|
331
|
-
if (pendingSweepRearm) {
|
|
332
|
-
pendingSweepRearm.cancelled = true;
|
|
333
|
-
pendingSweepRearm = null;
|
|
334
|
-
}
|
|
335
|
-
lspIdleResetTimeout = setTimeout(() => {
|
|
336
|
-
lspIdleResetTimeout = null;
|
|
337
|
-
// #1618: a full workspace sweep (`lens_diagnostics mode=full`) grants
|
|
338
|
-
// itself a wall-clock ceiling that can outlive this timer's delay — this
|
|
339
|
-
// used to fire straight into an in-flight sweep and destroy the very
|
|
340
|
-
// service the sweep was actively touching, mislabeling every file the
|
|
341
|
-
// sweep had not yet reached as budget exhaustion. Defer instead of
|
|
342
|
-
// firing: re-arm a FRESH `delayMs` timer once the sweep releases its
|
|
343
|
-
// hold, rather than resuming a countdown that's already elapsed (which
|
|
344
|
-
// would fire the instant the hold releases) or destroying mid-sweep.
|
|
345
|
-
if (isWorkspaceSweepActive()) {
|
|
346
|
-
const rearmToken = { cancelled: false };
|
|
347
|
-
pendingSweepRearm = rearmToken;
|
|
348
|
-
runWhenWorkspaceSweepIdle(() => {
|
|
349
|
-
if (rearmToken.cancelled)
|
|
350
|
-
return;
|
|
351
|
-
if (pendingSweepRearm === rearmToken)
|
|
352
|
-
pendingSweepRearm = null;
|
|
353
|
-
scheduleLSPIdleReset(resetFn, delayMs, options);
|
|
354
|
-
});
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
try {
|
|
358
|
-
if (options.isCurrentSession && !options.isCurrentSession()) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (options.isPrimarySession && !options.isPrimarySession()) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
resetFn();
|
|
365
|
-
}
|
|
366
|
-
catch (err) {
|
|
367
|
-
// Detached timers run outside a pi event boundary. They must never crash
|
|
368
|
-
// the extension process (for example if a host UI object was invalidated
|
|
369
|
-
// by session replacement before the timer fired).
|
|
370
|
-
reportIdleResetError(options.onError, err);
|
|
371
|
-
}
|
|
372
|
-
}, delayMs);
|
|
373
|
-
// unref so this timer does not prevent the process from exiting naturally
|
|
374
|
-
// (critical for subagent / --mode json -p usage where the process should
|
|
375
|
-
// exit after completing its work, not wait 240 seconds for this to fire)
|
|
376
|
-
lspIdleResetTimeout.unref();
|
|
377
|
-
}
|
|
378
|
-
// #1618 acceptance criterion 6: FULL_SCAN_WALL_CLOCK_MS (the full-sweep wall
|
|
379
|
-
// clock ceiling, `tools/lens-diagnostics.ts`) must stay under EVERY idle
|
|
380
|
-
// reset delay this module can arm — derived, not asserted, so the constants
|
|
381
|
-
// can't drift back into a relationship where a still-running sweep can
|
|
382
|
-
// outlive the timer. The AC1 hold above already makes a mid-sweep fire
|
|
383
|
-
// impossible regardless of this margin; this is defense in depth against a
|
|
384
|
-
// future caller that touches the LSP service outside
|
|
385
|
-
// `runWorkspaceDiagnostics`' hold. `SWEEP_IDLE_SAFETY_MARGIN_MS` is
|
|
386
|
-
// single-sourced from `workspace-sweep-hold.ts`, which also uses it for its
|
|
387
|
-
// own max-hold-age failsafe — one tunable, not two.
|
|
388
|
-
const DEFAULT_LSP_IDLE_RESET_MS = 240_000;
|
|
389
|
-
function sweepDerivedFloorMs() {
|
|
390
|
-
return getFullScanWallClockMs() + SWEEP_IDLE_SAFETY_MARGIN_MS;
|
|
391
|
-
}
|
|
392
|
-
/** The normal (non-subagent, non-budget-pressured) idle-reset delay. */
|
|
393
|
-
function getBaseLspIdleResetMs() {
|
|
394
|
-
return Math.max(DEFAULT_LSP_IDLE_RESET_MS, sweepDerivedFloorMs());
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* #1618 (R4): the subagent-light (#713) and cross-process-budget-pressured
|
|
398
|
-
* (#449) paths used to arm a flat, much SHORTER delay (60s default) than the
|
|
399
|
-
* sweep's own 300s ceiling — a 5:1 inversion covered only by the AC1 hold.
|
|
400
|
-
* Deriving this path too means AC6 ("the sweep's ceiling stays under every
|
|
401
|
-
* idle-reset delay") holds universally, not just for the common path, and an
|
|
402
|
-
* env override to either constant can never invert it (`Math.max` floors at
|
|
403
|
-
* the derived value no matter how small the override pushes the other side).
|
|
404
|
-
*
|
|
405
|
-
* Accepted cost (deliberate, not incidental — see R6 in the PR body): under
|
|
406
|
-
* default settings this now ALSO arms the ~360s derived floor rather than a
|
|
407
|
-
* true 60s teardown, trading some of #713's "release a short-lived
|
|
408
|
-
* subagent's fleet fast" benefit for AC6 holding without exceptions.
|
|
409
|
-
*/
|
|
410
|
-
function getShortenedLspIdleResetMs() {
|
|
411
|
-
return Math.max(getLspBudgetIdleTimeoutMs(), sweepDerivedFloorMs());
|
|
412
|
-
}
|
|
413
|
-
/** The idle-reset delay `handleTurnEnd` actually arms on a file-less turn —
|
|
414
|
-
* exported so tests assert against the REAL computed value instead of a
|
|
415
|
-
* hand-derived literal that can silently drift from this function. */
|
|
416
|
-
export function getEffectiveLspIdleResetMs() {
|
|
417
|
-
return isSubagentSession() || shouldShortenLspIdleTimeout()
|
|
418
|
-
? getShortenedLspIdleResetMs()
|
|
419
|
-
: getBaseLspIdleResetMs();
|
|
420
|
-
}
|
|
421
|
-
export function cancelLSPIdleReset() {
|
|
422
|
-
if (lspIdleResetTimeout) {
|
|
423
|
-
clearTimeout(lspIdleResetTimeout);
|
|
424
|
-
lspIdleResetTimeout = null;
|
|
425
|
-
}
|
|
426
|
-
if (pendingSweepRearm) {
|
|
427
|
-
pendingSweepRearm.cancelled = true;
|
|
428
|
-
pendingSweepRearm = null;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
function capTurnEndMessage(content) {
|
|
432
|
-
const maxLines = RUNTIME_CONFIG.turnEnd.maxLines;
|
|
433
|
-
const maxChars = RUNTIME_CONFIG.turnEnd.maxChars;
|
|
434
|
-
let out = content;
|
|
435
|
-
const lines = out.split("\n");
|
|
436
|
-
if (lines.length > maxLines) {
|
|
437
|
-
out = `${lines.slice(0, maxLines).join("\n")}\n... (truncated)`;
|
|
438
|
-
}
|
|
439
|
-
if (out.length > maxChars) {
|
|
440
|
-
out = `${out.slice(0, maxChars)}\n... (truncated)`;
|
|
441
|
-
}
|
|
442
|
-
return out;
|
|
443
|
-
}
|
|
444
|
-
export async function handleTurnEnd(deps) {
|
|
445
|
-
const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, host = "pi", owner, resetLSPService, resetFormatService, } = deps;
|
|
446
|
-
const turnIndexAtDispatch = runtime.turnIndex;
|
|
447
|
-
const clearOwnedTurnState = () => {
|
|
448
|
-
if (runtime.turnIndex !== turnIndexAtDispatch) {
|
|
449
|
-
dbg(`turn_end: retaining newer turn state (dispatch=${turnIndexAtDispatch}, current=${runtime.turnIndex})`);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
cacheManager.clearTurnState(cwd, currentOwner);
|
|
453
|
-
};
|
|
454
|
-
// #449 slice 1: piggyback the instance-registry heartbeat on this existing
|
|
455
|
-
// per-turn touchpoint rather than adding a new timer/interval. Cheap (reads
|
|
456
|
-
// process.memoryUsage().rss, one read-modify-write of instances.json) and
|
|
457
|
-
// fire-and-forget — the kill-switch check + no-op behavior live inside
|
|
458
|
-
// updateHeartbeat itself, so this call site doesn't need to know about it.
|
|
459
|
-
//
|
|
460
|
-
// #620: intentionally RSS-only here — CPU%/LSP-child sampling (which shells
|
|
461
|
-
// out to `pidusage`, and a full CIM query on Windows for a spawn's process
|
|
462
|
-
// tree) is left to the quiet-window "instance_registry_heartbeat" task
|
|
463
|
-
// (clients/quiet-window.ts's `buildHeartbeatResourcePatch`), which fires on
|
|
464
|
-
// the idle `agent_settled` window rather than every single turn end. Every
|
|
465
|
-
// turn end is a much hotter path than an idle window, and the issue's own
|
|
466
|
-
// guardrail is not to let the measurement itself become a new source of
|
|
467
|
-
// per-turn overhead worth investigating.
|
|
468
|
-
void updateHeartbeat().catch(() => {
|
|
469
|
-
// best-effort observability — never fail turn_end over this
|
|
470
|
-
});
|
|
471
|
-
const cwd = ctxCwd ?? process.cwd();
|
|
472
|
-
let turnState = cacheManager.readTurnState(cwd);
|
|
473
|
-
// A live foreign writer owns this worklist. Do not clear or consume another
|
|
474
|
-
// pi/MCP session's files; a dead/aged owner is safely evicted instead.
|
|
475
|
-
// #2504: `sessionStartedAt` dates the persisted worklist against THIS
|
|
476
|
-
// session. Without it an ownerless turn-state.json — the resting shape
|
|
477
|
-
// before #2504 — read back as "owned" no matter how old it was.
|
|
478
|
-
const currentOwner = {
|
|
479
|
-
...(owner ?? {
|
|
480
|
-
kind: "pi",
|
|
481
|
-
id: runtime.telemetrySessionId,
|
|
482
|
-
pid: process.pid,
|
|
483
|
-
lastSeen: new Date().toISOString(),
|
|
484
|
-
}),
|
|
485
|
-
sessionStartedAt: owner?.sessionStartedAt ?? runtime.sessionStartedAt,
|
|
486
|
-
};
|
|
487
|
-
const access = cacheManager.getTurnStateAccess(cwd, currentOwner);
|
|
488
|
-
// Captured BEFORE the eviction below rewrites the file: the owner the gate
|
|
489
|
-
// actually judged. This pair is what would have settled #2504 from the
|
|
490
|
-
// debug log alone.
|
|
491
|
-
const gateOwnerLabel = turnState.owner
|
|
492
|
-
? `${turnState.owner.kind}:${turnState.owner.id}`
|
|
493
|
-
: turnState.sessionId
|
|
494
|
-
? `legacy:${turnState.sessionId}`
|
|
495
|
-
: "none";
|
|
496
|
-
const sameProcessPiSessionHandoff = access === "foreign-live" &&
|
|
497
|
-
currentOwner.kind === "pi" &&
|
|
498
|
-
turnState.owner?.kind === "pi" &&
|
|
499
|
-
turnState.owner.pid === process.pid &&
|
|
500
|
-
turnState.owner.id !== currentOwner.id;
|
|
501
|
-
if (access === "foreign-live" && !sameProcessPiSessionHandoff) {
|
|
502
|
-
dbg(`turn_end: foreign live owner retained (${turnState.owner?.kind ?? "legacy"}:${turnState.owner?.id ?? turnState.sessionId})`);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
if (access === "available" &&
|
|
506
|
-
(turnState.files || turnState.owner || turnState.sessionId)) {
|
|
507
|
-
dbg("turn_end: evicting stale turn-state owner");
|
|
508
|
-
clearOwnedTurnState();
|
|
509
|
-
turnState = cacheManager.readTurnState(cwd);
|
|
510
|
-
}
|
|
511
|
-
const files = Object.keys(turnState.files);
|
|
512
|
-
/**
|
|
513
|
-
* #2275: widget-footer sibling of #1950's inline-blocker cap, for the
|
|
514
|
-
* widget store's OWN dependency-drift demotion
|
|
515
|
-
* (`markWidgetFileBlockersStale`, driven by the freshness sweep further
|
|
516
|
-
* down this function) — a completely separate store from
|
|
517
|
-
* `RuntimeCoordinator`'s inline-blocker map, so it needed its own
|
|
518
|
-
* delivery count (`WidgetDiagnostic.staleDeliveryCount`) rather than
|
|
519
|
-
* inheriting one.
|
|
520
|
-
*
|
|
521
|
-
* Review F1: the population is what the footer RENDERED since the last
|
|
522
|
-
* turn end, drained here — not every file that merely holds a demoted
|
|
523
|
-
* row. The footer draws one record per pass (`withBlocking[0]`, its top
|
|
524
|
-
* five entries) and may not be drawn at all, so a per-turn walk of the
|
|
525
|
-
* whole store charged deliveries the agent never received and retired a
|
|
526
|
-
* delivery early. This is the widget-surface analogue of the inline
|
|
527
|
-
* loop's own `pendingDependencyDriftDeliveries` deferral below: both
|
|
528
|
-
* commit a delivery only once the surface has actually served it. Every
|
|
529
|
-
* `deliveryCount` reported to the ledger is therefore a count of RENDERS.
|
|
530
|
-
*
|
|
531
|
-
* Fix-round 3 (#2275 review F1): this drain/charge MUST run before the
|
|
532
|
-
* `files.length === 0` early return below — a read-only turn (no
|
|
533
|
-
* modified files) still repaints the footer and can draw a demoted row,
|
|
534
|
-
* so a cap that only charged deliveries below the early return silently
|
|
535
|
-
* starved on quiet turns: the footer re-rendered the same demoted row
|
|
536
|
-
* every turn while the delivery count never advanced. The drain is a
|
|
537
|
-
* Set.take() plus one map lookup per drained file — cheap enough to run
|
|
538
|
-
* unconditionally on every turn end.
|
|
539
|
-
*/
|
|
540
|
-
let widgetDemotedFindingsRetired = 0;
|
|
541
|
-
for (const wPath of drainRenderedDependencyDriftFilePaths()) {
|
|
542
|
-
const deliveryCount = incrementWidgetDependencyDriftDelivery(wPath);
|
|
543
|
-
if (deliveryCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
|
|
544
|
-
const capRetired = retireWidgetDependencyDriftBlockers(wPath);
|
|
545
|
-
if (capRetired) {
|
|
546
|
-
widgetDemotedFindingsRetired += 1;
|
|
547
|
-
incrementDegradationCount({
|
|
548
|
-
kind: "demoted-finding-retired",
|
|
549
|
-
subject: `widget-blocker:${toRunnerDisplayPath(cwd, wPath)}`,
|
|
550
|
-
reason: `capped after ${deliveryCount} deliveries with no re-run; hidden from the pi-lens footer, still listed by lens_diagnostics mode=all — re-run can still confirm`,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
// R1 (#1443 follow-up): a read-only turn (no files touched) must not take
|
|
556
|
-
// the fast idle-reset path while a carried cascade run — or one still
|
|
557
|
-
// settling — is waiting for its delivery opportunity. Falling through to
|
|
558
|
-
// the normal pipeline lets the settle/drain/merge logic below run exactly
|
|
559
|
-
// as it does for an edit turn, so a carried finding reaches the agent
|
|
560
|
-
// instead of dying unrendered. `hasCascadeRuns()` is a cheap peek (no
|
|
561
|
-
// pending work almost every turn), so the common read-only turn still
|
|
562
|
-
// takes the early return below.
|
|
563
|
-
// A foreign live owner must not deliver another session's pending findings.
|
|
564
|
-
// A no-file turn falls through when this process has pending runner work so
|
|
565
|
-
// the ordinary freshness gate and delivery cache can run. Max-cycle cleanup
|
|
566
|
-
// below intentionally remains a terminal reset; its pending work stays in
|
|
567
|
-
// the bounded handoff store for the next eligible turn.
|
|
568
|
-
if (files.length === 0 && !runtime.hasCascadeRuns()) {
|
|
569
|
-
// A genuinely clean session must invalidate the persisted guard record.
|
|
570
|
-
// Blocker records are retained only while the runtime still reports one.
|
|
571
|
-
if (getFlag("lens-guard") && !runtime.gitGuardHasBlockers) {
|
|
572
|
-
const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
573
|
-
if (guardRecord?.sessionId === runtime.telemetrySessionId &&
|
|
574
|
-
guardRecord.testFailures !== true) {
|
|
575
|
-
cacheManager.clearCache("turn-end-findings", cwd);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
// #713: subagent sessions use a shorter idle reset (nominally 60s) — a
|
|
579
|
-
// short-lived task agent holding a warm fleet for 4 minutes after its
|
|
580
|
-
// last turn is pure waste under fan-out. Classify ONCE here so every
|
|
581
|
-
// tick in this call path shares the same answer. PI_LENS_SUBAGENT_FULL=1
|
|
582
|
-
// restores the base delay via isSubagentSession() returning false.
|
|
583
|
-
// #1618: both branches route through `getEffectiveLspIdleResetMs` so
|
|
584
|
-
// AC6's derivation applies universally — see that function's doc for
|
|
585
|
-
// why the "shorter" path is not always literally 60s anymore.
|
|
586
|
-
const idleResetMs = getEffectiveLspIdleResetMs();
|
|
587
|
-
dbg(`turn_end: no modified files, scheduling LSP idle reset (${idleResetMs / 1000}s)`);
|
|
588
|
-
if (!getFlag("no-lsp")) {
|
|
589
|
-
const sessionGeneration = runtime.sessionGeneration;
|
|
590
|
-
scheduleLSPIdleReset(resetLSPService, idleResetMs, {
|
|
591
|
-
isCurrentSession: () => runtime.isCurrentSession(sessionGeneration),
|
|
592
|
-
// #2157 fix round 2: a secondary (subagent) evaluation's own timer
|
|
593
|
-
// must not release the primary's shared fleet — see the option's
|
|
594
|
-
// doc comment on `scheduleLSPIdleReset`.
|
|
595
|
-
isPrimarySession: () => {
|
|
596
|
-
const activePrimarySessionId = getActiveSessionId();
|
|
597
|
-
return (activePrimarySessionId === undefined ||
|
|
598
|
-
activePrimarySessionId === runtime.telemetrySessionId);
|
|
599
|
-
},
|
|
600
|
-
onError: (err) => dbg(`lsp idle reset failed: ${err}`),
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
resetFormatService();
|
|
604
|
-
if (pendingRunnerFindingsSize() === 0)
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
// Cancel any pending idle reset since we're actively working. #1618: also
|
|
608
|
-
// checks `pendingSweepRearm` — a timer deferred behind an in-flight
|
|
609
|
-
// workspace sweep already nulled `lspIdleResetTimeout` (the setTimeout
|
|
610
|
-
// callback clears it before checking the hold), so this guard used to
|
|
611
|
-
// read "nothing pending" and skip the cancel while a rearm was still
|
|
612
|
-
// queued to fire the instant the sweep released its hold — resurrecting
|
|
613
|
-
// idle reset on a session that had since gone back to active editing.
|
|
614
|
-
if (files.length > 0 && (lspIdleResetTimeout || pendingSweepRearm)) {
|
|
615
|
-
cancelLSPIdleReset();
|
|
616
|
-
dbg("turn_end: cancelled pending LSP idle reset (active editing)");
|
|
617
|
-
}
|
|
618
|
-
dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}, access: ${access}, owner: ${gateOwnerLabel}`);
|
|
619
|
-
if (cacheManager.isMaxCyclesExceeded(cwd)) {
|
|
620
|
-
dbg("turn_end: max cycles exceeded, clearing state and forcing through");
|
|
621
|
-
clearOwnedTurnState();
|
|
622
|
-
runtime.fixedThisTurn.clear();
|
|
623
|
-
resetFormatService();
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
const turnEndStart = Date.now();
|
|
627
|
-
const blockerParts = [];
|
|
628
|
-
/**
|
|
629
|
-
* #1622 review M2: findings the freshness gate demoted. A third tier between
|
|
630
|
-
* blockers and advisories — not a blocker, because the cached coordinate is
|
|
631
|
-
* untrustworthy; not an advisory, because the advisory label reads "no action
|
|
632
|
-
* required this turn" and these DO require a re-scan. Each part carries its
|
|
633
|
-
* own imperative preamble rather than inheriting that label.
|
|
634
|
-
*/
|
|
635
|
-
const staleSecretParts = [];
|
|
636
|
-
const advisoryParts = [];
|
|
637
|
-
const projectDiagnosticsDelta = [];
|
|
638
|
-
const projectDiagnosticsSources = new Set();
|
|
639
|
-
// #1641: past-EOF gate. Runs BEFORE the dependency-drift sweep below — a
|
|
640
|
-
// cheap statSync per cited file is worth paying first so the pricier
|
|
641
|
-
// import-parsing sweep can skip anything already taken out of the
|
|
642
|
-
// authoritative channel this turn (see blocker-past-eof.ts's module doc
|
|
643
|
-
// for the full composition rule with #1631's gate).
|
|
644
|
-
const blockerPastEofStart = Date.now();
|
|
645
|
-
const blockerPastEof = sweepInlineBlockerPastEof(runtime, cwd);
|
|
646
|
-
logLatency({
|
|
647
|
-
type: "phase",
|
|
648
|
-
toolName: "turn_end",
|
|
649
|
-
filePath: cwd,
|
|
650
|
-
phase: "blocker_past_eof_sweep",
|
|
651
|
-
durationMs: Date.now() - blockerPastEofStart,
|
|
652
|
-
metadata: {
|
|
653
|
-
total: blockerPastEof.total,
|
|
654
|
-
checked: blockerPastEof.checked,
|
|
655
|
-
demoted: blockerPastEof.demoted,
|
|
656
|
-
// #1944 review F3: `healed` is gone. Retirement makes the falling edge
|
|
657
|
-
// unreachable on this store, so the field could only ever log zero —
|
|
658
|
-
// see `BlockerPastEofCounts`.
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
// #1631: freshness gate. A cached blocker is a verdict about the file AND
|
|
662
|
-
// everything it imports; before re-serving it, sweep for out-of-band drift of
|
|
663
|
-
// the file or its forward imports and demote drifted entries to a
|
|
664
|
-
// `[stale — re-run to confirm]` advisory instead of re-asserting them at full
|
|
665
|
-
// authority (#1419 demote-not-drop).
|
|
666
|
-
const blockerFreshnessStart = Date.now();
|
|
667
|
-
// #1790: widen the sweep's population with widget-store rows a cache-served
|
|
668
|
-
// replay populated without ever touching RuntimeCoordinator's inline-blocker
|
|
669
|
-
// map — see blocker-freshness.ts's `WidgetSweepBlockerEntry` doc for why this
|
|
670
|
-
// is injected here rather than imported by blocker-freshness.ts itself.
|
|
671
|
-
const blockerFreshness = await sweepInlineBlockerFreshness(runtime, cwd, {
|
|
672
|
-
// #2982: the hook's own signal, so the self axis's filesystem work is
|
|
673
|
-
// bounded by the same abort everything else in this handler honours.
|
|
674
|
-
...(deps.signal === undefined ? {} : { signal: deps.signal }),
|
|
675
|
-
additionalEntries: getWidgetBlockingFilesForSweep().map((row) => ({
|
|
676
|
-
filePath: row.filePath,
|
|
677
|
-
recordedAtMs: row.recordedAtMs,
|
|
678
|
-
demote: () => markWidgetFileBlockersStale(row.filePath, "dependency-drift"),
|
|
679
|
-
})),
|
|
680
|
-
});
|
|
681
|
-
logLatency({
|
|
682
|
-
type: "phase",
|
|
683
|
-
toolName: "turn_end",
|
|
684
|
-
filePath: cwd,
|
|
685
|
-
phase: "blocker_freshness_sweep",
|
|
686
|
-
durationMs: Date.now() - blockerFreshnessStart,
|
|
687
|
-
metadata: {
|
|
688
|
-
total: blockerFreshness.total,
|
|
689
|
-
kept: blockerFreshness.kept,
|
|
690
|
-
revalidated: blockerFreshness.revalidated,
|
|
691
|
-
alreadyStale: blockerFreshness.alreadyStale,
|
|
692
|
-
truncatedImports: blockerFreshness.truncatedImports,
|
|
693
|
-
selfHealed: blockerFreshness.selfHealed,
|
|
694
|
-
selfUnverifiable: blockerFreshness.selfUnverifiable,
|
|
695
|
-
hashBudgetExhausted: blockerFreshness.hashBudgetExhausted,
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
// Re-surface inline blockers from this turn that the agent didn't fix.
|
|
699
|
-
// These were shown inline during write/edit but the agent moved on without resolving them.
|
|
700
|
-
const unresolvedBlockers = runtime.getInlineBlockersSnapshot();
|
|
701
|
-
/** #1944/#1950: demotions retired after their delivery limit. */
|
|
702
|
-
let demotedFindingsRetired = 0;
|
|
703
|
-
/**
|
|
704
|
-
* #1950 fix-round F1: dependency-drift delivery-count commits, deferred
|
|
705
|
-
* until this turn's content is confirmed NOT suppressed by the
|
|
706
|
-
* `turn-end-findings-last` signature dedupe further down. That dedupe
|
|
707
|
-
* silences a turn whose rendered content is byte-identical to the last
|
|
708
|
-
* one actually delivered — the agent never sees a suppressed turn, so
|
|
709
|
-
* committing the counter for it would count a delivery that didn't
|
|
710
|
-
* happen. Each entry here is invoked only from the "not suppressed"
|
|
711
|
-
* branch below.
|
|
712
|
-
*/
|
|
713
|
-
const pendingDependencyDriftDeliveries = [];
|
|
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;
|
|
725
|
-
const displayPath = toRunnerDisplayPath(cwd, bPath);
|
|
726
|
-
const tally = { displayPath, sources: record.sources };
|
|
727
|
-
if (stale) {
|
|
728
|
-
inlinePolicyEntries.push({ ...tally, stale: true });
|
|
729
|
-
// #1631: demoted — out of the authoritative blocker channel and into the
|
|
730
|
-
// advisory channel with a stale marker, so the agent is told to re-run
|
|
731
|
-
// rather than pressured by a verdict that may already be resolved.
|
|
732
|
-
//
|
|
733
|
-
// #1944: the CHANNEL change is not enough. Until this call the advisory
|
|
734
|
-
// embedded the blocker body verbatim, so the agent read "🔴 STOP — 11
|
|
735
|
-
// issue(s) must be fixed" with dead line numbers under a hedge line it
|
|
736
|
-
// ignored. Degrade the body itself, and — when the file shrank past the
|
|
737
|
-
// cited lines, so no re-run can ever confirm it — retire the record
|
|
738
|
-
// after this ONE delivery instead of re-serving it for the rest of the
|
|
739
|
-
// session.
|
|
740
|
-
const deadLines = blockerPastEof.deadLinesByPath.get(bPath) ?? [];
|
|
741
|
-
const degraded = degradeDemotedFindingBody(summary, { deadLines });
|
|
742
|
-
const retired = runtime.retireDemotedPastEofBlocker(bPath, deadLines);
|
|
743
|
-
let retirementNote;
|
|
744
|
-
if (retired) {
|
|
745
|
-
demotedFindingsRetired += 1;
|
|
746
|
-
// Bounded by the ledger's own per-kind/subject tally, and the subject
|
|
747
|
-
// keeps the discriminating identity (which store, which file).
|
|
748
|
-
incrementDegradationCount({
|
|
749
|
-
kind: "demoted-finding-retired",
|
|
750
|
-
subject: `inline-blocker:${displayPath}`,
|
|
751
|
-
reason: `file shrank past cited line(s) ${deadLines.join(", ")}; retired after one degraded delivery`,
|
|
752
|
-
});
|
|
753
|
-
retirementNote = formatRetirementNote(deadLines);
|
|
754
|
-
}
|
|
755
|
-
else if (staleReason === "dependency-drift") {
|
|
756
|
-
// #1950: a dependency-drift demotion is recoverable (its coordinates
|
|
757
|
-
// are still in bounds), so it does NOT retire after one delivery like
|
|
758
|
-
// the past-EOF case above — but nothing capped how many times the
|
|
759
|
-
// SAME demoted-but-unconfirmed record re-serves, and incident data
|
|
760
|
-
// showed repeat deliveries carrying near-zero information after the
|
|
761
|
-
// first. Cap it at DEPENDENCY_DRIFT_MAX_DELIVERIES instead.
|
|
762
|
-
//
|
|
763
|
-
// The count driving THIS render is a peek (fix-round F1): the actual
|
|
764
|
-
// increment is deferred to `pendingDependencyDriftDeliveries` below,
|
|
765
|
-
// committed only once this turn's content is known to reach the
|
|
766
|
-
// agent, so a suppressed turn's tentative render never advances the
|
|
767
|
-
// stored count.
|
|
768
|
-
const tentativeCount = runtime.peekInlineBlockerStaleDeliveryCount(bPath) + 1;
|
|
769
|
-
if (tentativeCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
|
|
770
|
-
retirementNote = formatDeliveryCapNote(tentativeCount);
|
|
771
|
-
}
|
|
772
|
-
pendingDependencyDriftDeliveries.push(() => {
|
|
773
|
-
const deliveryCount = runtime.incrementInlineBlockerStaleDelivery(bPath);
|
|
774
|
-
if (deliveryCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
|
|
775
|
-
const capRetired = runtime.retireDemotedDependencyDriftBlocker(bPath);
|
|
776
|
-
if (capRetired) {
|
|
777
|
-
demotedFindingsRetired += 1;
|
|
778
|
-
incrementDegradationCount({
|
|
779
|
-
kind: "demoted-finding-retired",
|
|
780
|
-
subject: `inline-blocker:${displayPath}`,
|
|
781
|
-
reason: `capped after ${deliveryCount} deliveries with no re-run; re-run can still confirm`,
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
// @delivery-surface: runtime-turn:unresolved-inline-blocker
|
|
788
|
-
advisoryParts.push(`${STALE_LINE_MARKER} ${displayPath}:\n${degraded.body}` +
|
|
789
|
-
(retirementNote ? `\n${retirementNote}` : ""));
|
|
790
|
-
}
|
|
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
|
-
: "";
|
|
813
|
-
// @delivery-surface: runtime-turn:unresolved-inline-blocker
|
|
814
|
-
blockerParts.push(`Unresolved from this turn — ${displayPath}${suppressedNote}:\n${policy.body}`);
|
|
815
|
-
}
|
|
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
|
-
}
|
|
847
|
-
// Drain the deferred cascade computes kicked off this turn (#450). They ran
|
|
848
|
-
// concurrently off the write hot path; wait a bounded time for them here so
|
|
849
|
-
// their runs are available to the merge below. A compute still in flight at
|
|
850
|
-
// the cap is carried over to the next turn_end (never dropped).
|
|
851
|
-
const cascadeSettleStart = Date.now();
|
|
852
|
-
const { settled, timedOut } = await runtime.settleCascadeRuns(cascadeSettleWaitMs(), { trackTurnEndClock: true });
|
|
853
|
-
logLatency({
|
|
854
|
-
type: "phase",
|
|
855
|
-
toolName: "turn_end",
|
|
856
|
-
filePath: cwd,
|
|
857
|
-
phase: "cascade_settle_wait",
|
|
858
|
-
durationMs: Date.now() - cascadeSettleStart,
|
|
859
|
-
metadata: { settled, timedOut },
|
|
860
|
-
});
|
|
861
|
-
// Merge accumulated cascade results from all pipeline runs this turn.
|
|
862
|
-
// Two-pass dedup:
|
|
863
|
-
// 1. Primary-level: dedup by primary file (last writer wins).
|
|
864
|
-
// 2. Neighbor-level: each neighbor is claimed by the latest cascade result
|
|
865
|
-
// that covers it — suppresses stale neighbor state from earlier writes.
|
|
866
|
-
const t0 = Date.now();
|
|
867
|
-
const cascadeRuns = runtime.consumeCascadeRuns().filter((run) => {
|
|
868
|
-
const originSeq = run.origin?.projectSeq;
|
|
869
|
-
const originTurn = run.origin?.turnSeq;
|
|
870
|
-
// A deferred result from AFTER a later write is not current state. Old test
|
|
871
|
-
// fixtures without provenance remain accepted for compatibility.
|
|
872
|
-
//
|
|
873
|
-
// #1443: `turnSeq` alone is NOT a supersede signal, and it used to be an
|
|
874
|
-
// unconditional reject. Every LATE run — one whose compute missed the
|
|
875
|
-
// settle cap and was re-parked by `settleCascadeRuns`, and one the
|
|
876
|
-
// quiet-window reconcile appended after this turn's predecessor already
|
|
877
|
-
// consumed (carried across turn_start by `beginTurn`) — is BY DEFINITION
|
|
878
|
-
// from an earlier turn, so `originTurn === runtime.turnIndex` was always
|
|
879
|
-
// false for exactly the runs the carry-over was built to preserve. Both
|
|
880
|
-
// producers' contracts were dead code: the measured cases were the two
|
|
881
|
-
// highest-fan-out cascades of the day (38 and 40 neighbours).
|
|
882
|
-
//
|
|
883
|
-
// R2 (#1443 follow-up): `projectSeq` alone is NOT a per-file supersede
|
|
884
|
-
// signal — it is GLOBAL, advancing on every pi-observed write anywhere in
|
|
885
|
-
// the project. Rejecting on any mismatch meant an edit to an unrelated
|
|
886
|
-
// file superseded a run that had nothing to do with it, reintroducing the
|
|
887
|
-
// exact 38/40-neighbour loss #1443 was written to fix, one filter down.
|
|
888
|
-
// `getFilesChangedSince` (#451) is the honest per-file signal: a run is
|
|
889
|
-
// superseded only if its own primary file or one of its neighbours was
|
|
890
|
-
// actually rewritten since it launched. A late-but-not-superseded run is
|
|
891
|
-
// surfaced; a superseded one is dropped with a RECORD (never silently),
|
|
892
|
-
// so the loss stays countable.
|
|
893
|
-
if (originSeq !== undefined) {
|
|
894
|
-
const changedSince = runtime.getFilesChangedSince(originSeq);
|
|
895
|
-
if (changedSince.length > 0) {
|
|
896
|
-
const changedSet = new Set(changedSince);
|
|
897
|
-
const primaryKey = normalizeMapKey(path.resolve(run.filePath));
|
|
898
|
-
const neighborKeys = [
|
|
899
|
-
...(run.result?.neighbors ?? []).map((n) => n.filePath),
|
|
900
|
-
...(run.selectedNeighborPaths ?? []),
|
|
901
|
-
].map((filePath) => normalizeMapKey(path.resolve(filePath)));
|
|
902
|
-
const supersededByOwnFile = changedSet.has(primaryKey) ||
|
|
903
|
-
neighborKeys.some((k) => changedSet.has(k));
|
|
904
|
-
if (supersededByOwnFile) {
|
|
905
|
-
logCascade({
|
|
906
|
-
phase: "cascade_carry_over_drop",
|
|
907
|
-
filePath: run.filePath,
|
|
908
|
-
neighborCount: run.neighborCount,
|
|
909
|
-
diagnosticCount: run.diagnosticCount,
|
|
910
|
-
reason: "superseded_by_later_write",
|
|
911
|
-
metadata: {
|
|
912
|
-
originProjectSeq: originSeq,
|
|
913
|
-
projectSeq: runtime.projectSeq,
|
|
914
|
-
originTurnSeq: originTurn,
|
|
915
|
-
turnIndex: runtime.turnIndex,
|
|
916
|
-
carriedTurns: run.carriedTurns,
|
|
917
|
-
changedFiles: changedSince,
|
|
918
|
-
},
|
|
919
|
-
});
|
|
920
|
-
return false;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
return true;
|
|
925
|
-
});
|
|
926
|
-
const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
|
|
927
|
-
// Fix B (#3167): which results belong to a CARRIED run — the label is
|
|
928
|
-
// per-result at render time, so the run→result pairing must survive the
|
|
929
|
-
// flatMap above (which discards the wrapper).
|
|
930
|
-
// Fix B (#3168 F6): the success-path record — latency.log could not show
|
|
931
|
-
// the feature ever rendered without it (the only phases were the drop/
|
|
932
|
-
// settle records). One bounded record per turn, emitted after both render
|
|
933
|
-
// sections below.
|
|
934
|
-
let carriedRunsRendered = 0;
|
|
935
|
-
let labeledAdvisories = 0;
|
|
936
|
-
const carriedMetaByResult = new Map();
|
|
937
|
-
for (const r of cascadeRuns) {
|
|
938
|
-
if (r.result && (r.carriedTurns ?? 0) > 0 && r.carriedTurns !== undefined) {
|
|
939
|
-
carriedMetaByResult.set(r.result, {
|
|
940
|
-
carriedTurns: r.carriedTurns,
|
|
941
|
-
observedAt: r.observedAt,
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
// #1550 class sweep: every cascade record below summarises `cascadeResults`
|
|
946
|
-
// — runs, which carry their own paths and can be carried across turns
|
|
947
|
-
// (#1443) — so labelling them with the turn's first EDITED file is the same
|
|
948
|
-
// mis-attribution the `cascade_indeterminate` fix removes. On a read-only
|
|
949
|
-
// drain turn `files` is empty and the old `?? cwd` fallback stamped a bare
|
|
950
|
-
// DIRECTORY as the record's file. These three are turn-level AGGREGATES (no
|
|
951
|
-
// per-file cause is claimed), so one label suffices; the edited file and cwd
|
|
952
|
-
// stay as fallbacks.
|
|
953
|
-
const cascadeLogFilePath = cascadeResults[0]?.filePath ?? files[0] ?? cwd;
|
|
954
|
-
if (cascadeResults.length > 0) {
|
|
955
|
-
const seen = new Map();
|
|
956
|
-
for (const result of cascadeResults) {
|
|
957
|
-
seen.set(normalizeMapKey(result.filePath), result);
|
|
958
|
-
}
|
|
959
|
-
// Iterate in reverse so the latest result claims each neighbor first.
|
|
960
|
-
const neighborOwner = new Map();
|
|
961
|
-
for (const result of [...seen.values()].reverse()) {
|
|
962
|
-
const pk = normalizeMapKey(result.filePath);
|
|
963
|
-
for (const n of result.neighbors) {
|
|
964
|
-
const nk = normalizeMapKey(n.filePath);
|
|
965
|
-
if (!neighborOwner.has(nk))
|
|
966
|
-
neighborOwner.set(nk, pk);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
const parts = [];
|
|
970
|
-
// #1446 item 1: track what actually gets injected — a suppressed result
|
|
971
|
-
// (real formatted cascade text, but every one of its neighbors was claimed
|
|
972
|
-
// by a LATER result — see the reverse-iteration ownership pass above) was
|
|
973
|
-
// previously indistinguishable from "no output"; this counts it explicitly
|
|
974
|
-
// instead of letting it vanish.
|
|
975
|
-
let injectedNeighborCount = 0;
|
|
976
|
-
let injectedDiagnosticCount = 0;
|
|
977
|
-
let suppressedByOwnership = 0;
|
|
978
|
-
for (const result of seen.values()) {
|
|
979
|
-
const pk = normalizeMapKey(result.filePath);
|
|
980
|
-
const ownsAny = result.neighbors.some((n) => neighborOwner.get(normalizeMapKey(n.filePath)) === pk);
|
|
981
|
-
if (ownsAny && result.formatted) {
|
|
982
|
-
// Fix B (#3167/#3168): a carried run's re-rendered blocker is labeled
|
|
983
|
-
// so the agent can tell it from a fresh observation — with the
|
|
984
|
-
// run's own observation age (#3168 F3).
|
|
985
|
-
const carryMeta = carriedMetaByResult.get(result);
|
|
986
|
-
const carrySuffix = cascadeCarrySuffix(carryMeta?.carriedTurns, carryMeta?.observedAt);
|
|
987
|
-
parts.push(carrySuffix
|
|
988
|
-
? `${result.formatted}\n${carrySuffix}`
|
|
989
|
-
: result.formatted);
|
|
990
|
-
if (carrySuffix)
|
|
991
|
-
carriedRunsRendered += 1;
|
|
992
|
-
injectedNeighborCount += result.neighbors.length;
|
|
993
|
-
injectedDiagnosticCount += result.neighbors.reduce((s, n) => s + n.diagnostics.length, 0);
|
|
994
|
-
}
|
|
995
|
-
else if (!ownsAny && result.formatted) {
|
|
996
|
-
suppressedByOwnership++;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
// Suggest tests for cascade neighbors (files with diagnostics)
|
|
1000
|
-
const neighborFilesWithErrors = cascadeResults
|
|
1001
|
-
.flatMap((r) => r.neighbors)
|
|
1002
|
-
.filter((n) => n.diagnostics.length > 0)
|
|
1003
|
-
.map((n) => n.filePath);
|
|
1004
|
-
const uniqueNeighborFiles = [...new Set(neighborFilesWithErrors)];
|
|
1005
|
-
let testSuggestionCount = 0;
|
|
1006
|
-
if (uniqueNeighborFiles.length > 0 &&
|
|
1007
|
-
typeof testRunnerClient.suggestTestFiles === "function") {
|
|
1008
|
-
const testSuggestions = testRunnerClient.suggestTestFiles(uniqueNeighborFiles, cwd);
|
|
1009
|
-
testSuggestionCount = testSuggestions.length;
|
|
1010
|
-
// #1446 item 2: this path previously emitted nothing to any log — a
|
|
1011
|
-
// zero-suggestion outcome (neighbors had errors but no test file
|
|
1012
|
-
// resolved for any of them) is the more interesting case, so it is
|
|
1013
|
-
// recorded on the same phase rather than only logging on a hit.
|
|
1014
|
-
logCascade({
|
|
1015
|
-
phase: "cascade_test_targets",
|
|
1016
|
-
filePath: cascadeLogFilePath,
|
|
1017
|
-
neighborCount: uniqueNeighborFiles.length,
|
|
1018
|
-
metadata: {
|
|
1019
|
-
neighborFiles: uniqueNeighborFiles.slice(0, 10),
|
|
1020
|
-
suggestedTestFiles: testSuggestions
|
|
1021
|
-
.slice(0, 10)
|
|
1022
|
-
.map((s) => s.testFile),
|
|
1023
|
-
runner: testSuggestions[0]?.runner,
|
|
1024
|
-
truncated: testSuggestions.length > 10,
|
|
1025
|
-
zeroSuggestions: testSuggestions.length === 0,
|
|
1026
|
-
},
|
|
1027
|
-
});
|
|
1028
|
-
if (testSuggestions.length > 0) {
|
|
1029
|
-
const testLines = testSuggestions
|
|
1030
|
-
.slice(0, 5)
|
|
1031
|
-
.map((s) => ` ${toRunnerDisplayPath(cwd, s.testFile)} (${s.runner})`);
|
|
1032
|
-
let testSection = `🧪 Likely tests for affected neighbors:\n${testLines.join("\n")}`;
|
|
1033
|
-
if (testSuggestions.length > 5) {
|
|
1034
|
-
testSection += `\n ... and ${testSuggestions.length - 5} more`;
|
|
1035
|
-
}
|
|
1036
|
-
parts.push(testSection);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
if (parts.length > 0) {
|
|
1040
|
-
const section = parts.join("\n\n");
|
|
1041
|
-
// @delivery-surface: runtime-turn:cascade-blocker
|
|
1042
|
-
blockerParts.push(section);
|
|
1043
|
-
// #1446 item 1: proves the cascade section reached `blockerParts` —
|
|
1044
|
-
// i.e. it was QUEUED for persistence into the turn-end advisory — not
|
|
1045
|
-
// that it reached the agent. The counters alone (cascade_result,
|
|
1046
|
-
// cascade_turn_end) never confirmed even that much, only computation.
|
|
1047
|
-
// Actual delivery happens later, via consumeTurnEndFindings/
|
|
1048
|
-
// peekTurnEndFindings, and can still be suppressed after this point
|
|
1049
|
-
// (e.g. allFilesDeleted, cross-turn dedup, or the session ending
|
|
1050
|
-
// before the next turn_end drains it) — this record does not prove
|
|
1051
|
-
// the agent ever saw the text.
|
|
1052
|
-
logCascade({
|
|
1053
|
-
phase: "cascade_injected",
|
|
1054
|
-
filePath: cascadeLogFilePath,
|
|
1055
|
-
neighborCount: injectedNeighborCount,
|
|
1056
|
-
diagnosticCount: injectedDiagnosticCount,
|
|
1057
|
-
metadata: {
|
|
1058
|
-
sectionChars: section.length,
|
|
1059
|
-
testSuggestionCount,
|
|
1060
|
-
suppressedByOwnership,
|
|
1061
|
-
},
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
logCascade({
|
|
1065
|
-
phase: "cascade_turn_end",
|
|
1066
|
-
filePath: cascadeLogFilePath,
|
|
1067
|
-
neighborCount: cascadeResults.reduce((s, r) => s + r.neighbors.length, 0),
|
|
1068
|
-
diagnosticCount: cascadeResults.reduce((s, r) => s + r.neighbors.reduce((ns, n) => ns + n.diagnostics.length, 0), 0),
|
|
1069
|
-
metadata: {
|
|
1070
|
-
fileCount: cascadeResults.length,
|
|
1071
|
-
mergedResults: seen.size,
|
|
1072
|
-
},
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
// #1023: surface an HONEST note whenever a cascade run could not compute
|
|
1076
|
-
// downstream impact (degraded/over-cap graph, missing node, a thrown compute,
|
|
1077
|
-
// or a deliberately budget-truncated neighbor set) — never a silent all-clear
|
|
1078
|
-
// (#533). This goes to the ADVISORY tier,
|
|
1079
|
-
// NOT the blocker tier: in an over-cap monorepo the graph is `skipped` on
|
|
1080
|
-
// every edit, so a blocker would fire hard and never clear turn state every
|
|
1081
|
-
// turn (over-escalation — the mirror of the silent-all-clear bug). Advisory
|
|
1082
|
-
// still reaches the agent, just without the blocker mechanics. Keyed strictly
|
|
1083
|
-
// off the `indeterminate` marker threaded by the compute; a healthy build
|
|
1084
|
-
// with a genuinely empty dependent set carries no marker and stays silent
|
|
1085
|
-
// (over-correction guard).
|
|
1086
|
-
const indeterminateRuns = cascadeRuns.filter((r) => r.indeterminate);
|
|
1087
|
-
if (indeterminateRuns.length > 0) {
|
|
1088
|
-
// #1104 (review P3 on PR #1143, rides with the resultId main body): this
|
|
1089
|
-
// preamble used to hardcode a graph-unavailability frame for EVERY
|
|
1090
|
-
// indeterminate reason. That's accurate for `graph_degraded`/
|
|
1091
|
-
// `missing_node`/`error` (the graph really couldn't produce a dependent
|
|
1092
|
-
// set), but `lsp_binding_rejected` is a DIFFERENT failure shape — the
|
|
1093
|
-
// graph WAS available and dependents WERE derived; only their LSP
|
|
1094
|
-
// diagnostics display was withheld because a fallback snapshot's content
|
|
1095
|
-
// binding didn't match current disk. Saying "the review graph was
|
|
1096
|
-
// unavailable" for that case mis-attributes the cause. Bucket by reason
|
|
1097
|
-
// family so each gets its own accurate frame.
|
|
1098
|
-
const buildAdvisory = (runs, frame) => {
|
|
1099
|
-
if (runs.length === 0)
|
|
1100
|
-
return undefined;
|
|
1101
|
-
const byDetail = new Map();
|
|
1102
|
-
for (const r of runs) {
|
|
1103
|
-
const detail = r.indeterminate?.detail ?? frame.fallbackDetail(r);
|
|
1104
|
-
const files = byDetail.get(detail) ?? [];
|
|
1105
|
-
files.push(toRunnerDisplayPath(cwd, r.filePath));
|
|
1106
|
-
byDetail.set(detail, files);
|
|
1107
|
-
}
|
|
1108
|
-
const lines = [];
|
|
1109
|
-
for (const [detail, filesRaw] of byDetail) {
|
|
1110
|
-
const files = [...new Set(filesRaw)];
|
|
1111
|
-
const shown = files.slice(0, 5).join(", ");
|
|
1112
|
-
const more = files.length > 5 ? ` (+${files.length - 5} more)` : "";
|
|
1113
|
-
lines.push(` • ${detail}: ${shown}${more}`);
|
|
1114
|
-
}
|
|
1115
|
-
const fileCount = new Set(runs.map((r) => normalizeMapKey(r.filePath)))
|
|
1116
|
-
.size;
|
|
1117
|
-
const reasons = [...byDetail.keys()].join("; ");
|
|
1118
|
-
return `${frame.lead(fileCount, reasons)}\n${lines.join("\n")}`;
|
|
1119
|
-
};
|
|
1120
|
-
// #1445: `excluded_by_role` (test files excluded from the graph BY DESIGN,
|
|
1121
|
-
// #260) is never agent-facing — it is not a graph failure, and #1080
|
|
1122
|
-
// already excludes test-role files from every neighbor surface, so "a
|
|
1123
|
-
// clean result does not cover them" would itself be a false claim. It
|
|
1124
|
-
// stays visible in the `cascade_indeterminate` log below (metadata-only,
|
|
1125
|
-
// info-level) so the log can tell an intentional exclusion from a real
|
|
1126
|
-
// graph gap, but it never reaches `buildAdvisory`/the agent.
|
|
1127
|
-
const graphRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason !== "lsp_binding_rejected" &&
|
|
1128
|
-
r.indeterminate?.reason !== "excluded_by_role" &&
|
|
1129
|
-
r.indeterminate?.reason !== "budget_truncated" &&
|
|
1130
|
-
r.indeterminate?.budget === undefined);
|
|
1131
|
-
const bindingRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "lsp_binding_rejected" &&
|
|
1132
|
-
r.indeterminate?.budget === undefined);
|
|
1133
|
-
// Budget coverage can be merged into a graph or binding marker, so its
|
|
1134
|
-
// advisory bucket follows the evidence rather than replacing that reason.
|
|
1135
|
-
const budgetRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "budget_truncated" ||
|
|
1136
|
-
r.indeterminate?.budget !== undefined);
|
|
1137
|
-
// Factual/informational phrasing — the advisory tier wraps this with an
|
|
1138
|
-
// "ℹ️ Advisory — no action required this turn:" label, so an imperative
|
|
1139
|
-
// ("review dependents manually") would contradict it. The #533 substance
|
|
1140
|
-
// stays: a clean cascade result does NOT cover these files' dependents.
|
|
1141
|
-
// Fix B (#3167/#3168 F4): a coverage advisory computed from a CARRIED
|
|
1142
|
-
// indeterminate run describes the previous turn's evidence — label it so
|
|
1143
|
-
// the absence-of-coverage statement is not read as current. A MIXED
|
|
1144
|
-
// bucket (some carried, some fresh) is left UNLABELED: a Math.max
|
|
1145
|
-
// suffix on the finished multi-line advisory would attach the carry to
|
|
1146
|
-
// a file that was not carried.
|
|
1147
|
-
const withCarryLabel = (advisory, runs) => {
|
|
1148
|
-
if (advisory === undefined)
|
|
1149
|
-
return undefined;
|
|
1150
|
-
const carried = runs.filter((r) => (r.carriedTurns ?? 0) > 0);
|
|
1151
|
-
if (carried.length === 0 || carried.length !== runs.length) {
|
|
1152
|
-
return advisory;
|
|
1153
|
-
}
|
|
1154
|
-
// #3168 F13: the bucket's age is the OLDEST carried observation, and
|
|
1155
|
-
// only when EVERY carried run carries one. A `Math.min` sentinel
|
|
1156
|
-
// (`?? Number.MAX_SAFE_INTEGER`) silently ignored the unstamped runs
|
|
1157
|
-
// and stated a confident age for a bucket that contains an unaged
|
|
1158
|
-
// one; one missing stamp collapses the age half to the helper's
|
|
1159
|
-
// neutral "scan age unknown" wording instead.
|
|
1160
|
-
const stamps = carried.map((r) => r.observedAt);
|
|
1161
|
-
const observedAt = stamps.every((s) => s !== undefined)
|
|
1162
|
-
? Math.min(...stamps)
|
|
1163
|
-
: undefined;
|
|
1164
|
-
const suffix = cascadeCarrySuffix(carried[0]?.carriedTurns, observedAt);
|
|
1165
|
-
if (!suffix)
|
|
1166
|
-
return advisory;
|
|
1167
|
-
labeledAdvisories += 1;
|
|
1168
|
-
// #3168 F11: newline, as the blocker path does above. A space join
|
|
1169
|
-
// welded the suffix onto the LAST bullet of a multi-bullet advisory,
|
|
1170
|
-
// so the carry label read as a property of that one file.
|
|
1171
|
-
return `${advisory}\n${suffix}`;
|
|
1172
|
-
};
|
|
1173
|
-
const graphAdvisory = withCarryLabel(buildAdvisory(graphRuns, {
|
|
1174
|
-
lead: (fileCount, reasons) => `Cascade could not compute downstream impact for ${fileCount} edited file(s) this turn — ` +
|
|
1175
|
-
`the review graph was unavailable (${reasons}), so their dependents were not ` +
|
|
1176
|
-
`cascade-checked and a clean cascade result does not cover them.`,
|
|
1177
|
-
fallbackDetail: (r) => r.indeterminate?.reason === "missing_node"
|
|
1178
|
-
? "changed file not in the review graph"
|
|
1179
|
-
: "review graph unavailable",
|
|
1180
|
-
}), graphRuns);
|
|
1181
|
-
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
1182
|
-
if (graphAdvisory)
|
|
1183
|
-
advisoryParts.push(graphAdvisory);
|
|
1184
|
-
const bindingAdvisory = withCarryLabel(buildAdvisory(bindingRuns, {
|
|
1185
|
-
lead: (fileCount, reasons) => `Cascade identified dependents for ${fileCount} edited file(s) this turn, but their ` +
|
|
1186
|
-
`diagnostics could not be freshly confirmed (${reasons}) and were withheld — a clean ` +
|
|
1187
|
-
`cascade result does not cover them.`,
|
|
1188
|
-
fallbackDetail: () => "cascade diagnostics withheld (binding rejected)",
|
|
1189
|
-
}), bindingRuns);
|
|
1190
|
-
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
1191
|
-
if (bindingAdvisory)
|
|
1192
|
-
advisoryParts.push(bindingAdvisory);
|
|
1193
|
-
const budgetAdvisory = withCarryLabel(buildAdvisory(budgetRuns, {
|
|
1194
|
-
lead: (fileCount, reasons) => `Cascade checked the selected neighbors for ${fileCount} edited file(s) this turn, ` +
|
|
1195
|
-
`but some eligible dependents were not checked because the cascade budget ` +
|
|
1196
|
-
`was exhausted (${reasons}); a clean cascade result does not cover them.`,
|
|
1197
|
-
fallbackDetail: (r) => {
|
|
1198
|
-
const budget = r.indeterminate?.budget;
|
|
1199
|
-
if (!budget)
|
|
1200
|
-
return "cascade budget omitted eligible dependents";
|
|
1201
|
-
const detail = `cascade budget checked ${budget.selectedCount} of ${budget.eligibleCount} eligible dependents (${budget.truncatedCount} omitted)`;
|
|
1202
|
-
return budget.transitiveTruncated
|
|
1203
|
-
? `${detail}; transitive expansion was capped before all eligible dependents were enumerated`
|
|
1204
|
-
: detail;
|
|
1205
|
-
},
|
|
1206
|
-
}), budgetRuns);
|
|
1207
|
-
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
1208
|
-
if (budgetAdvisory)
|
|
1209
|
-
advisoryParts.push(budgetAdvisory);
|
|
1210
|
-
const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
|
|
1211
|
-
// #1550: attribute each reason to the file that PRODUCED it. This record
|
|
1212
|
-
// used to stamp `filePath: files[0] ?? cwd` — the turn's first EDITED file
|
|
1213
|
-
// — and a bare `reasons` array with no file association. The two sets are
|
|
1214
|
-
// disjoint: a run can be carried across turns (#1443), and an edited file
|
|
1215
|
-
// can skip the graph entirely (markdown/JSON return `non_code` before
|
|
1216
|
-
// computeImpactCascade ever runs). So the log blamed a file that could not
|
|
1217
|
-
// have produced the reason — a markdown file credited with `missing_node`,
|
|
1218
|
-
// a non-test source file credited with `excluded_by_role` — and the defect
|
|
1219
|
-
// read as "concentrated on test files" only because the first edited file
|
|
1220
|
-
// of a turn usually is one. `fileCount` and the agent-facing advisory
|
|
1221
|
-
// already keyed off `r.filePath`; only this record's labels did not.
|
|
1222
|
-
const byFile = indeterminateRuns.map((r) => ({
|
|
1223
|
-
file: toRunnerDisplayPath(cwd, r.filePath),
|
|
1224
|
-
reason: r.indeterminate?.reason,
|
|
1225
|
-
...(r.indeterminate?.detail && { detail: r.indeterminate.detail }),
|
|
1226
|
-
...(r.indeterminate?.budget && { budget: r.indeterminate.budget }),
|
|
1227
|
-
...(r.indeterminate?.diagnostic && {
|
|
1228
|
-
diagnostic: r.indeterminate.diagnostic,
|
|
1229
|
-
}),
|
|
1230
|
-
}));
|
|
1231
|
-
logCascade({
|
|
1232
|
-
phase: "cascade_indeterminate",
|
|
1233
|
-
// The first indeterminate run's own file. `files[0] ?? cwd` survives only
|
|
1234
|
-
// as a last resort for a run with no path at all.
|
|
1235
|
-
filePath: indeterminateRuns[0]?.filePath ?? files[0] ?? cwd,
|
|
1236
|
-
metadata: {
|
|
1237
|
-
fileCount,
|
|
1238
|
-
reasons: indeterminateRuns.map((r) => r.indeterminate?.reason),
|
|
1239
|
-
byFile: byFile.slice(0, 20),
|
|
1240
|
-
...(byFile.length > 20 && { byFileTruncated: byFile.length - 20 }),
|
|
1241
|
-
},
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
// Fix B (#3168 F6): the success-path record — see the counters above.
|
|
1245
|
-
if (carriedRunsRendered > 0 || labeledAdvisories > 0) {
|
|
1246
|
-
logCascade({
|
|
1247
|
-
phase: "cascade_carry_rendered",
|
|
1248
|
-
filePath: cascadeLogFilePath,
|
|
1249
|
-
metadata: { carriedRunsRendered, labeledAdvisories },
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
const cascadeSkipped = {
|
|
1253
|
-
blockers: 0,
|
|
1254
|
-
non_code: 0,
|
|
1255
|
-
no_neighbors: 0,
|
|
1256
|
-
clean: 0,
|
|
1257
|
-
indeterminate: 0,
|
|
1258
|
-
error: 0,
|
|
1259
|
-
};
|
|
1260
|
-
for (const r of cascadeRuns) {
|
|
1261
|
-
if (r.skipReason)
|
|
1262
|
-
cascadeSkipped[r.skipReason] = (cascadeSkipped[r.skipReason] ?? 0) + 1;
|
|
1263
|
-
}
|
|
1264
|
-
logLatency({
|
|
1265
|
-
type: "phase",
|
|
1266
|
-
toolName: "turn_end",
|
|
1267
|
-
filePath: cwd,
|
|
1268
|
-
phase: "cascade_merge",
|
|
1269
|
-
durationMs: Date.now() - t0,
|
|
1270
|
-
metadata: {
|
|
1271
|
-
runsTotal: cascadeRuns.length,
|
|
1272
|
-
resultCount: cascadeResults.length,
|
|
1273
|
-
neighborCount: cascadeRuns.reduce((s, r) => s + r.neighborCount, 0),
|
|
1274
|
-
diagnosticCount: cascadeRuns.reduce((s, r) => s + r.diagnosticCount, 0),
|
|
1275
|
-
skipped: cascadeSkipped,
|
|
1276
|
-
},
|
|
1277
|
-
});
|
|
1278
|
-
const t2 = Date.now();
|
|
1279
|
-
let knipMeta = {};
|
|
1280
|
-
if (runtime.isStartupScanInFlight("knip")) {
|
|
1281
|
-
dbg("turn_end: skipping knip (startup scan still in flight)");
|
|
1282
|
-
knipMeta = { skipped: true };
|
|
1283
|
-
}
|
|
1284
|
-
else {
|
|
1285
|
-
// Let KnipClient resolve/validate a real JS project root before probing or
|
|
1286
|
-
// auto-installing knip. Non-JS repos (for example Unity projects) should not
|
|
1287
|
-
// run tool checks every turn. Also back off after a timeout/kill so every
|
|
1288
|
-
// agent turn does not spend 30s launching another heavyweight knip process.
|
|
1289
|
-
const prevKnip = cacheManager.readCache("knip", cwd);
|
|
1290
|
-
// An availability failure is NOT a hard knip failure: knip never ran, so
|
|
1291
|
-
// there is nothing to back off from, and backing off would make an
|
|
1292
|
-
// expiring probe verdict permanent again (#1467).
|
|
1293
|
-
const previousFailedHard = prevKnip &&
|
|
1294
|
-
!prevKnip.data.success &&
|
|
1295
|
-
!prevKnip.data.failureKind &&
|
|
1296
|
-
/(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prevKnip.data.summary);
|
|
1297
|
-
if (previousFailedHard) {
|
|
1298
|
-
dbg(`turn_end: skipping knip after recent failure: ${prevKnip.data.summary}`);
|
|
1299
|
-
knipMeta = { skipped: true, reason: prevKnip.data.summary };
|
|
1300
|
-
}
|
|
1301
|
-
else {
|
|
1302
|
-
const knipResult = await knipClient.analyze(cwd, getKnipIgnorePatterns(), {
|
|
1303
|
-
projectSeq: runtime.projectSeq,
|
|
1304
|
-
});
|
|
1305
|
-
// Never overwrite a good scan with a failure (#925, #1467): the last
|
|
1306
|
-
// good result stays until a new successful scan replaces it.
|
|
1307
|
-
const knipWouldPoison = wouldPoisonCache(prevKnip, knipResult);
|
|
1308
|
-
if (knipWouldPoison) {
|
|
1309
|
-
dbg(`turn_end: keeping last good knip cache; this run failed: ${knipResult.summary}`);
|
|
1310
|
-
}
|
|
1311
|
-
else {
|
|
1312
|
-
cacheManager.writeCache("knip", knipResult, cwd);
|
|
1313
|
-
}
|
|
1314
|
-
knipMeta = {
|
|
1315
|
-
execution: knipResult.execution ?? "executed",
|
|
1316
|
-
success: knipResult.success,
|
|
1317
|
-
totalIssues: knipResult.issues.length,
|
|
1318
|
-
newIssues: 0,
|
|
1319
|
-
blockerIssues: 0,
|
|
1320
|
-
// #3248: bounded per-turn, on the row this lane already writes —
|
|
1321
|
-
// never one record per finding.
|
|
1322
|
-
dispositionSuppressed: 0,
|
|
1323
|
-
...(!knipResult.success && { reason: knipResult.summary }),
|
|
1324
|
-
...(knipResult.failureKind && { failureKind: knipResult.failureKind }),
|
|
1325
|
-
...(knipWouldPoison && { cacheKept: true }),
|
|
1326
|
-
};
|
|
1327
|
-
if (knipResult.success && knipResult.issues.length > 0) {
|
|
1328
|
-
// Deliberately excludes the line number — see stableFindingKey's
|
|
1329
|
-
// doc comment (#1483: mirrors the dead-code fix in #1477).
|
|
1330
|
-
const issueKey = (i) => stableFindingKey(i.type, i.file, i.name, i.package);
|
|
1331
|
-
const prevKeys = new Set((prevKnip?.data?.issues ?? []).map(issueKey));
|
|
1332
|
-
const modifiedSet = new Set(files.map((f) => resolveRunnerPath(cwd, f)));
|
|
1333
|
-
const newIssues = knipResult.issues.filter((issue) => {
|
|
1334
|
-
if (prevKeys.has(issueKey(issue)))
|
|
1335
|
-
return false;
|
|
1336
|
-
if (!issue.file)
|
|
1337
|
-
return false;
|
|
1338
|
-
const abs = resolveRunnerPath(cwd, issue.file);
|
|
1339
|
-
return modifiedSet.has(abs);
|
|
1340
|
-
});
|
|
1341
|
-
knipMeta.newIssues = newIssues.length;
|
|
1342
|
-
if (newIssues.length > 0) {
|
|
1343
|
-
projectDiagnosticsDelta.push(...knipIssuesToProjectDiagnostics(cwd, newIssues));
|
|
1344
|
-
projectDiagnosticsSources.add("knip");
|
|
1345
|
-
}
|
|
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");
|
|
1370
|
-
knipMeta.blockerIssues = blockerIssues.length;
|
|
1371
|
-
if (blockerIssues.length > 0) {
|
|
1372
|
-
let report = "🔴 New unresolved imports/deps in modified code (Knip):\n";
|
|
1373
|
-
let firstPath = null;
|
|
1374
|
-
for (const issue of blockerIssues.slice(0, 5)) {
|
|
1375
|
-
const display = issue.file
|
|
1376
|
-
? toRunnerDisplayPath(cwd, issue.file)
|
|
1377
|
-
: "(unknown)";
|
|
1378
|
-
if (!firstPath && display !== "(unknown)")
|
|
1379
|
-
firstPath = display;
|
|
1380
|
-
report += ` ${display}${issue.line ? `:${issue.line}` : ""} — ${issue.type}: ${issue.name}\n`;
|
|
1381
|
-
}
|
|
1382
|
-
if (firstPath) {
|
|
1383
|
-
report += ` First location: ${firstPath}\n`;
|
|
1384
|
-
}
|
|
1385
|
-
// @delivery-surface: runtime-turn:knip-blocker
|
|
1386
|
-
blockerParts.push(report);
|
|
1387
|
-
}
|
|
1388
|
-
// Turn-end injects only this turn's HIGH-CONFIDENCE, ATTRIBUTABLE
|
|
1389
|
-
// delta: symbols in files the agent just edited that became unused
|
|
1390
|
-
// (weren't flagged in the previous scan) — low-volume and actionable
|
|
1391
|
-
// now. The FULL project-wide dead-code picture is deliberately NOT
|
|
1392
|
-
// injected per turn (hundreds of mostly-pre-existing findings would
|
|
1393
|
-
// drown the blockers and burn context every turn); it's available
|
|
1394
|
-
// on demand via lens_diagnostics. The delta also feeds the session-slop
|
|
1395
|
-
// record (`projectDiagnosticsDelta`) above.
|
|
1396
|
-
const unusedExportDelta = knipDeliverable.kept.filter((i) => i.type === "export" || i.type === "enumMember");
|
|
1397
|
-
if (unusedExportDelta.length > 0) {
|
|
1398
|
-
let report = "⚠️ Newly unused exports in files you edited — check if callers need updating (Knip):\n";
|
|
1399
|
-
for (const issue of unusedExportDelta.slice(0, 5)) {
|
|
1400
|
-
const display = issue.file
|
|
1401
|
-
? toRunnerDisplayPath(cwd, issue.file)
|
|
1402
|
-
: "(unknown)";
|
|
1403
|
-
report += ` ${display}${issue.line ? `:${issue.line}` : ""} — ${issue.name}\n`;
|
|
1404
|
-
}
|
|
1405
|
-
// @delivery-surface: runtime-turn:knip-advisory
|
|
1406
|
-
advisoryParts.push(report);
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
logLatency({
|
|
1412
|
-
type: "phase",
|
|
1413
|
-
toolName: "turn_end",
|
|
1414
|
-
filePath: cwd,
|
|
1415
|
-
phase: "knip",
|
|
1416
|
-
durationMs: Date.now() - t2,
|
|
1417
|
-
metadata: knipMeta,
|
|
1418
|
-
});
|
|
1419
|
-
// Cross-file dead-code (#127) for non-JS/TS languages, on knip's contract:
|
|
1420
|
-
// re-scan only when this turn touched a file the client owns, then inject the
|
|
1421
|
-
// ATTRIBUTABLE delta — symbols in those files that became unused because of
|
|
1422
|
-
// the edit. The project-wide list is deliberately NOT injected per turn (the
|
|
1423
|
-
// same reasoning as knip above) and stays available via lens_diagnostics.
|
|
1424
|
-
// MUST run before the projectDiagnosticsDelta write below, or a dead-code-only
|
|
1425
|
-
// turn would persist nothing.
|
|
1426
|
-
const tDeadCode = Date.now();
|
|
1427
|
-
const deadCodeMeta = {};
|
|
1428
|
-
if (runtime.isStartupScanInFlight("dead-code")) {
|
|
1429
|
-
dbg("turn_end: skipping dead-code (startup scan still in flight)");
|
|
1430
|
-
deadCodeMeta.skipped = true;
|
|
1431
|
-
deadCodeMeta.reason = "startup_scan_in_flight";
|
|
1432
|
-
}
|
|
1433
|
-
else if (deadCodeClients.length === 0) {
|
|
1434
|
-
deadCodeMeta.reason = "no_clients";
|
|
1435
|
-
}
|
|
1436
|
-
else {
|
|
1437
|
-
// The modified-file set costs a resolveRunnerPath per file, and that walks
|
|
1438
|
-
// every ancestor to the filesystem root on a miss. Build it lazily, only
|
|
1439
|
-
// once a client has actually claimed this project, so an all-JS repo with
|
|
1440
|
-
// no dead-code client pays nothing per turn. Knip does the same.
|
|
1441
|
-
let modifiedSet = null;
|
|
1442
|
-
const modifiedFiles = () => (modifiedSet ??= new Set(files.map((f) => resolveRunnerPath(cwd, f))));
|
|
1443
|
-
let newIssueTotal = 0;
|
|
1444
|
-
/** #3248: bounded per-turn on this lane's own row, never per finding. */
|
|
1445
|
-
let deadCodeDispositionSuppressed = 0;
|
|
1446
|
-
const reasons = [];
|
|
1447
|
-
// A malformed client or deps object must never abort turn_end. Before the
|
|
1448
|
-
// per-turn delta this block only read a cache; now it iterates and awaits,
|
|
1449
|
-
// so the whole thing needs the guard, not just `client.analyze`.
|
|
1450
|
-
try {
|
|
1451
|
-
for (const client of deadCodeClients) {
|
|
1452
|
-
if (!client.detect(cwd)) {
|
|
1453
|
-
reasons.push(`${client.id}:not_detected`);
|
|
1454
|
-
continue;
|
|
1455
|
-
}
|
|
1456
|
-
if (![...modifiedFiles()].some((f) => client.owns(f))) {
|
|
1457
|
-
reasons.push(`${client.id}:no_owned_files`);
|
|
1458
|
-
continue;
|
|
1459
|
-
}
|
|
1460
|
-
const cacheKey = `dead-code-${client.id}`;
|
|
1461
|
-
const prev = cacheManager.readCache(cacheKey, cwd);
|
|
1462
|
-
// Back off after a timeout/kill so an unresponsive scanner cannot cost
|
|
1463
|
-
// every later turn its full analysis budget (mirrors knip).
|
|
1464
|
-
if (prev &&
|
|
1465
|
-
!prev.data.success &&
|
|
1466
|
-
/(timed out|killed|SIGTERM|SIGKILL|SIGABRT)/i.test(prev.data.summary)) {
|
|
1467
|
-
dbg(`turn_end: skipping dead-code after failure: ${prev.data.summary}`);
|
|
1468
|
-
deadCodeMeta.skipped = true;
|
|
1469
|
-
reasons.push(`${client.id}:backoff:${prev.data.summary}`);
|
|
1470
|
-
continue;
|
|
1471
|
-
}
|
|
1472
|
-
const startMs = Date.now();
|
|
1473
|
-
try {
|
|
1474
|
-
const result = await client.analyze(cwd);
|
|
1475
|
-
const durationMs = Date.now() - startMs;
|
|
1476
|
-
// Never overwrite a good scan with a failure (#925, #1467): a
|
|
1477
|
-
// vulture timeout on one .py turn would otherwise evict the
|
|
1478
|
-
// session_start scan, and the backoff above would then latch
|
|
1479
|
-
// off the poisoned record.
|
|
1480
|
-
if (wouldPoisonCache(prev, result)) {
|
|
1481
|
-
dbg(`turn_end: keeping last good dead-code(${client.id}) cache; this run failed: ${result.summary}`);
|
|
1482
|
-
deadCodeMeta.cacheKept = true;
|
|
1483
|
-
}
|
|
1484
|
-
else {
|
|
1485
|
-
cacheManager.writeCache(cacheKey, result, cwd, {
|
|
1486
|
-
scanDurationMs: durationMs,
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
// One event per cross-file scan (AGENTS.md) — the per-turn scan is
|
|
1490
|
-
// now the primary path, so dead-code.log must see it too.
|
|
1491
|
-
logDeadCodeScan({
|
|
1492
|
-
language: client.language,
|
|
1493
|
-
success: result.success,
|
|
1494
|
-
cached: false,
|
|
1495
|
-
unusedExports: result.unusedExports.length,
|
|
1496
|
-
unusedFiles: result.unusedFiles.length,
|
|
1497
|
-
unusedDeps: result.unusedDeps.length,
|
|
1498
|
-
unlistedDeps: result.unlistedDeps.length,
|
|
1499
|
-
durationMs: result.durationMs ?? durationMs,
|
|
1500
|
-
...(!result.success && { reason: result.summary }),
|
|
1501
|
-
});
|
|
1502
|
-
deadCodeMeta.success = result.success;
|
|
1503
|
-
if (!result.success) {
|
|
1504
|
-
reasons.push(`${client.id}:scan_failed:${result.summary}`);
|
|
1505
|
-
continue;
|
|
1506
|
-
}
|
|
1507
|
-
deadCodeMeta.totalIssues =
|
|
1508
|
-
(deadCodeMeta.totalIssues ?? 0) + deadCodeIssues(result).length;
|
|
1509
|
-
// No baseline means every finding looks new. Report nothing rather
|
|
1510
|
-
// than blame the edit for the whole project's pre-existing debt.
|
|
1511
|
-
if (!prev?.data.success) {
|
|
1512
|
-
reasons.push(`${client.id}:no_previous_scan`);
|
|
1513
|
-
continue;
|
|
1514
|
-
}
|
|
1515
|
-
const prevKeys = new Set(deadCodeIssues(prev.data).map(deadCodeIssueKey));
|
|
1516
|
-
const modified = modifiedFiles();
|
|
1517
|
-
const newIssues = deadCodeIssues(result).filter((issue) => {
|
|
1518
|
-
if (prevKeys.has(deadCodeIssueKey(issue)))
|
|
1519
|
-
return false;
|
|
1520
|
-
if (!issue.file)
|
|
1521
|
-
return false;
|
|
1522
|
-
return modified.has(resolveRunnerPath(cwd, issue.file));
|
|
1523
|
-
});
|
|
1524
|
-
if (newIssues.length === 0) {
|
|
1525
|
-
reasons.push(`${client.id}:clean`);
|
|
1526
|
-
continue;
|
|
1527
|
-
}
|
|
1528
|
-
newIssueTotal += newIssues.length;
|
|
1529
|
-
projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
|
|
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
|
-
}
|
|
1544
|
-
// @delivery-surface: runtime-turn:dead-code-advisory
|
|
1545
|
-
advisoryParts.push(formatDeadCodeDelta(deadCodeDeliverable.kept, result.language));
|
|
1546
|
-
}
|
|
1547
|
-
catch (err) {
|
|
1548
|
-
dbg(`turn_end: dead-code(${client.id}) failed: ${err}`);
|
|
1549
|
-
reasons.push(`${client.id}:threw`);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
catch (err) {
|
|
1554
|
-
dbg(`turn_end: dead-code block failed: ${err}`);
|
|
1555
|
-
reasons.push("block_threw");
|
|
1556
|
-
}
|
|
1557
|
-
deadCodeMeta.newIssues = newIssueTotal;
|
|
1558
|
-
deadCodeMeta.dispositionSuppressed = deadCodeDispositionSuppressed;
|
|
1559
|
-
if (reasons.length > 0)
|
|
1560
|
-
deadCodeMeta.reason = reasons.join(",");
|
|
1561
|
-
}
|
|
1562
|
-
logLatency({
|
|
1563
|
-
type: "phase",
|
|
1564
|
-
toolName: "turn_end",
|
|
1565
|
-
filePath: cwd,
|
|
1566
|
-
phase: "dead-code",
|
|
1567
|
-
durationMs: Date.now() - tDeadCode,
|
|
1568
|
-
metadata: deadCodeMeta,
|
|
1569
|
-
});
|
|
1570
|
-
// #1617: running total of findings this turn's advisory/blocker sections
|
|
1571
|
-
// dropped because an agent/user marked them false-positive/won't-fix —
|
|
1572
|
-
// the #1616 suppressed-bucket rule applied to turn_end's own reporting
|
|
1573
|
-
// lanes, so a mark's effect stays visible even though the finding itself
|
|
1574
|
-
// no longer appears above. Review-round F4 (#1625): kept per-lane, not
|
|
1575
|
-
// just a bare total, so the eventual trace says WHICH lane's marks did
|
|
1576
|
-
// the suppressing.
|
|
1577
|
-
let dispositionSuppressedTotal = 0;
|
|
1578
|
-
const dispositionSuppressedByLane = {};
|
|
1579
|
-
function recordDispositionSuppressed(lane, count) {
|
|
1580
|
-
if (count <= 0)
|
|
1581
|
-
return;
|
|
1582
|
-
dispositionSuppressedTotal += count;
|
|
1583
|
-
dispositionSuppressedByLane[lane] =
|
|
1584
|
-
(dispositionSuppressedByLane[lane] ?? 0) + count;
|
|
1585
|
-
}
|
|
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 = {
|
|
1650
|
-
cwd,
|
|
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
|
-
},
|
|
1710
|
-
});
|
|
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);
|
|
1717
|
-
}
|
|
1718
|
-
// @delivery-surface: runtime-turn:govulncheck-advisory
|
|
1719
|
-
advisoryParts.push(...(govDelivery.advisoryParts ?? []));
|
|
1720
|
-
// Secrets — UNIFIED surfacing (#131 Mode 3). gitleaks, trivy secret, and the
|
|
1721
|
-
// ast-grep hardcoded-secret rules can each flag the SAME line with different
|
|
1722
|
-
// rule ids, which the rule-keyed diagnostic dedup can't collapse. Collapse by
|
|
1723
|
-
// location so a committed/hardcoded secret is reported ONCE (with combined
|
|
1724
|
-
// provenance) — a blocker, since credentials need rotation before merge.
|
|
1725
|
-
//
|
|
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);
|
|
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 ?? []));
|
|
1747
|
-
// trivy — surface session_start-cached dependency CVEs (#131, Phase 1).
|
|
1748
|
-
// CRITICAL is a blocker (a known-exploitable CVE in a shipped dep is real
|
|
1749
|
-
// production risk); HIGH/MEDIUM/LOW are advisory. The agent gets the upgrade
|
|
1750
|
-
// target as a hint and decides — we never auto-edit lockfiles.
|
|
1751
|
-
//
|
|
1752
|
-
// #1634: these three trivy reports (critical blocker, non-critical
|
|
1753
|
-
// advisory, license advisory below) name a PACKAGE, not a file:line — there
|
|
1754
|
-
// is no cited path for `gateFindingsByPathFreshness` to stat, so unlike the
|
|
1755
|
-
// secrets/govulncheck stores above this store cannot be freshness-GATED.
|
|
1756
|
-
// It is the delivery gate's explicit-label escape hatch instead
|
|
1757
|
-
// (`clients/finding-delivery-gate.ts`, surfaces `runtime-turn:trivy-*`):
|
|
1758
|
-
// the session_start cache can be arbitrarily old, so its age is stated
|
|
1759
|
-
// plainly rather than presenting a CRITICAL blocker as if it were current.
|
|
1760
|
-
// This runs on top of (not instead of) #1625's disposition filter below —
|
|
1761
|
-
// a suppressed finding never reaches this render at all, so the two only
|
|
1762
|
-
// ever compose.
|
|
1763
|
-
const trivyAgeLabel = formatCacheAgeLabel(trivyCacheEntry?.data?.scannedAt);
|
|
1764
|
-
const trivyFindingsFiltered = filterFindingsByDisposition(trivyCacheEntry?.data?.findings ?? [], cwd, (f) => trivyFindingToProjectDiagnostic(cwd, f));
|
|
1765
|
-
recordDispositionSuppressed("trivy", trivyFindingsFiltered.suppressed);
|
|
1766
|
-
if (trivyFindingsFiltered.kept.length) {
|
|
1767
|
-
const all = trivyFindingsFiltered.kept;
|
|
1768
|
-
const critical = all.filter((f) => f.severity === "CRITICAL");
|
|
1769
|
-
const advisory = all.filter((f) => f.severity !== "CRITICAL");
|
|
1770
|
-
const fmt = (f) => {
|
|
1771
|
-
const pkg = f.installedVersion
|
|
1772
|
-
? `${f.pkgName}@${f.installedVersion}`
|
|
1773
|
-
: f.pkgName;
|
|
1774
|
-
const fix = f.fixedVersion
|
|
1775
|
-
? ` — upgrade to ${f.fixedVersion} or later`
|
|
1776
|
-
: " — no fix yet, track upstream";
|
|
1777
|
-
return ` ${f.vulnerabilityId} (${pkg})${fix}\n`;
|
|
1778
|
-
};
|
|
1779
|
-
if (critical.length) {
|
|
1780
|
-
const shown = critical.slice(0, 5);
|
|
1781
|
-
let report = `🔴 STOP — CRITICAL dependency CVEs (trivy, ${trivyAgeLabel}). Upgrade before shipping:\n`;
|
|
1782
|
-
for (const f of shown)
|
|
1783
|
-
report += fmt(f);
|
|
1784
|
-
if (critical.length > shown.length) {
|
|
1785
|
-
report += ` … and ${critical.length - shown.length} more\n`;
|
|
1786
|
-
}
|
|
1787
|
-
// @delivery-surface: runtime-turn:trivy-critical-blocker
|
|
1788
|
-
blockerParts.push(report);
|
|
1789
|
-
}
|
|
1790
|
-
if (advisory.length) {
|
|
1791
|
-
const shown = advisory.slice(0, 5);
|
|
1792
|
-
let report = `🛡️ Dependency CVEs (trivy, ${trivyAgeLabel}) — upgrade where possible:\n`;
|
|
1793
|
-
for (const f of shown)
|
|
1794
|
-
report += fmt(f);
|
|
1795
|
-
if (advisory.length > shown.length) {
|
|
1796
|
-
report += ` … and ${advisory.length - shown.length} more\n`;
|
|
1797
|
-
}
|
|
1798
|
-
// @delivery-surface: runtime-turn:trivy-cve-advisory
|
|
1799
|
-
advisoryParts.push(report);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
// trivy — dependency license risk (#131 Mode 4). Advisory only: a copyleft /
|
|
1803
|
-
// restricted license in a proprietary tree is a compliance signal, not a
|
|
1804
|
-
// build break. Surfaced from the same cached `trivy fs` pass — same #1634
|
|
1805
|
-
// explicit-label rationale as the CVE reports above (no cited path to gate).
|
|
1806
|
-
const licenses = trivyCacheEntry?.data?.licenses ?? [];
|
|
1807
|
-
if (licenses.length) {
|
|
1808
|
-
const shown = licenses.slice(0, 5);
|
|
1809
|
-
let report = `📜 Dependency license risk (trivy, ${trivyAgeLabel}) — review for compliance:\n`;
|
|
1810
|
-
for (const l of shown) {
|
|
1811
|
-
const cat = l.category ? `, ${l.category}` : "";
|
|
1812
|
-
report += ` ${l.pkgName} — ${l.license} (${l.severity}${cat})\n`;
|
|
1813
|
-
}
|
|
1814
|
-
if (licenses.length > shown.length) {
|
|
1815
|
-
report += ` … and ${licenses.length - shown.length} more\n`;
|
|
1816
|
-
}
|
|
1817
|
-
// @delivery-surface: runtime-turn:trivy-license-advisory
|
|
1818
|
-
advisoryParts.push(report);
|
|
1819
|
-
}
|
|
1820
|
-
// #1616 suppressed-bucket rule: surface the running disposition-drop total
|
|
1821
|
-
// as its own advisory line so a mark's effect is visible, not a silent
|
|
1822
|
-
// absence — trace, not a vanish.
|
|
1823
|
-
if (dispositionSuppressedTotal > 0) {
|
|
1824
|
-
// Review-round F4 (#1625): per-lane attribution, e.g.
|
|
1825
|
-
// "gitleaks 2, govulncheck 1" — not just a bare total.
|
|
1826
|
-
const byLane = Object.entries(dispositionSuppressedByLane)
|
|
1827
|
-
.map(([lane, count]) => `${lane} ${count}`)
|
|
1828
|
-
.join(", ");
|
|
1829
|
-
// @delivery-surface: runtime-turn:disposition-suppressed-notice
|
|
1830
|
-
advisoryParts.push(`suppressed by disposition: ${dispositionSuppressedTotal} finding(s) ` +
|
|
1831
|
-
`dropped from this turn's gitleaks/govulncheck/trivy sections (${byLane}) ` +
|
|
1832
|
-
"(marked false-positive or won't-fix).");
|
|
1833
|
-
}
|
|
1834
|
-
const t3 = Date.now();
|
|
1835
|
-
let madgeStats;
|
|
1836
|
-
// Off by default (#766): this pass only writes debug output, and user-facing
|
|
1837
|
-
// madge diagnostics come from the session-start `madge` cache + the
|
|
1838
|
-
// `lens_diagnostics` extractor. Enabled with `--lens-turn-end-madge` /
|
|
1839
|
-
// `turnEnd.madge.enabled=true` for those who want the per-edit circular note.
|
|
1840
|
-
if (getFlag("lens-turn-end-madge") && (await depChecker.ensureAvailable())) {
|
|
1841
|
-
const madgeFiles = cacheManager.getFilesForMadge(cwd);
|
|
1842
|
-
if (madgeFiles.length > 0) {
|
|
1843
|
-
dbg(`turn_end: madge checking ${madgeFiles.length} file(s) for circular deps`);
|
|
1844
|
-
// Checked concurrently (bounded) rather than one `await` per file —
|
|
1845
|
-
// the shared circular-dep state update is deferred/folded inside
|
|
1846
|
-
// checkFilesBatch so concurrent spawns can't clobber each other (#766).
|
|
1847
|
-
const absFiles = madgeFiles.map((file) => path.resolve(cwd, file));
|
|
1848
|
-
const batch = await depChecker.checkFilesBatch(absFiles, cwd);
|
|
1849
|
-
const depResults = batch.results;
|
|
1850
|
-
madgeStats = batch.stats;
|
|
1851
|
-
for (const file of madgeFiles) {
|
|
1852
|
-
const absPath = path.resolve(cwd, file);
|
|
1853
|
-
const depResult = depResults.get(absPath);
|
|
1854
|
-
if (!depResult)
|
|
1855
|
-
continue;
|
|
1856
|
-
if (depResult.hasCircular && depResult.circular.length > 0) {
|
|
1857
|
-
// Whole-project circular deps are surfaced in lens_diagnostics via the
|
|
1858
|
-
// session-start `madge` cache + extractor; this per-file turn-end pass
|
|
1859
|
-
// only logs (blockers-only mode suppresses circular-dep notes).
|
|
1860
|
-
dbg(`turn_end: circular dependency note for ${file} (suppressed in blockers-only mode)`);
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
logLatency({
|
|
1866
|
-
type: "phase",
|
|
1867
|
-
toolName: "turn_end",
|
|
1868
|
-
filePath: cwd,
|
|
1869
|
-
phase: "madge",
|
|
1870
|
-
durationMs: Date.now() - t3,
|
|
1871
|
-
// A ~0ms entry with no metadata is indistinguishable from "ran and was
|
|
1872
|
-
// fast" when re-analyzing the #766 tail — mark the skipped case.
|
|
1873
|
-
metadata: madgeStats ?? { skipped: true },
|
|
1874
|
-
});
|
|
1875
|
-
// --- Test runner: fire once per turn after all edits are done ---
|
|
1876
|
-
// Runs for each unique test target across modified files; results remain in
|
|
1877
|
-
// the pull-diagnostics cache and are delivered after the agent settles.
|
|
1878
|
-
if (!getFlag("no-tests") && files.length > 0) {
|
|
1879
|
-
const seen = new Set();
|
|
1880
|
-
// `strategy` is widened by one value the resolver itself cannot produce:
|
|
1881
|
-
// a target carried over from the previous turn's cut batch (#2522 R2 F1).
|
|
1882
|
-
const targets = [];
|
|
1883
|
-
// #628: also target the test companions of this turn's cascade neighbors
|
|
1884
|
-
// (files that import an edited file) — a neighbor's own tests can break
|
|
1885
|
-
// even though the neighbor's source wasn't touched. Reuses `cascadeResults`,
|
|
1886
|
-
// already computed above (from the same #450 deferred-cascade drain) for the
|
|
1887
|
-
// LSP cascade-diagnostics merge — no second reverse-dependency walk, and the
|
|
1888
|
-
// neighbor set inherits whatever budget the cascade compute already applied
|
|
1889
|
-
// (CASCADE_NEIGHBOUR_BUDGET), so this can't turn into unbounded per-edit work.
|
|
1890
|
-
const candidates = [];
|
|
1891
|
-
const seenCandidateKeys = new Set();
|
|
1892
|
-
for (const file of files) {
|
|
1893
|
-
const abs = resolveRunnerPath(cwd, file);
|
|
1894
|
-
const key = normalizeMapKey(abs);
|
|
1895
|
-
if (seenCandidateKeys.has(key))
|
|
1896
|
-
continue;
|
|
1897
|
-
seenCandidateKeys.add(key);
|
|
1898
|
-
candidates.push({ display: file, abs, isNeighbor: false });
|
|
1899
|
-
}
|
|
1900
|
-
for (const result of cascadeResults) {
|
|
1901
|
-
for (const neighbor of result.neighbors) {
|
|
1902
|
-
const abs = path.isAbsolute(neighbor.filePath)
|
|
1903
|
-
? neighbor.filePath
|
|
1904
|
-
: resolveRunnerPath(cwd, neighbor.filePath);
|
|
1905
|
-
const key = normalizeMapKey(abs);
|
|
1906
|
-
if (seenCandidateKeys.has(key))
|
|
1907
|
-
continue;
|
|
1908
|
-
seenCandidateKeys.add(key);
|
|
1909
|
-
candidates.push({ display: neighbor.filePath, abs, isNeighbor: true });
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
// #2522 review round 2, F1: the previous turn's unfinished targets go
|
|
1913
|
-
// FIRST — ahead of failed-first/related/self — under the same
|
|
1914
|
-
// TEST_RUNNER_MAX_TARGETS cap, so a batch that keeps getting cut makes
|
|
1915
|
-
// progress instead of re-running whatever this turn happened to touch
|
|
1916
|
-
// and dropping the remainder again. The list is consumed here (written
|
|
1917
|
-
// back empty below) so a target can never be carried forever.
|
|
1918
|
-
const priorTestCache = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
1919
|
-
const carriedDeferred = priorTestCache?.deferredTargets ?? [];
|
|
1920
|
-
// #2522 review round 3, F5: a deferral list belongs to the session that
|
|
1921
|
-
// cut it. `runtime.telemetrySessionId` is the same identity the batch
|
|
1922
|
-
// stamps its own entries with below (`firedSessionId`), so this is a
|
|
1923
|
-
// single comparison against one source of truth.
|
|
1924
|
-
const turnSessionId = sessionId ?? runtime.telemetrySessionId;
|
|
1925
|
-
const isThisSession = (entry) => entry.sessionId === turnSessionId;
|
|
1926
|
-
const entryPathKey = (entry) => normalizeMapKey(path.resolve(cwd, entry.testFile));
|
|
1927
|
-
/**
|
|
1928
|
-
* #2522 review round 3, F1: the retirement, carried forward.
|
|
1929
|
-
*
|
|
1930
|
-
* Round 2 announced the retirement and dropped it on the floor, so the
|
|
1931
|
-
* candidate loop below re-resolved the very same file through
|
|
1932
|
-
* `related`/`self` in the SAME turn and ran it anyway with a fresh
|
|
1933
|
-
* (undefined) attempt count. Steady state was a 3-turn cycle that spawned
|
|
1934
|
-
* and cut the slow suite on every turn. The retired set is persisted on
|
|
1935
|
-
* the record instead.
|
|
1936
|
-
*
|
|
1937
|
-
* Round 4, I1: this is a SELECTION filter and nothing more. Round 3 also
|
|
1938
|
-
* wrote the filtered list straight back, so a turn taken by the other
|
|
1939
|
-
* entry point into the same project — `clients/mcp/session.ts` passes no
|
|
1940
|
-
* `sessionId` and falls back to `telemetrySessionId`, while `index.ts`
|
|
1941
|
-
* passes pi's stable id — erased the entries belonging to the other
|
|
1942
|
-
* route. Both lists survive every write now; see `writeTestFindings`.
|
|
1943
|
-
*/
|
|
1944
|
-
const retiredCarry = (priorTestCache?.retiredTargets ?? []).filter(isThisSession);
|
|
1945
|
-
const retiredKeys = new Set(retiredCarry.map(entryPathKey));
|
|
1946
|
-
/**
|
|
1947
|
-
* Deferral entries of THIS session that this turn has settled: dispatched
|
|
1948
|
-
* into `targets`, retired at the cap, or dropped as unrunnable. Only
|
|
1949
|
-
* these may leave the persisted list, and only when this turn's own write
|
|
1950
|
-
* is not re-asserting them (a cut target comes straight back with
|
|
1951
|
-
* `attempts + 1`).
|
|
1952
|
-
*/
|
|
1953
|
-
const resolvedDeferralKeys = new Set();
|
|
1954
|
-
/**
|
|
1955
|
-
* #2522 review round 4, I5: entries this turn could not fit under
|
|
1956
|
-
* `TEST_RUNNER_MAX_TARGETS`. They were not run, so they keep their attempt
|
|
1957
|
-
* count and stay on the list — round 3 folded the over-cap case in with
|
|
1958
|
-
* "no longer resolves" and dropped them outright, so a carry larger than
|
|
1959
|
-
* the cap lost its tail every turn under a dbg line that said the files
|
|
1960
|
-
* were gone.
|
|
1961
|
-
*/
|
|
1962
|
-
const heldDeferred = [];
|
|
1963
|
-
/**
|
|
1964
|
-
* The ONE writer of `test-runner-findings` in this block, so the rules
|
|
1965
|
-
* about what survives a write are stated once instead of at six call sites
|
|
1966
|
-
* that a seventh would silently not join (#2522 review round 3, F1; same
|
|
1967
|
-
* net-count rule as `retireTestFindings` in `runtime-context.ts`).
|
|
1968
|
-
*
|
|
1969
|
-
* Three rules, all of them round-4 invariants:
|
|
1970
|
-
* - I1: the LIVE record is re-read here and merged into, never replaced.
|
|
1971
|
-
* A row belonging to another session is untouchable — this turn may
|
|
1972
|
-
* ignore it, never delete it.
|
|
1973
|
-
* - I2: every write merges. A batch that publishes clean, a batch that
|
|
1974
|
-
* publishes failures and a superseded batch all go through this, so
|
|
1975
|
-
* none of them can overwrite a concurrent batch's cut set. Removal is
|
|
1976
|
-
* narrow by construction: an entry leaves only if it is this session's,
|
|
1977
|
-
* this turn settled it, and this write is not re-asserting it.
|
|
1978
|
-
* - the persisted lists are bounded (`TEST_RUNNER_MAX_PERSISTED_TARGETS`)
|
|
1979
|
-
* with this session's rows ordered LAST, so the bound sheds foreign
|
|
1980
|
-
* rows first and can never evict what this turn just recorded.
|
|
1981
|
-
*/
|
|
1982
|
-
const boundPersisted = (entries, label) => {
|
|
1983
|
-
const ordered = [
|
|
1984
|
-
...entries.filter((entry) => !isThisSession(entry)),
|
|
1985
|
-
...entries.filter(isThisSession),
|
|
1986
|
-
];
|
|
1987
|
-
if (ordered.length <= TEST_RUNNER_MAX_PERSISTED_TARGETS)
|
|
1988
|
-
return ordered;
|
|
1989
|
-
dbg(`turn_end: ${label} test target list held ${ordered.length} entries, bounded to the newest ${TEST_RUNNER_MAX_PERSISTED_TARGETS} (oldest foreign-session rows dropped)`);
|
|
1990
|
-
return ordered.slice(-TEST_RUNNER_MAX_PERSISTED_TARGETS);
|
|
1991
|
-
};
|
|
1992
|
-
const writeTestFindings = (record, ownDeferred) => {
|
|
1993
|
-
const live = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
1994
|
-
const own = mergeDeferredTargets(heldDeferred, ownDeferred);
|
|
1995
|
-
const ownKeys = new Set(own.map(entryPathKey));
|
|
1996
|
-
const merged = mergeDeferredTargets(live?.deferredTargets ?? [], own).filter((entry) => {
|
|
1997
|
-
if (!isThisSession(entry))
|
|
1998
|
-
return true;
|
|
1999
|
-
const key = entryPathKey(entry);
|
|
2000
|
-
return !resolvedDeferralKeys.has(key) || ownKeys.has(key);
|
|
2001
|
-
});
|
|
2002
|
-
cacheManager.writeCache("test-runner-findings", {
|
|
2003
|
-
...record,
|
|
2004
|
-
deferredTargets: boundPersisted(merged, "deferred"),
|
|
2005
|
-
retiredTargets: boundPersisted(mergeDeferredTargets(live?.retiredTargets ?? [], retiredCarry), "retired"),
|
|
2006
|
-
}, cwd);
|
|
2007
|
-
};
|
|
2008
|
-
let deadDeferred = 0;
|
|
2009
|
-
let heldOverCap = 0;
|
|
2010
|
-
let foreignDeferred = 0;
|
|
2011
|
-
let redispatchedDeferred = 0;
|
|
2012
|
-
let retiredThisTurn = 0;
|
|
2013
|
-
for (const carried of carriedDeferred) {
|
|
2014
|
-
const testFile = path.resolve(cwd, carried.testFile);
|
|
2015
|
-
const carriedKey = normalizeMapKey(testFile);
|
|
2016
|
-
// I1: another session still owes this run. Skip it, leave it on the
|
|
2017
|
-
// record — `writeTestFindings` never removes a row this turn does not
|
|
2018
|
-
// own.
|
|
2019
|
-
if (!isThisSession(carried)) {
|
|
2020
|
-
foreignDeferred++;
|
|
2021
|
-
continue;
|
|
2022
|
-
}
|
|
2023
|
-
if (seen.has(carriedKey)) {
|
|
2024
|
-
resolvedDeferralKeys.add(carriedKey);
|
|
2025
|
-
continue;
|
|
2026
|
-
}
|
|
2027
|
-
const attempts = carried.attempts ?? 0;
|
|
2028
|
-
if (attempts >= TEST_RUNNER_MAX_DEFERRALS) {
|
|
2029
|
-
resolvedDeferralKeys.add(carriedKey);
|
|
2030
|
-
if (!retiredKeys.has(carriedKey)) {
|
|
2031
|
-
retiredKeys.add(carriedKey);
|
|
2032
|
-
retiredThisTurn++;
|
|
2033
|
-
retiredCarry.push({
|
|
2034
|
-
testFile,
|
|
2035
|
-
runner: carried.runner,
|
|
2036
|
-
attempts,
|
|
2037
|
-
sessionId: turnSessionId,
|
|
2038
|
-
});
|
|
2039
|
-
// Never silent, and counted rather than once-per-subject: this is
|
|
2040
|
-
// the ledger entry that names WHICH suite is too slow to belong in
|
|
2041
|
-
// a per-turn batch at all.
|
|
2042
|
-
incrementDegradationCount({
|
|
2043
|
-
kind: "test-runner-batch-capped",
|
|
2044
|
-
subject: `${cwd}:deferral-exhausted`,
|
|
2045
|
-
reason: `test target ${path.relative(cwd, testFile)} was cut at the turn-end batch budget ${attempts} turn(s) running and is retired from turn-end selection for the rest of this session — too slow for a per-turn batch, run it explicitly`,
|
|
2046
|
-
});
|
|
2047
|
-
dbg(`turn_end: retiring deferred test target ${path.relative(cwd, testFile)} after ${attempts} cut batch(es) — too slow for the turn-end budget, run it explicitly`);
|
|
2048
|
-
}
|
|
2049
|
-
continue;
|
|
2050
|
-
}
|
|
2051
|
-
// A RunnerConfig carries functions, so the cache stores the runner KEY
|
|
2052
|
-
// and the config is re-resolved here from the single registry.
|
|
2053
|
-
const config = RUNNERS[carried.runner];
|
|
2054
|
-
if (!config ||
|
|
2055
|
-
isExcludedTestTarget(testFile, cwd) ||
|
|
2056
|
-
!fs.existsSync(testFile)) {
|
|
2057
|
-
resolvedDeferralKeys.add(carriedKey);
|
|
2058
|
-
deadDeferred++;
|
|
2059
|
-
continue;
|
|
2060
|
-
}
|
|
2061
|
-
if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
|
|
2062
|
-
// I5: not settled — held, with `attempts` UNCHANGED. It was never
|
|
2063
|
-
// dispatched, so charging it toward retirement would retire a suite
|
|
2064
|
-
// this turn simply had no room for.
|
|
2065
|
-
heldOverCap++;
|
|
2066
|
-
heldDeferred.push({
|
|
2067
|
-
testFile,
|
|
2068
|
-
runner: carried.runner,
|
|
2069
|
-
sourceFile: carried.sourceFile ?? testFile,
|
|
2070
|
-
attempts,
|
|
2071
|
-
sessionId: turnSessionId,
|
|
2072
|
-
});
|
|
2073
|
-
continue;
|
|
2074
|
-
}
|
|
2075
|
-
resolvedDeferralKeys.add(carriedKey);
|
|
2076
|
-
seen.add(carriedKey);
|
|
2077
|
-
redispatchedDeferred++;
|
|
2078
|
-
targets.push({
|
|
2079
|
-
testFile,
|
|
2080
|
-
sourceFile: carried.sourceFile ?? testFile,
|
|
2081
|
-
runner: carried.runner,
|
|
2082
|
-
config,
|
|
2083
|
-
strategy: "deferred",
|
|
2084
|
-
deferralAttempts: attempts,
|
|
2085
|
-
});
|
|
2086
|
-
dbg(`turn_end: re-running deferred test target ${path.relative(cwd, testFile)} (${carried.runner}) from the previous turn's cut batch`);
|
|
2087
|
-
}
|
|
2088
|
-
if (deadDeferred > 0) {
|
|
2089
|
-
dbg(`turn_end: dropped ${deadDeferred} deferred test target(s) that no longer resolve (missing file, excluded, or unknown runner)`);
|
|
2090
|
-
}
|
|
2091
|
-
if (heldOverCap > 0) {
|
|
2092
|
-
dbg(`turn_end: held ${heldOverCap} deferred test target(s) over the per-turn cap of ${TEST_RUNNER_MAX_TARGETS} — kept on the deferral list for the next turn, attempts unchanged`);
|
|
2093
|
-
}
|
|
2094
|
-
if (foreignDeferred > 0) {
|
|
2095
|
-
dbg(`turn_end: ignored ${foreignDeferred} deferred test target(s) belonging to another session — left on the record for the session that cut them`);
|
|
2096
|
-
}
|
|
2097
|
-
if (carriedDeferred.length > 0) {
|
|
2098
|
-
// #2522 review round 4, S1: the ONE pushed record for this path. The
|
|
2099
|
-
// MCP / Stop-hook route calls `handleTurnEnd` with `dbg: noop`, so every
|
|
2100
|
-
// line above is invisible there and the deferral machinery — the part
|
|
2101
|
-
// that decides whether a suite runs at all — left no trace whatsoever
|
|
2102
|
-
// on the route that fires it most.
|
|
2103
|
-
logLatency({
|
|
2104
|
-
type: "phase",
|
|
2105
|
-
toolName: "turn_end",
|
|
2106
|
-
filePath: cwd,
|
|
2107
|
-
phase: "test_runner_deferral",
|
|
2108
|
-
durationMs: 0,
|
|
2109
|
-
metadata: {
|
|
2110
|
-
carried: carriedDeferred.length,
|
|
2111
|
-
redispatched: redispatchedDeferred,
|
|
2112
|
-
heldOverCap,
|
|
2113
|
-
retired: retiredThisTurn,
|
|
2114
|
-
dropped: deadDeferred,
|
|
2115
|
-
foreignSession: foreignDeferred,
|
|
2116
|
-
sessionId: turnSessionId,
|
|
2117
|
-
},
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
let overCapTargets = 0;
|
|
2121
|
-
let missingTargetFiles = 0;
|
|
2122
|
-
let excludedTargets = 0;
|
|
2123
|
-
let retiredSkips = 0;
|
|
2124
|
-
for (const { display, abs, isNeighbor } of candidates) {
|
|
2125
|
-
const target = testRunnerClient.getTestRunTarget(abs, cwd, runtime.turnIndex);
|
|
2126
|
-
const targetKey = target ? normalizeMapKey(target.testFile) : "";
|
|
2127
|
-
if (target && !seen.has(targetKey)) {
|
|
2128
|
-
seen.add(targetKey);
|
|
2129
|
-
// #2522 review round 3, F1: THE gate that makes the deferral cap a
|
|
2130
|
-
// cap. Whichever strategy produced this target — related, self,
|
|
2131
|
-
// failed-first — a target already retired this session for
|
|
2132
|
-
// outrunning the whole batch budget is not fired again. Without
|
|
2133
|
-
// this the retire branch above was decorative: it logged, and the
|
|
2134
|
-
// candidate loop three lines down re-resolved the same file and
|
|
2135
|
-
// spawned it anyway, with its attempt counter reset to zero.
|
|
2136
|
-
if (retiredKeys.has(targetKey)) {
|
|
2137
|
-
retiredSkips++;
|
|
2138
|
-
dbg(`turn_end: ${display} → test target retired earlier this session (outran the turn-end batch budget), skipping spawn (${path.relative(cwd, target.testFile)})`);
|
|
2139
|
-
continue;
|
|
2140
|
-
}
|
|
2141
|
-
// #2522: built-in exclusion for turn-end SELECTION — a resolved
|
|
2142
|
-
// target under an integration/e2e directory or naming convention
|
|
2143
|
-
// is never auto-fired, whichever strategy (failed-first/related/
|
|
2144
|
-
// self) produced it. No per-project config knob (maintainer
|
|
2145
|
-
// decision); see `TURN_END_EXCLUDED_TEST_GLOBS`.
|
|
2146
|
-
// LATENT HAZARD, deliberately left as-is: this `continue` skips the
|
|
2147
|
-
// candidate entirely, including `retireMissingFailedTargets`. If an
|
|
2148
|
-
// excluded target were ever seeded into the persisted failed set
|
|
2149
|
-
// (it cannot be today — nothing writes that set except a runner
|
|
2150
|
-
// RESULT, and an excluded target is never run, so it can never
|
|
2151
|
-
// produce one), it would sit there unretired forever, chosen by
|
|
2152
|
-
// the failed-first strategy on every turn and dropped here on
|
|
2153
|
-
// every turn. Any future writer of the failed set must retire
|
|
2154
|
-
// excluded entries at the write site, not here.
|
|
2155
|
-
if (isExcludedTestTarget(target.testFile, cwd)) {
|
|
2156
|
-
excludedTargets++;
|
|
2157
|
-
dbg(`turn_end: ${display} → test target excluded (integration/e2e), skipping spawn (${path.relative(cwd, target.testFile)})`);
|
|
2158
|
-
continue;
|
|
2159
|
-
}
|
|
2160
|
-
// #2504: a conventional target that no longer exists on disk still
|
|
2161
|
-
// cost a full runner spawn, which came back "Test file not found"
|
|
2162
|
-
// and was then dropped as an expected skip further down. 9 of the
|
|
2163
|
-
// reported turn's 59 spawns were this. One statSync is cheaper
|
|
2164
|
-
// than a vitest process by four orders of magnitude.
|
|
2165
|
-
if (!fs.existsSync(target.testFile)) {
|
|
2166
|
-
missingTargetFiles++;
|
|
2167
|
-
dbg(`turn_end: ${display} → test file missing, skipping spawn (${path.relative(cwd, target.testFile)})`);
|
|
2168
|
-
continue;
|
|
2169
|
-
}
|
|
2170
|
-
if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
|
|
2171
|
-
overCapTargets++;
|
|
2172
|
-
continue;
|
|
2173
|
-
}
|
|
2174
|
-
targets.push({ ...target, sourceFile: abs });
|
|
2175
|
-
dbg(`turn_end: ${display} → test ${target.runner} ${path.relative(cwd, target.testFile)} (${target.strategy}${isNeighbor ? ", cascade-neighbor" : ""})`);
|
|
2176
|
-
}
|
|
2177
|
-
else if (!target) {
|
|
2178
|
-
dbg(`turn_end: ${display} → no test file found${isNeighbor ? " (cascade-neighbor)" : ""}`);
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
if (excludedTargets > 0) {
|
|
2182
|
-
dbg(`turn_end: excluded ${excludedTargets} test target(s) under the built-in integration/e2e exclusion list`);
|
|
2183
|
-
}
|
|
2184
|
-
if (retiredSkips > 0) {
|
|
2185
|
-
dbg(`turn_end: skipped ${retiredSkips} test target(s) retired earlier this session for outrunning the turn-end batch budget`);
|
|
2186
|
-
}
|
|
2187
|
-
if (missingTargetFiles > 0) {
|
|
2188
|
-
dbg(`turn_end: skipped ${missingTargetFiles} test target(s) whose file no longer exists`);
|
|
2189
|
-
}
|
|
2190
|
-
if (overCapTargets > 0) {
|
|
2191
|
-
// Never silent: the agent is told that some of this turn's tests were
|
|
2192
|
-
// not run, rather than reading an all-green batch that covered part
|
|
2193
|
-
// of the edit set.
|
|
2194
|
-
recordDegradationOnce({
|
|
2195
|
-
kind: "test-runner-batch-capped",
|
|
2196
|
-
subject: cwd,
|
|
2197
|
-
reason: `turn touched more test targets than one turn may fire; ran ${TEST_RUNNER_MAX_TARGETS}, skipped ${overCapTargets} — re-run the remainder with lens_diagnostics or edit them in a smaller batch`,
|
|
2198
|
-
});
|
|
2199
|
-
dbg(`turn_end: test target count capped at ${TEST_RUNNER_MAX_TARGETS}, ${overCapTargets} skipped`);
|
|
2200
|
-
}
|
|
2201
|
-
if (targets.length > 0) {
|
|
2202
|
-
for (const target of targets) {
|
|
2203
|
-
target.fileSeqAtRun = runtime.getFileSeq(target.sourceFile);
|
|
2204
|
-
}
|
|
2205
|
-
dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking, max ${TEST_RUNNER_BATCH_CONCURRENCY} concurrent)`);
|
|
2206
|
-
const firedAtTurn = runtime.turnIndex;
|
|
2207
|
-
const firedSessionId = turnSessionId;
|
|
2208
|
-
const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
|
|
2209
|
-
const provenanceFiles = [
|
|
2210
|
-
...candidates.map((candidate) => ({
|
|
2211
|
-
path: candidate.abs,
|
|
2212
|
-
role: "source",
|
|
2213
|
-
})),
|
|
2214
|
-
...targets.map((target) => ({
|
|
2215
|
-
path: target.testFile,
|
|
2216
|
-
role: "test",
|
|
2217
|
-
})),
|
|
2218
|
-
];
|
|
2219
|
-
const launchedFrom = snapshotAdvisoryProvenance({
|
|
2220
|
-
cwd,
|
|
2221
|
-
runtime,
|
|
2222
|
-
generation: testRunGeneration,
|
|
2223
|
-
files: provenanceFiles,
|
|
2224
|
-
});
|
|
2225
|
-
writeTestFindings({
|
|
2226
|
-
...(priorTestCache ?? { content: "" }),
|
|
2227
|
-
testRunGeneration,
|
|
2228
|
-
},
|
|
2229
|
-
// Consumed above into `targets` (or retired, or dropped). Anything
|
|
2230
|
-
// this turn HELD is re-asserted by the writer itself; the batch's own
|
|
2231
|
-
// outcome adds the NEW cut set below.
|
|
2232
|
-
[]);
|
|
2233
|
-
runTestTargetsBounded({
|
|
2234
|
-
targets,
|
|
2235
|
-
concurrency: TEST_RUNNER_BATCH_CONCURRENCY,
|
|
2236
|
-
budgetMs: TEST_RUNNER_BATCH_BUDGET_MS,
|
|
2237
|
-
// Both bounds, per AGENTS.md: a wall budget AND the ambient
|
|
2238
|
-
// abort signal the rest of the spawn layer already honours.
|
|
2239
|
-
signal: getAmbientAbortSignal(),
|
|
2240
|
-
run: (t, batchSignal) => testRunnerClient.runTestFileAsync(t.testFile, cwd, {
|
|
2241
|
-
runner: t.runner,
|
|
2242
|
-
config: t.config,
|
|
2243
|
-
turnIndex: firedAtTurn,
|
|
2244
|
-
// #2522 R2 F1: the BATCH's signal, so spending the 20s wall
|
|
2245
|
-
// budget kills this spawn rather than letting it run out its
|
|
2246
|
-
// own 60s timeout behind a batch that has already returned.
|
|
2247
|
-
signal: batchSignal,
|
|
2248
|
-
}),
|
|
2249
|
-
})
|
|
2250
|
-
.then(({ results, deferred, stopReason }) => {
|
|
2251
|
-
const settledResults = results;
|
|
2252
|
-
const verdicts = settledResults.flatMap((result) => {
|
|
2253
|
-
if (result.status === "rejected")
|
|
2254
|
-
return [];
|
|
2255
|
-
const target = targets.find((candidate) => candidate.testFile === result.value.file);
|
|
2256
|
-
return target
|
|
2257
|
-
? [
|
|
2258
|
-
{
|
|
2259
|
-
file: result.value.file,
|
|
2260
|
-
sourceFile: target.sourceFile,
|
|
2261
|
-
fileSeq: target.fileSeqAtRun === undefined
|
|
2262
|
-
? {
|
|
2263
|
-
state: "unknown",
|
|
2264
|
-
reason: "sequence-unavailable",
|
|
2265
|
-
}
|
|
2266
|
-
: {
|
|
2267
|
-
state: "known",
|
|
2268
|
-
value: target.fileSeqAtRun,
|
|
2269
|
-
},
|
|
2270
|
-
},
|
|
2271
|
-
]
|
|
2272
|
-
: [];
|
|
2273
|
-
});
|
|
2274
|
-
const deferredTargets = deferred.map((t) => ({
|
|
2275
|
-
testFile: t.testFile,
|
|
2276
|
-
sourceFile: t.sourceFile,
|
|
2277
|
-
runner: t.runner,
|
|
2278
|
-
// One more cut batch for this target. Read back by the
|
|
2279
|
-
// selection loop above, which retires it at
|
|
2280
|
-
// TEST_RUNNER_MAX_DEFERRALS rather than carrying it forever.
|
|
2281
|
-
attempts: (t.deferralAttempts ?? 0) + 1,
|
|
2282
|
-
// #2522 R3 F5: stamped with the session that cut it, so the
|
|
2283
|
-
// next session re-arms this target instead of inheriting an
|
|
2284
|
-
// attempt count measured under a load it never saw.
|
|
2285
|
-
sessionId: firedSessionId,
|
|
2286
|
-
}));
|
|
2287
|
-
if (deferred.length > 0) {
|
|
2288
|
-
// #2522 review round 2, F4: `incrementDegradationCount`, not
|
|
2289
|
-
// `recordDegradationOnce`. At a 20s budget this is a
|
|
2290
|
-
// RECURRING event, and a once-per-subject entry reports the
|
|
2291
|
-
// tenth cut batch of a session exactly like the first.
|
|
2292
|
-
incrementDegradationCount({
|
|
2293
|
-
kind: "test-runner-batch-capped",
|
|
2294
|
-
subject: `${cwd}:${stopReason ?? "incomplete"}`,
|
|
2295
|
-
reason: `test batch stopped early (${stopReason ?? "incomplete"}); ${deferred.length} target(s) killed or never dispatched, deferred to the next turn`,
|
|
2296
|
-
});
|
|
2297
|
-
dbg(`turn_end: test batch stopped early (${stopReason ?? "incomplete"}), ${deferred.length} target(s) deferred to the next turn`);
|
|
2298
|
-
}
|
|
2299
|
-
const deferralNote = deferred.length > 0
|
|
2300
|
-
? `${deferred.length} test target(s) did not finish within the turn-end batch budget and are deferred to the next turn (they run first): ${deferredTargets
|
|
2301
|
-
.map((t) => path.relative(cwd, t.testFile))
|
|
2302
|
-
.join(", ")}`
|
|
2303
|
-
: "";
|
|
2304
|
-
/**
|
|
2305
|
-
* A newer batch has already published for this project — this
|
|
2306
|
-
* one's results are stale by construction and must not overwrite
|
|
2307
|
-
* it. Was inlined three times; extracted once round 2 added a
|
|
2308
|
-
* fourth call site.
|
|
2309
|
-
*/
|
|
2310
|
-
const supersededByNewerGeneration = (label) => {
|
|
2311
|
-
const current = cacheManager.readCache("test-runner-findings", cwd)?.data;
|
|
2312
|
-
const currentGeneration = current?.testRunGeneration;
|
|
2313
|
-
if (currentGeneration !== undefined &&
|
|
2314
|
-
currentGeneration > testRunGeneration) {
|
|
2315
|
-
dbg(`turn_end: ${label}test generation ${testRunGeneration} superseded by ${currentGeneration}`);
|
|
2316
|
-
// #2522 review round 3, F3: returning here USED to drop this
|
|
2317
|
-
// batch's cut targets entirely. The newer batch's own pre-run
|
|
2318
|
-
// write already cleared them, and it never saw this set — so
|
|
2319
|
-
// with two overlapping batches the targets the older one was
|
|
2320
|
-
// cut on were never deferred, never re-selected, and the suite
|
|
2321
|
-
// they belong to simply never ran again. Hand them over
|
|
2322
|
-
// instead. Round 4, I2: the merge is `writeTestFindings`'s own
|
|
2323
|
-
// rule now, not a second copy of it here — every branch below
|
|
2324
|
-
// merges the same way, so a NEWER batch publishing after this
|
|
2325
|
-
// one no longer flattens what this hand-over just recorded.
|
|
2326
|
-
if (deferredTargets.length > 0) {
|
|
2327
|
-
writeTestFindings({ ...(current ?? { content: "" }) }, deferredTargets);
|
|
2328
|
-
dbg(`turn_end: ${label}carried ${deferredTargets.length} cut target(s) into the newer generation's deferral set`);
|
|
2329
|
-
}
|
|
2330
|
-
return true;
|
|
2331
|
-
}
|
|
2332
|
-
return false;
|
|
2333
|
-
};
|
|
2334
|
-
const publishedAgainst = snapshotAdvisoryProvenance({
|
|
2335
|
-
cwd,
|
|
2336
|
-
runtime,
|
|
2337
|
-
generation: testRunGeneration,
|
|
2338
|
-
files: provenanceFiles,
|
|
2339
|
-
});
|
|
2340
|
-
const superseded = launchedFrom.revision.sessionId !==
|
|
2341
|
-
publishedAgainst.revision.sessionId ||
|
|
2342
|
-
launchedFrom.revision.projectSeq !==
|
|
2343
|
-
publishedAgainst.revision.projectSeq ||
|
|
2344
|
-
launchedFrom.revision.turnIndex !==
|
|
2345
|
-
publishedAgainst.revision.turnIndex ||
|
|
2346
|
-
launchedFrom.files.some((file, index) => publishedAgainst.files[index]?.sha256 !== file.sha256 ||
|
|
2347
|
-
publishedAgainst.files[index]?.path !== file.path);
|
|
2348
|
-
// #628: the turn advancing while tests ran no longer means the
|
|
2349
|
-
// results are thrown away — a late result is still real
|
|
2350
|
-
// information about what's currently broken. It's tagged `stale`
|
|
2351
|
-
// so a downstream consumer can distinguish it from a result that
|
|
2352
|
-
// arrived in time, but it's cached either way.
|
|
2353
|
-
const stale = runtime.turnIndex !== firedAtTurn;
|
|
2354
|
-
const failures = [];
|
|
2355
|
-
const resultValues = [];
|
|
2356
|
-
let rejectedCount = 0;
|
|
2357
|
-
// #2522: whether ANY reported item is a genuine failing test
|
|
2358
|
-
// (`failed > 0`) as opposed to the runner itself never
|
|
2359
|
-
// completing (timeout, missing provider/binary, a rejected
|
|
2360
|
-
// promise). Drives the delivery framing below — a batch made
|
|
2361
|
-
// up ENTIRELY of runner errors is not something the agent
|
|
2362
|
-
// introduced, so it must not read as "fix before continuing".
|
|
2363
|
-
let hasRealFailure = false;
|
|
2364
|
-
for (const r of results) {
|
|
2365
|
-
if (r.status === "rejected") {
|
|
2366
|
-
rejectedCount++;
|
|
2367
|
-
emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:rejected`, {
|
|
2368
|
-
filePath: cwd,
|
|
2369
|
-
durationMs: 0,
|
|
2370
|
-
metadata: {
|
|
2371
|
-
outcome: "runner-promise-rejected",
|
|
2372
|
-
sessionId: firedSessionId,
|
|
2373
|
-
generation: testRunGeneration,
|
|
2374
|
-
targetCount: targets.length,
|
|
2375
|
-
droppedDetailCount: 0,
|
|
2376
|
-
reason: String(r.reason).slice(0, 500),
|
|
2377
|
-
},
|
|
2378
|
-
}, {
|
|
2379
|
-
ledgerKind: "test-runner-delivery",
|
|
2380
|
-
reason: "test runner promise rejected",
|
|
2381
|
-
capPerTurn: { limit: 8, turnIndex: firedAtTurn },
|
|
2382
|
-
});
|
|
2383
|
-
dbg(`turn_end: test run rejected — ${r.reason}`);
|
|
2384
|
-
continue;
|
|
2385
|
-
}
|
|
2386
|
-
resultValues.push(r.value);
|
|
2387
|
-
const { file, runner, passed, failed, duration, error } = r.value;
|
|
2388
|
-
if (failed > 0)
|
|
2389
|
-
hasRealFailure = true;
|
|
2390
|
-
const shortFile = path.basename(file);
|
|
2391
|
-
// #1479: `(0ms)` used to be printed for a run nobody
|
|
2392
|
-
// timed — a payload with no suite timestamps, an
|
|
2393
|
-
// unrecognised summary line, or an empty result — and
|
|
2394
|
-
// that is the same string a genuinely sub-millisecond
|
|
2395
|
-
// run produces. A reader could not tell "measured 0"
|
|
2396
|
-
// from "not measured", which is the confusion #1452 was
|
|
2397
|
-
// reported for. `duration` is now absent when it was
|
|
2398
|
-
// never measured, and this line says which one it has.
|
|
2399
|
-
//
|
|
2400
|
-
// #1480: the test is `formatRunDurationMs`, not an
|
|
2401
|
-
// inline comparison. The "absent = unmeasured" contract
|
|
2402
|
-
// was being re-derived at every site that read a
|
|
2403
|
-
// duration, and a site that gets it slightly wrong —
|
|
2404
|
-
// treating a measured `0` as absent — puts the bug back
|
|
2405
|
-
// without touching this comment.
|
|
2406
|
-
const elapsed = formatRunDurationMs(duration);
|
|
2407
|
-
// Lifted out of the template below for the same reason
|
|
2408
|
-
// `elapsed` is: the pair read as a nested ternary, which
|
|
2409
|
-
// this line only got flagged for because #1479 touched it.
|
|
2410
|
-
const verdict = failed > 0 ? "FAIL" : "PASS";
|
|
2411
|
-
// #2532 review S1: folded onto `isRunnerErrorResult` instead of
|
|
2412
|
-
// re-deriving `error && passed === 0 && failed === 0` here — that
|
|
2413
|
-
// local spelling missed a runner error reported alongside partial
|
|
2414
|
-
// passes (pytest `Interrupted` after some tests already passed),
|
|
2415
|
-
// which read as a clean "PASS Np/0f" line with the error silently
|
|
2416
|
-
// dropped. A partial run says so explicitly rather than reading
|
|
2417
|
-
// as a clean pass.
|
|
2418
|
-
const summary = isRunnerErrorResult(r.value)
|
|
2419
|
-
? passed > 0
|
|
2420
|
-
? `error: ${error} (${passed} passed before)`
|
|
2421
|
-
: `error: ${error}`
|
|
2422
|
-
: `${verdict} ${passed}p/${failed}f (${elapsed})`;
|
|
2423
|
-
dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} → ${summary}`);
|
|
2424
|
-
// #1524: also fires on `error` alone, not just `failed > 0`.
|
|
2425
|
-
// A runner error (the suite never started, or was
|
|
2426
|
-
// interrupted before finishing — spawn/config/timeout
|
|
2427
|
-
// failure) can arrive with `failed === 0` even when tests
|
|
2428
|
-
// DID pass before it (#2532 review S2 — not "by
|
|
2429
|
-
// construction": `parsePytestOutput` sets `error` from the
|
|
2430
|
-
// exit code independently of the parsed counts, so
|
|
2431
|
-
// `isRunnerErrorResult` is `failed === 0 && !!error`, not an
|
|
2432
|
-
// invariant elsewhere). Gating on `failed > 0` alone dropped
|
|
2433
|
-
// it silently: the agent got no context at all, and the
|
|
2434
|
-
// empty `failures` array below sent this result down the
|
|
2435
|
-
// "all tests passed" branch, clearing any prior real
|
|
2436
|
-
// test-failure git-guard blocker. `formatResult` already
|
|
2437
|
-
// renders the error-only case as "Could not run tests: ...".
|
|
2438
|
-
if (failed > 0 || error) {
|
|
2439
|
-
// #2028: "Test file not found" is an expected skip
|
|
2440
|
-
// (conventional test path without an actual file),
|
|
2441
|
-
// not an actionable failure. Don't surface it.
|
|
2442
|
-
if (error &&
|
|
2443
|
-
String(r.value?.error ?? "").includes("Test file not found")) {
|
|
2444
|
-
continue;
|
|
2445
|
-
}
|
|
2446
|
-
const formatted = testRunnerClient.formatResult(r.value);
|
|
2447
|
-
if (formatted)
|
|
2448
|
-
failures.push(formatted);
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
if (rejectedCount > 0) {
|
|
2452
|
-
failures.push(`Test runner rejected ${rejectedCount} promise(s) before producing a structured result.`);
|
|
2453
|
-
}
|
|
2454
|
-
if (failures.length > 0) {
|
|
2455
|
-
if (supersededByNewerGeneration(""))
|
|
2456
|
-
return;
|
|
2457
|
-
const content = [
|
|
2458
|
-
stale
|
|
2459
|
-
? "[from a prior turn — the edit that triggered this run had already been superseded by the time results came back]"
|
|
2460
|
-
: "",
|
|
2461
|
-
failures.join("\n\n"),
|
|
2462
|
-
// A batch can BOTH find a real failure and be cut short.
|
|
2463
|
-
// The agent needs to see the failure AND that the run was
|
|
2464
|
-
// incomplete, or it reads a partial list as the full one.
|
|
2465
|
-
deferralNote,
|
|
2466
|
-
]
|
|
2467
|
-
.filter(Boolean)
|
|
2468
|
-
.join("\n\n");
|
|
2469
|
-
// #2522: nothing in this batch is a genuine failing test —
|
|
2470
|
-
// every entry is a runner error (timeout, missing
|
|
2471
|
-
// provider/binary, a rejected promise). `peekTestFindings`
|
|
2472
|
-
// reads this to deliver advisory framing instead of
|
|
2473
|
-
// "fix before continuing", since the agent introduced
|
|
2474
|
-
// nothing here to fix.
|
|
2475
|
-
const runnerErrorOnly = !hasRealFailure;
|
|
2476
|
-
writeTestFindings({
|
|
2477
|
-
content,
|
|
2478
|
-
stale,
|
|
2479
|
-
results: resultValues,
|
|
2480
|
-
verdicts,
|
|
2481
|
-
testRunGeneration,
|
|
2482
|
-
launchedFrom,
|
|
2483
|
-
publishedAgainst,
|
|
2484
|
-
provenance: publishedAgainst,
|
|
2485
|
-
superseded,
|
|
2486
|
-
runnerErrorOnly,
|
|
2487
|
-
}, deferredTargets);
|
|
2488
|
-
try {
|
|
2489
|
-
deps.onTestRunnerComplete?.({
|
|
2490
|
-
cwd,
|
|
2491
|
-
sessionId: firedSessionId,
|
|
2492
|
-
generation: testRunGeneration,
|
|
2493
|
-
targetCount: targets.length,
|
|
2494
|
-
hasFindings: true,
|
|
2495
|
-
});
|
|
2496
|
-
}
|
|
2497
|
-
catch (deliveryErr) {
|
|
2498
|
-
dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
|
|
2499
|
-
}
|
|
2500
|
-
if (getFlag("lens-guard") &&
|
|
2501
|
-
firedSessionId === runtime.telemetrySessionId) {
|
|
2502
|
-
// #1524: `&& !value.error` — a runner-error result has
|
|
2503
|
-
// `failed === 0` (the suite never ran, so nothing could
|
|
2504
|
-
// fail), but it is not a pass. Without the filter it
|
|
2505
|
-
// would clear a prior real test-failure git-guard
|
|
2506
|
-
// blocker on the strength of a suite that never
|
|
2507
|
-
// started. And the call itself is skipped when this
|
|
2508
|
-
// list is empty rather than passed as `[]`:
|
|
2509
|
-
// `clearGitGuardTestFailure`'s own empty-array
|
|
2510
|
-
// fallback treats "no files named" as "clear every
|
|
2511
|
-
// blocked file", so an all-error batch (one go file,
|
|
2512
|
-
// runner-error, zero clean files) would otherwise
|
|
2513
|
-
// clear every blocker through that fallback instead
|
|
2514
|
-
// of clearing none.
|
|
2515
|
-
const cleanFiles = resultValues
|
|
2516
|
-
.filter((value) => value.failed === 0 && !value.error)
|
|
2517
|
-
.map((value) => value.file);
|
|
2518
|
-
if (cleanFiles.length > 0) {
|
|
2519
|
-
clearGitGuardTestFailure(cacheManager, cwd, runtime, cleanFiles);
|
|
2520
|
-
}
|
|
2521
|
-
// #2532: `runnerErrorOnly` (computed above for the turn-end
|
|
2522
|
-
// delivery framing) is true exactly when nothing in this
|
|
2523
|
-
// batch is a genuine failing test — every entry is a
|
|
2524
|
-
// runner error the agent did not introduce. Without this
|
|
2525
|
-
// gate, an all-runner-error batch still called
|
|
2526
|
-
// `mergeGitGuardTestFailure` with an EMPTY failed-files
|
|
2527
|
-
// list, which unconditionally sets `hasBlockers: true`:
|
|
2528
|
-
// the identical event the turn-end message reports as
|
|
2529
|
-
// advisory read as "COMMIT BLOCKED" under --lens-guard.
|
|
2530
|
-
if (!runnerErrorOnly) {
|
|
2531
|
-
mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, resultValues
|
|
2532
|
-
.filter((value) => value.failed > 0)
|
|
2533
|
-
.map((value) => value.file));
|
|
2534
|
-
}
|
|
2535
|
-
else {
|
|
2536
|
-
// #2532 review T2: the skip above is otherwise pull-only —
|
|
2537
|
-
// nothing records that a batch with real content
|
|
2538
|
-
// (`failures.length > 0`) was deliberately kept OFF the
|
|
2539
|
-
// `--lens-guard` blocker because every entry was a runner
|
|
2540
|
-
// error. Same phase/ledger as the rejected-promise event
|
|
2541
|
-
// above, so both `--lens-guard` demotions land in one
|
|
2542
|
-
// queryable place. Reached only inside the enclosing
|
|
2543
|
-
// `getFlag("lens-guard") && firedSessionId === …` check —
|
|
2544
|
-
// no point recording a demotion the flag can't act on.
|
|
2545
|
-
emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:runner-error-only`, {
|
|
2546
|
-
filePath: cwd,
|
|
2547
|
-
durationMs: 0,
|
|
2548
|
-
metadata: {
|
|
2549
|
-
outcome: "runner-error-only-not-blocking",
|
|
2550
|
-
sessionId: firedSessionId,
|
|
2551
|
-
generation: testRunGeneration,
|
|
2552
|
-
targetCount: targets.length,
|
|
2553
|
-
droppedDetailCount: 0,
|
|
2554
|
-
},
|
|
2555
|
-
}, {
|
|
2556
|
-
ledgerKind: "test-runner-delivery",
|
|
2557
|
-
reason: "runner-error-only batch kept off the --lens-guard blocker",
|
|
2558
|
-
capPerTurn: { limit: 8, turnIndex: firedAtTurn },
|
|
2559
|
-
});
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
dbg(`turn_end: ${failures.length} test failure(s) cached for pull diagnostics and post-agent delivery${stale ? " (stale — turn advanced while tests ran)" : ""}`);
|
|
2563
|
-
}
|
|
2564
|
-
else if (deferred.length > 0) {
|
|
2565
|
-
// #2522 review round 2, F1: a PARTIAL batch is not a clean
|
|
2566
|
-
// batch. Pre-round-2 this fell through to the branch below —
|
|
2567
|
-
// `content: ""`, "all tests passed", and a `--lens-guard`
|
|
2568
|
-
// clear — on the strength of a run that never finished. The
|
|
2569
|
-
// record names the deferral instead; the commit gate is left
|
|
2570
|
-
// exactly as it was, because nothing here proves anything
|
|
2571
|
-
// passed that was not already proven.
|
|
2572
|
-
if (supersededByNewerGeneration("deferred "))
|
|
2573
|
-
return;
|
|
2574
|
-
writeTestFindings({
|
|
2575
|
-
content: deferralNote,
|
|
2576
|
-
stale,
|
|
2577
|
-
results: resultValues,
|
|
2578
|
-
verdicts,
|
|
2579
|
-
testRunGeneration,
|
|
2580
|
-
launchedFrom,
|
|
2581
|
-
publishedAgainst,
|
|
2582
|
-
provenance: publishedAgainst,
|
|
2583
|
-
superseded,
|
|
2584
|
-
// Advisory framing: an unfinished batch is not a
|
|
2585
|
-
// failure the agent introduced, exactly like a runner
|
|
2586
|
-
// error. See `TestRunnerFindingsCache.runnerErrorOnly`.
|
|
2587
|
-
runnerErrorOnly: true,
|
|
2588
|
-
}, deferredTargets);
|
|
2589
|
-
try {
|
|
2590
|
-
deps.onTestRunnerComplete?.({
|
|
2591
|
-
cwd,
|
|
2592
|
-
sessionId: firedSessionId,
|
|
2593
|
-
generation: testRunGeneration,
|
|
2594
|
-
targetCount: targets.length,
|
|
2595
|
-
hasFindings: true,
|
|
2596
|
-
});
|
|
2597
|
-
}
|
|
2598
|
-
catch (deliveryErr) {
|
|
2599
|
-
dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
|
|
2600
|
-
}
|
|
2601
|
-
dbg(`turn_end: partial test batch — ${results.length} target(s) settled, ${deferred.length} deferred to the next turn; NOT recorded as a clean run`);
|
|
2602
|
-
}
|
|
2603
|
-
else if (results.length > 0) {
|
|
2604
|
-
if (supersededByNewerGeneration("clean "))
|
|
2605
|
-
return;
|
|
2606
|
-
writeTestFindings({
|
|
2607
|
-
...(priorTestCache ?? { content: "" }),
|
|
2608
|
-
content: "",
|
|
2609
|
-
stale: false,
|
|
2610
|
-
results: resultValues,
|
|
2611
|
-
testRunGeneration,
|
|
2612
|
-
launchedFrom,
|
|
2613
|
-
publishedAgainst,
|
|
2614
|
-
provenance: publishedAgainst,
|
|
2615
|
-
superseded,
|
|
2616
|
-
},
|
|
2617
|
-
// Nothing was cut. This turn's own settled entries drop out;
|
|
2618
|
-
// I2 keeps every row this batch does not own — including a
|
|
2619
|
-
// concurrent older batch's hand-over.
|
|
2620
|
-
[]);
|
|
2621
|
-
try {
|
|
2622
|
-
deps.onTestRunnerComplete?.({
|
|
2623
|
-
cwd,
|
|
2624
|
-
sessionId: firedSessionId,
|
|
2625
|
-
generation: testRunGeneration,
|
|
2626
|
-
targetCount: targets.length,
|
|
2627
|
-
hasFindings: false,
|
|
2628
|
-
});
|
|
2629
|
-
}
|
|
2630
|
-
catch (deliveryErr) {
|
|
2631
|
-
dbg(`turn_end: test delivery staging failed — ${deliveryErr}`);
|
|
2632
|
-
}
|
|
2633
|
-
if (getFlag("lens-guard") &&
|
|
2634
|
-
firedSessionId === runtime.telemetrySessionId) {
|
|
2635
|
-
clearGitGuardTestFailure(cacheManager, cwd, runtime, resultValues.map((value) => value.file));
|
|
2636
|
-
}
|
|
2637
|
-
dbg(`turn_end: all tests passed${stale ? " (stale — turn advanced while tests ran)" : ""}`);
|
|
2638
|
-
}
|
|
2639
|
-
})
|
|
2640
|
-
.catch(() => { });
|
|
2641
|
-
}
|
|
2642
|
-
else if (carriedDeferred.length > 0) {
|
|
2643
|
-
// Nothing fired, so no batch outcome will write the list back, but this
|
|
2644
|
-
// turn still settled part of it (targets retired at the cap, or dropped
|
|
2645
|
-
// because their file is gone). Persist that here rather than re-reading
|
|
2646
|
-
// a set of dead paths on every future turn; `writeTestFindings` carries
|
|
2647
|
-
// this turn's retirements alongside, which is what the deferral-cap
|
|
2648
|
-
// branch above depends on when it retires the LAST carried target and
|
|
2649
|
-
// fires nothing. Rows this turn only HELD (over the cap) or does not
|
|
2650
|
-
// own (another session's) are re-asserted by the writer itself.
|
|
2651
|
-
writeTestFindings({ ...(priorTestCache ?? { content: "" }) }, []);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
if (runtime.errorDebtBaseline && files.length > 0) {
|
|
2655
|
-
dbg("turn_end: marking error debt check for next session");
|
|
2656
|
-
cacheManager.writeCache("errorDebt", {
|
|
2657
|
-
pendingCheck: true,
|
|
2658
|
-
baselineTestsPassed: runtime.errorDebtBaseline.testsPassed,
|
|
2659
|
-
}, cwd);
|
|
2660
|
-
}
|
|
2661
|
-
// Session summaries are intentionally suppressed at turn_end to avoid
|
|
2662
|
-
// distracting the agent with non-blocking telemetry.
|
|
2663
|
-
// Call-graph impact analysis — surface WillBreak/MayBreak callers for modified
|
|
2664
|
-
// symbols. MUST run BEFORE the writeProjectDiagnosticsDeltaReport serialization
|
|
2665
|
-
// below: it is a delta contributor (like knip above), pushing into
|
|
2666
|
-
// projectDiagnosticsDelta / projectDiagnosticsSources. If it ran after the
|
|
2667
|
-
// single write, a call-graph-only turn would persist nothing and a mixed turn
|
|
2668
|
-
// would drop the call-graph entries — so lens_diagnostics (which only reads the
|
|
2669
|
-
// persisted report) would never surface the findings (#179/#533).
|
|
2670
|
-
if (runtime.callGraph && files.length > 0) {
|
|
2671
|
-
const coverage = runtime.callGraph.coverage;
|
|
2672
|
-
if (!coverage || coverage.complete !== true) {
|
|
2673
|
-
// An incomplete graph can still contain useful edges, but emitting them
|
|
2674
|
-
// as ordinary impact findings would turn unsupported/partial extraction
|
|
2675
|
-
// into an authoritative-looking clean result for the rest of the file.
|
|
2676
|
-
// Keep the limitation visible and require a complete graph for this
|
|
2677
|
-
// user-facing impact surface (#1070).
|
|
2678
|
-
// @delivery-surface: runtime-turn:call-graph-advisory
|
|
2679
|
-
advisoryParts.push("Call-graph impact was not emitted because call-graph extraction coverage is incomplete; " +
|
|
2680
|
-
"the affected files may have unreported callers.");
|
|
2681
|
-
}
|
|
2682
|
-
else {
|
|
2683
|
-
const callGraphStart = Date.now();
|
|
2684
|
-
try {
|
|
2685
|
-
const { impact, formatImpact, parseSymbolKey } = await import("./call-graph.js");
|
|
2686
|
-
const { callGraphImpactToProjectDiagnostics } = await import("./project-diagnostics/runner-adapters/call-graph-impact.js");
|
|
2687
|
-
const impactLines = [];
|
|
2688
|
-
/** #3248: bounded per-turn, never per finding. */
|
|
2689
|
-
let callGraphDispositionSuppressed = 0;
|
|
2690
|
-
const impactFindings = [];
|
|
2691
|
-
for (const filePath of files.slice(0, 5)) {
|
|
2692
|
-
// Turn-state files may be cwd-relative while graph keys are absolute,
|
|
2693
|
-
// and persisted graphs can contain either slash style/casing. Compare
|
|
2694
|
-
// through the shared normalized path seam; keep the original filePath
|
|
2695
|
-
// only for display and diagnostics.
|
|
2696
|
-
const changedFileKey = normalizeMapKey(resolveRunnerPath(cwd, filePath));
|
|
2697
|
-
const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => {
|
|
2698
|
-
const graphFilePath = parseSymbolKey(k).filePath;
|
|
2699
|
-
return (normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) ===
|
|
2700
|
-
changedFileKey);
|
|
2701
|
-
});
|
|
2702
|
-
for (const calleeKey of fileCallerKeys.slice(0, 3)) {
|
|
2703
|
-
// #1080: drop KNOWN test-role callers BEFORE both the human advisory
|
|
2704
|
-
// (formatImpact below) and the persisted delta (impactFindings →
|
|
2705
|
-
// callGraphImpactToProjectDiagnostics) — the advisory is rendered
|
|
2706
|
-
// first, so the filter must reach the shared `results` set that feeds
|
|
2707
|
-
// both. A test caller supplied by an old/fixture/expanded graph must
|
|
2708
|
-
// appear in neither surface. Fail-open: an unparseable/unclassifiable
|
|
2709
|
-
// key is retained (the adapter re-applies the same predicate).
|
|
2710
|
-
const results = impact(runtime.callGraph, calleeKey).filter((r) => {
|
|
2711
|
-
const callerFile = parseSymbolKey(r.symbolKey).filePath;
|
|
2712
|
-
return (!callerFile ||
|
|
2713
|
-
!isTestRoleCollateral(resolveRunnerPath(cwd, callerFile)));
|
|
2714
|
-
});
|
|
2715
|
-
if (results.length > 0) {
|
|
2716
|
-
impactFindings.push({ calleeKey, results });
|
|
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) : "";
|
|
2744
|
-
if (summary)
|
|
2745
|
-
impactLines.push(` ${parseSymbolKey(calleeKey).symbolName ?? calleeKey}: ${summary}`);
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
if (impactLines.length > 0) {
|
|
2750
|
-
// @delivery-surface: runtime-turn:call-graph-advisory
|
|
2751
|
-
advisoryParts.push(`📊 Call-graph impact (changed symbols have callers):\n${impactLines.join("\n")}`);
|
|
2752
|
-
}
|
|
2753
|
-
if (impactFindings.length > 0) {
|
|
2754
|
-
const impactDiagnostics = callGraphImpactToProjectDiagnostics(cwd, impactFindings);
|
|
2755
|
-
if (impactDiagnostics.length > 0) {
|
|
2756
|
-
projectDiagnosticsDelta.push(...impactDiagnostics);
|
|
2757
|
-
projectDiagnosticsSources.add("call-graph");
|
|
2758
|
-
}
|
|
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
|
-
}
|
|
2777
|
-
// Non-fatal — call graph is best-effort
|
|
2778
|
-
}
|
|
2779
|
-
catch {
|
|
2780
|
-
// Non-fatal — call graph is best-effort
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
if (projectDiagnosticsDelta.length > 0) {
|
|
2785
|
-
writeProjectDiagnosticsDeltaReport(cwd, {
|
|
2786
|
-
version: PROJECT_DIAGNOSTICS_CACHE_VERSION,
|
|
2787
|
-
cwd,
|
|
2788
|
-
generatedAt: new Date().toISOString(),
|
|
2789
|
-
sessionId: runtime.telemetrySessionId,
|
|
2790
|
-
turnIndex: runtime.turnIndex,
|
|
2791
|
-
projectSeqStart: runtime.turnStartProjectSeq,
|
|
2792
|
-
projectSeqEnd: runtime.projectSeq,
|
|
2793
|
-
diagnostics: projectDiagnosticsDelta,
|
|
2794
|
-
sources: [...projectDiagnosticsSources].sort((a, b) => a.localeCompare(b)),
|
|
2795
|
-
});
|
|
2796
|
-
}
|
|
2797
|
-
const t4 = Date.now();
|
|
2798
|
-
const modifiedRangesByFile = new Map(Object.entries(turnState.files).map(([file, state]) => [
|
|
2799
|
-
normalizeMapKey(resolveRunnerPath(cwd, file)),
|
|
2800
|
-
state.modifiedRanges,
|
|
2801
|
-
]));
|
|
2802
|
-
const getFileSeq = runtime.getFileSeq;
|
|
2803
|
-
const fileSeqByPath = new Map();
|
|
2804
|
-
if (getFileSeq) {
|
|
2805
|
-
for (const file of files) {
|
|
2806
|
-
const filePath = normalizeMapKey(resolveRunnerPath(cwd, file));
|
|
2807
|
-
fileSeqByPath.set(filePath, getFileSeq.call(runtime, filePath));
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
let reverify;
|
|
2811
|
-
if (getFlag("lens-actionable-warnings")) {
|
|
2812
|
-
// #3170: re-verify carried deferred findings before the advisory
|
|
2813
|
-
// assembles — a finding whose file is unchanged re-serves from the
|
|
2814
|
-
// persisted report without ever being re-observed; root-cause-fixed-
|
|
2815
|
-
// elsewhere findings converge here instead of repeating. Bounded in the
|
|
2816
|
-
// module (≤4 files, wall budget, abort signal). #3176 F1: the
|
|
2817
|
-
// replacement entries fold into THIS turn's single in-band publish
|
|
2818
|
-
// below — a separate replacement publish spends the carry marker and
|
|
2819
|
-
// this publish then drops the entries at the scope guard (the blocker
|
|
2820
|
-
// the review caught).
|
|
2821
|
-
const persistedReport = cacheManager.readCache("actionable-warnings", cwd, Number.MAX_SAFE_INTEGER)?.data;
|
|
2822
|
-
if (persistedReport?.files?.some((entry) => entry.origin === "deferred")) {
|
|
2823
|
-
const reverifyLspService = getLSPService();
|
|
2824
|
-
if (reverifyLspService) {
|
|
2825
|
-
// #2523: the hook-path await is bound-wrapped with the hook's own
|
|
2826
|
-
// budget and signal; the pass's internal deadline (3s) is the
|
|
2827
|
-
// tighter of the two.
|
|
2828
|
-
reverify = await bounded(runPersistentReverify({
|
|
2829
|
-
report: persistedReport,
|
|
2830
|
-
cwd,
|
|
2831
|
-
lspService: reverifyLspService,
|
|
2832
|
-
signal: getAmbientAbortSignal(),
|
|
2833
|
-
}), {
|
|
2834
|
-
ms: HOOK_WALL_BUDGET_MS.turn_end,
|
|
2835
|
-
signal: getAmbientAbortSignal(),
|
|
2836
|
-
hook: "turn_end",
|
|
2837
|
-
label: "persistent_reverify",
|
|
2838
|
-
});
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
try {
|
|
2842
|
-
const report = await buildActionableWarningsReport({
|
|
2843
|
-
cwd,
|
|
2844
|
-
sessionId: runtime.telemetrySessionId,
|
|
2845
|
-
turnIndex: runtime.turnIndex,
|
|
2846
|
-
files,
|
|
2847
|
-
modifiedRangesByFile,
|
|
2848
|
-
dispatchWarnings: runtime.peekActionableWarnings(),
|
|
2849
|
-
includeLspCodeActions: !!getFlag("lens-actionable-warning-actions"),
|
|
2850
|
-
projectSeqStart: runtime.turnStartProjectSeq,
|
|
2851
|
-
projectSeqEnd: runtime.projectSeq,
|
|
2852
|
-
fileSeqByPath,
|
|
2853
|
-
deltaOnly: !getFlag("lens-actionable-warning-all"),
|
|
2854
|
-
dbg,
|
|
2855
|
-
// #2504: this call is AWAITED on the turn_end hook, so its cost is
|
|
2856
|
-
// terminal-blocking time. The file cap and wall budget are its
|
|
2857
|
-
// bounds; the abort signal is the second one AGENTS.md requires.
|
|
2858
|
-
signal: getAmbientAbortSignal(),
|
|
2859
|
-
// A cold-cache turn hands the fresh-pull loop back here, off the
|
|
2860
|
-
// hook — it lands in the same cache the in-band report goes to.
|
|
2861
|
-
onDeferredReport: (deferred) => {
|
|
2862
|
-
try {
|
|
2863
|
-
// #2504 review round 2 (F2): GUARDED. This callback fires up
|
|
2864
|
-
// to a minute after the turn that armed it, and the report
|
|
2865
|
-
// it carries is stamped with THAT turn's
|
|
2866
|
-
// turnIndex/projectSeq. Writing it unconditionally
|
|
2867
|
-
// overwrote a newer report, which `agent_end` then rejected
|
|
2868
|
-
// as `project_seq_mismatch` (silently skipping the autofix
|
|
2869
|
-
// pass) and `lens_diagnostics` re-served as a stale delta.
|
|
2870
|
-
// #2504 review round 4 (F1): guarded PER FILE, not per
|
|
2871
|
-
// report. The round-3 shape (publish, or discard whole
|
|
2872
|
-
// when something newer is persisted) composed with
|
|
2873
|
-
// incumbent-wins into "publish nothing" — every turn_end
|
|
2874
|
-
// with modified files persists an in-band report with a
|
|
2875
|
-
// strictly increasing turnIndex, and the decline fires
|
|
2876
|
-
// exactly when such a turn runs while a loop is in
|
|
2877
|
-
// flight, so a decline always implied a supersede. The
|
|
2878
|
-
// merge upserts the entries whose file has not moved and
|
|
2879
|
-
// drops only those that have.
|
|
2880
|
-
writeDeferredActionableWarningsReport({
|
|
2881
|
-
cacheManager,
|
|
2882
|
-
cwd,
|
|
2883
|
-
report: deferred,
|
|
2884
|
-
// The LIVE per-file sequence at WRITE time is the
|
|
2885
|
-
// baseline the merge judges each entry against. The
|
|
2886
|
-
// runtime is the only thing that knows it: a file
|
|
2887
|
-
// edited into cleanliness by a later turn is absent
|
|
2888
|
-
// from the persisted report entirely.
|
|
2889
|
-
getFileSeq: getFileSeq
|
|
2890
|
-
? (filePath) => getFileSeq.call(runtime, filePath)
|
|
2891
|
-
: undefined,
|
|
2892
|
-
dbg,
|
|
2893
|
-
});
|
|
2894
|
-
}
|
|
2895
|
-
catch (deferErr) {
|
|
2896
|
-
dbg(`turn_end: deferred actionable-warnings write failed — ${deferErr}`);
|
|
2897
|
-
}
|
|
2898
|
-
},
|
|
2899
|
-
});
|
|
2900
|
-
// #2504 review round 5 (F1): THE publish choke point. This was a
|
|
2901
|
-
// blind `writeActionableWarningsReport` while the deferred callback
|
|
2902
|
-
// above read-modify-wrote the SAME cache key. The deferred merge can
|
|
2903
|
-
// land anywhere inside this handleTurnEnd -- the cascade settle,
|
|
2904
|
-
// knip, madge, the test batch, the in-band LSP enrichment are all
|
|
2905
|
-
// awaited between the moment it is armed and the moment we get
|
|
2906
|
-
// here -- and the blind write erased it 607 ms later in the
|
|
2907
|
-
// reviewer's trace. The publisher now always reads what is persisted
|
|
2908
|
-
// and merges per file, so the two writers cannot race. It carries
|
|
2909
|
-
// forward only entries a DEFERRAL produced, and only while their
|
|
2910
|
-
// file has not moved, so a `turn_delta` report does not accumulate
|
|
2911
|
-
// every prior turn's findings.
|
|
2912
|
-
// #3176 F1: the re-verify replacements fold into THIS report — one
|
|
2913
|
-
// in-band publish total, so the carry marker is spent exactly once and
|
|
2914
|
-
// the merged report cannot drop the entries behind the publish (the
|
|
2915
|
-
// blocker the review caught).
|
|
2916
|
-
if (reverify?.replacementFiles.length) {
|
|
2917
|
-
const replacementByPath = new Map(reverify.replacementFiles.map((file) => [
|
|
2918
|
-
normalizeMapKey(file.filePath),
|
|
2919
|
-
file,
|
|
2920
|
-
]));
|
|
2921
|
-
const files = (report.files ?? []).map((file) => replacementByPath.get(normalizeMapKey(file.filePath)) ?? file);
|
|
2922
|
-
for (const replacement of reverify.replacementFiles) {
|
|
2923
|
-
if (!files.some((file) => normalizeMapKey(file.filePath) ===
|
|
2924
|
-
normalizeMapKey(replacement.filePath))) {
|
|
2925
|
-
files.push(replacement);
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
report.files = files;
|
|
2929
|
-
}
|
|
2930
|
-
const publishResult = publishActionableWarningsReport(cacheManager, cwd, report, {
|
|
2931
|
-
origin: "in-band",
|
|
2932
|
-
getFileSeq: getFileSeq
|
|
2933
|
-
? (filePath) => getFileSeq.call(runtime, filePath)
|
|
2934
|
-
: undefined,
|
|
2935
|
-
dbg,
|
|
2936
|
-
});
|
|
2937
|
-
// #2504 review round 6 (a): the advisory must read the MERGED report,
|
|
2938
|
-
// not the pre-merge `report` this turn assembled -- a rescued deferred
|
|
2939
|
-
// entry lives only on `publishResult.report`, and formatting the
|
|
2940
|
-
// pre-merge report silently dropped it from the turn_end advisory
|
|
2941
|
-
// even though it was correctly persisted to cache.
|
|
2942
|
-
// #2521: `cwd` is what resolves the store location the advisory
|
|
2943
|
-
// names -- it is the SAME cwd `publishActionableWarningsReport`
|
|
2944
|
-
// just wrote through, so the two cannot disagree.
|
|
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
|
-
});
|
|
2975
|
-
// @delivery-surface: runtime-turn:actionable-warnings-advisory
|
|
2976
|
-
if (advisory)
|
|
2977
|
-
advisoryParts.push(advisory);
|
|
2978
|
-
logActionableWarningsEvent({
|
|
2979
|
-
event: advisory ? "advisory_injected" : "advisory_skipped",
|
|
2980
|
-
sessionId: runtime.telemetrySessionId,
|
|
2981
|
-
metadata: {
|
|
2982
|
-
turnIndex: runtime.turnIndex,
|
|
2983
|
-
// #2504 review round 7 (F2): the MERGED report, matching the
|
|
2984
|
-
// advisory text above it (round 6, a) -- a rescued deferred
|
|
2985
|
-
// entry lives only on `publishResult.report`, and the pre-merge
|
|
2986
|
-
// `report` this turn assembled undercounts it. This is the value
|
|
2987
|
-
// scripts/analyze-pi-lens-logs.mjs sums, so an undercount here is
|
|
2988
|
-
// a silent miscount there too.
|
|
2989
|
-
unsuppressed: publishResult.report.summary.unsuppressed,
|
|
2990
|
-
},
|
|
2991
|
-
});
|
|
2992
|
-
logLatency({
|
|
2993
|
-
type: "phase",
|
|
2994
|
-
toolName: "turn_end",
|
|
2995
|
-
filePath: cwd,
|
|
2996
|
-
phase: "actionable_warnings_report",
|
|
2997
|
-
durationMs: Date.now() - t4,
|
|
2998
|
-
metadata: report.summary,
|
|
2999
|
-
});
|
|
3000
|
-
}
|
|
3001
|
-
catch (err) {
|
|
3002
|
-
dbg(`turn_end: actionable warning report failed: ${err}`);
|
|
3003
|
-
logLatency({
|
|
3004
|
-
type: "phase",
|
|
3005
|
-
toolName: "turn_end",
|
|
3006
|
-
filePath: cwd,
|
|
3007
|
-
phase: "actionable_warnings_report",
|
|
3008
|
-
durationMs: Date.now() - t4,
|
|
3009
|
-
metadata: {
|
|
3010
|
-
failed: true,
|
|
3011
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3012
|
-
},
|
|
3013
|
-
});
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
const t5 = Date.now();
|
|
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
|
-
}
|
|
3053
|
-
const qualityReport = buildCodeQualityWarningsReport({
|
|
3054
|
-
cwd,
|
|
3055
|
-
sessionId: runtime.telemetrySessionId,
|
|
3056
|
-
turnIndex: runtime.turnIndex,
|
|
3057
|
-
warnings: qualityWarnings,
|
|
3058
|
-
modifiedRangesByFile,
|
|
3059
|
-
projectSeqStart: runtime.turnStartProjectSeq,
|
|
3060
|
-
projectSeqEnd: runtime.projectSeq,
|
|
3061
|
-
fileSeqByPath,
|
|
3062
|
-
});
|
|
3063
|
-
writeCodeQualityWarningsReport(cacheManager, cwd, qualityReport);
|
|
3064
|
-
appendCodeQualityWarningsHistory(cwd, qualityReport);
|
|
3065
|
-
const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd, host);
|
|
3066
|
-
// @delivery-surface: runtime-turn:code-quality-warnings-advisory
|
|
3067
|
-
if (advisory)
|
|
3068
|
-
advisoryParts.push(advisory);
|
|
3069
|
-
logLatency({
|
|
3070
|
-
type: "phase",
|
|
3071
|
-
toolName: "turn_end",
|
|
3072
|
-
filePath: cwd,
|
|
3073
|
-
phase: "code_quality_warnings_report",
|
|
3074
|
-
durationMs: Date.now() - t5,
|
|
3075
|
-
metadata: {
|
|
3076
|
-
...qualityReport.summary,
|
|
3077
|
-
// #3248: bounded per-turn on this lane's own row.
|
|
3078
|
-
dispositionSuppressed: qualityDispositionSuppressed,
|
|
3079
|
-
},
|
|
3080
|
-
});
|
|
3081
|
-
}
|
|
3082
|
-
catch (err) {
|
|
3083
|
-
dbg(`turn_end: code quality warning report failed: ${err}`);
|
|
3084
|
-
logLatency({
|
|
3085
|
-
type: "phase",
|
|
3086
|
-
toolName: "turn_end",
|
|
3087
|
-
filePath: cwd,
|
|
3088
|
-
phase: "code_quality_warnings_report",
|
|
3089
|
-
durationMs: Date.now() - t5,
|
|
3090
|
-
metadata: {
|
|
3091
|
-
failed: true,
|
|
3092
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3093
|
-
},
|
|
3094
|
-
});
|
|
3095
|
-
}
|
|
3096
|
-
cacheManager.incrementTurnCycle(cwd, currentOwner);
|
|
3097
|
-
// Collect-later CLI runners continue off the write path. Their completed
|
|
3098
|
-
// diagnostics use the same freshness gate as late auxiliary findings and
|
|
3099
|
-
// enter the ordinary turn-end advisory delivery channel.
|
|
3100
|
-
const runnerFindingsStart = Date.now();
|
|
3101
|
-
// Turn-end delivery is deliberately non-blocking. Collect already-settled
|
|
3102
|
-
// results and requeue the rest; the edit path already paid the deferral
|
|
3103
|
-
// decision, so another 2s wait would charge every turn while a runner is
|
|
3104
|
-
// still in flight (#2122 F5).
|
|
3105
|
-
const pendingRunnerFindings = await drainPendingRunnerFindings(0);
|
|
3106
|
-
let runnerFindingsDelivered = 0;
|
|
3107
|
-
let runnerFindingsStale = 0;
|
|
3108
|
-
let runnerFindingsFailed = 0;
|
|
3109
|
-
let runnerFindingsDropped = 0;
|
|
3110
|
-
/** #3248: bounded per-turn on this lane's own row, never per finding. */
|
|
3111
|
-
let runnerFindingsDispositionSuppressed = 0;
|
|
3112
|
-
const runnerFindingsDeliveredIds = [];
|
|
3113
|
-
for (const pending of pendingRunnerFindings) {
|
|
3114
|
-
const result = pending.result;
|
|
3115
|
-
if (!result)
|
|
3116
|
-
continue;
|
|
3117
|
-
recordRunner(pending.filePath, pending.runnerId, result.status, result.diagnostics.length, Date.now() - pending.markedAtMs, pending.writeIndex);
|
|
3118
|
-
if (result.status === "failed") {
|
|
3119
|
-
runnerFindingsFailed += 1;
|
|
3120
|
-
const detail = result.failureMessage ? `: ${result.failureMessage}` : "";
|
|
3121
|
-
// @delivery-surface: runtime-turn:late-runner-findings
|
|
3122
|
-
advisoryParts.push(`❌ Deferred runner ${pending.runnerId} failed (${result.failureKind ?? "unknown"})${detail}`);
|
|
3123
|
-
continue;
|
|
3124
|
-
}
|
|
3125
|
-
const findings = result.diagnostics;
|
|
3126
|
-
if (findings.length === 0)
|
|
3127
|
-
continue;
|
|
3128
|
-
const { "late-runner-findings": gate } = gateFindingsByPathFreshness({
|
|
3129
|
-
cwd,
|
|
3130
|
-
sources: {
|
|
3131
|
-
"late-runner-findings": {
|
|
3132
|
-
findings,
|
|
3133
|
-
scannedAt: pending.markedAtMs,
|
|
3134
|
-
citedPath: (finding) => finding.filePath,
|
|
3135
|
-
},
|
|
3136
|
-
},
|
|
3137
|
-
});
|
|
3138
|
-
runnerFindingsStale += gate.stale.length;
|
|
3139
|
-
if (gate.stale.length > 0) {
|
|
3140
|
-
// The runner answered for bytes older than the latest edit. Do not
|
|
3141
|
-
// re-arm this completed answer: only a new runner query can restore
|
|
3142
|
-
// coverage for the refreshed bytes.
|
|
3143
|
-
dropStaleRunnerFindings(pending);
|
|
3144
|
-
runnerFindingsDropped += 1;
|
|
3145
|
-
}
|
|
3146
|
-
if (gate.live.length === 0)
|
|
3147
|
-
continue;
|
|
3148
|
-
const displayPath = toRunnerDisplayPath(cwd, pending.filePath);
|
|
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) {
|
|
3178
|
-
if (runnerFindingsDeliveredIds.length < 50) {
|
|
3179
|
-
runnerFindingsDeliveredIds.push(finding.id);
|
|
3180
|
-
}
|
|
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
|
-
: "";
|
|
3187
|
-
// @delivery-surface: runtime-turn:late-runner-findings
|
|
3188
|
-
advisoryParts.push(`⏱️ Late runner diagnostics (${pending.runnerId} completed after the edit${runnerSuppressedNote}):\n${lines.join("\n")}`);
|
|
3189
|
-
}
|
|
3190
|
-
logLatency({
|
|
3191
|
-
type: "phase",
|
|
3192
|
-
toolName: "turn_end",
|
|
3193
|
-
filePath: cwd,
|
|
3194
|
-
phase: "late_runner_findings",
|
|
3195
|
-
durationMs: Date.now() - runnerFindingsStart,
|
|
3196
|
-
metadata: {
|
|
3197
|
-
pending: pendingRunnerFindings.length,
|
|
3198
|
-
delivered: runnerFindingsDelivered,
|
|
3199
|
-
stale: runnerFindingsStale,
|
|
3200
|
-
failed: runnerFindingsFailed,
|
|
3201
|
-
dropped: runnerFindingsDropped,
|
|
3202
|
-
dispositionSuppressed: runnerFindingsDispositionSuppressed,
|
|
3203
|
-
deliveredIds: runnerFindingsDeliveredIds,
|
|
3204
|
-
},
|
|
3205
|
-
});
|
|
3206
|
-
// #2001/#2002: collect-later delivery for auxiliary LSP servers whose
|
|
3207
|
-
// aux-grace window expired without a publication (opengrep on Windows:
|
|
3208
|
-
// ~8s per scan against a 2s grace — the scanner's eventual findings sat
|
|
3209
|
-
// in its client cache, agent-invisible). Probe each pending pair through
|
|
3210
|
-
// the read-only cache seam (never spawns), freshness-gate the result
|
|
3211
|
-
// against when the pair was marked, and deliver survivors as an advisory.
|
|
3212
|
-
// A pair whose client is alive but has STILL published nothing re-arms
|
|
3213
|
-
// (baseline preserved, TTL anchor advanced) until the re-arm TTL; a dead
|
|
3214
|
-
// client drops silently.
|
|
3215
|
-
const lateAuxStart = Date.now();
|
|
3216
|
-
const drainedPairs = drainPendingAuxiliaryCoverage();
|
|
3217
|
-
// #2168: cap evictions retire a pair before any drain can observe it — read
|
|
3218
|
-
// and reset that count here so it folds into this turn's reconciliation
|
|
3219
|
-
// sum instead of the pair vanishing uncounted.
|
|
3220
|
-
const lateAuxCapEvicted = drainPendingAuxCapEvictedCount();
|
|
3221
|
-
let lateAuxDelivered = 0;
|
|
3222
|
-
let lateAuxStale = 0;
|
|
3223
|
-
let lateAuxMissing = 0;
|
|
3224
|
-
let lateAuxRearmed = 0;
|
|
3225
|
-
let lateAuxClientGone = 0;
|
|
3226
|
-
let lateAuxProbeFailed = 0;
|
|
3227
|
-
let lateAuxCleanConfirmed = 0;
|
|
3228
|
-
let lateAuxExpired = 0;
|
|
3229
|
-
let lateAuxCeilingExhausted = 0;
|
|
3230
|
-
let lateAuxAnswered = 0;
|
|
3231
|
-
let lateAuxNotifyStallDemoted = 0;
|
|
3232
|
-
// #3102: dropped by the shared finding-policy stack (inline `pi-lens-ignore`
|
|
3233
|
-
// / stored disposition / `.pi-lens.json` rule policy) and, separately, by the
|
|
3234
|
-
// auxiliary profile's OWN native suppression inside `retagAuxiliaryDiagnostics`.
|
|
3235
|
-
// Both are reported in the one bounded per-turn record below — a drop is
|
|
3236
|
-
// never silent (shape 10).
|
|
3237
|
-
let lateAuxDispositionSuppressed = 0;
|
|
3238
|
-
let lateAuxAuxSuppressed = 0;
|
|
3239
|
-
const lateAuxCoverageGapPairs = [];
|
|
3240
|
-
let lateAuxCoverageGapDetailCount = 0;
|
|
3241
|
-
let lateAuxCoverageGapDropCount = 0;
|
|
3242
|
-
const lateAuxStuckPairs = [];
|
|
3243
|
-
if (drainedPairs.length > 0) {
|
|
3244
|
-
const lateObserverDeadline = Date.now() + HOOK_WALL_BUDGET_MS.turn_end;
|
|
3245
|
-
const byFile = new Map();
|
|
3246
|
-
for (const pair of drainedPairs) {
|
|
3247
|
-
const list = byFile.get(pair.filePath);
|
|
3248
|
-
if (list)
|
|
3249
|
-
list.push(pair);
|
|
3250
|
-
else
|
|
3251
|
-
byFile.set(pair.filePath, [pair]);
|
|
3252
|
-
}
|
|
3253
|
-
try {
|
|
3254
|
-
const service = getLSPService();
|
|
3255
|
-
for (const [lateAuxPath, pairs] of byFile) {
|
|
3256
|
-
let cached;
|
|
3257
|
-
try {
|
|
3258
|
-
cached = await service.readCachedDiagnosticsForServers(lateAuxPath, new Set(pairs.map((p) => p.serverId)));
|
|
3259
|
-
}
|
|
3260
|
-
catch {
|
|
3261
|
-
// #2027 round-1 P3-2 / #2167 R2-2: a transient probe rejection
|
|
3262
|
-
// tells us nothing about the pair's content, so treat it like the
|
|
3263
|
-
// "still scanning" branch below — re-arm under the SAME
|
|
3264
|
-
// ceiling/TTL bound rather than dropping the coverage outright.
|
|
3265
|
-
// `probeFailed` stays an honest per-turn failure count; it is
|
|
3266
|
-
// informational (like `stale`/`missing`), not a terminal bucket,
|
|
3267
|
-
// since the pair itself still resolves through rearmed/
|
|
3268
|
-
// ceilingExhausted/expired below.
|
|
3269
|
-
lateAuxProbeFailed += pairs.length;
|
|
3270
|
-
for (const pair of pairs) {
|
|
3271
|
-
if (!isPendingAuxiliaryPastRearmTtl(pair) &&
|
|
3272
|
-
(pair.rearmCount ?? 0) < MAX_LATE_AUX_REARMS) {
|
|
3273
|
-
rearmPendingAuxiliaryCoverage(pair);
|
|
3274
|
-
lateAuxRearmed += 1;
|
|
3275
|
-
}
|
|
3276
|
-
else if (isPendingAuxiliaryPastRearmTtl(pair)) {
|
|
3277
|
-
lateAuxExpired += 1;
|
|
3278
|
-
}
|
|
3279
|
-
else {
|
|
3280
|
-
lateAuxCeilingExhausted += 1;
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
3283
|
-
continue;
|
|
3284
|
-
}
|
|
3285
|
-
const displayLateAuxPath = toRunnerDisplayPath(cwd, lateAuxPath);
|
|
3286
|
-
// #3102: the file's CURRENT bytes, for the two content-bound halves
|
|
3287
|
-
// of the policy stack (inline `pi-lens-ignore` and the STRICT
|
|
3288
|
-
// `false-positive` anchor) and for the auxiliary profile's own
|
|
3289
|
-
// native suppression. Read at most ONCE per file per drain, lazily:
|
|
3290
|
-
// a turn that drains nothing, finds no live client, re-arms, or
|
|
3291
|
-
// confirms clean pays no I/O at all, and a file with several pending
|
|
3292
|
-
// servers pays one read for all of them. Measured cost on the one
|
|
3293
|
-
// file that does publish: 0.10-0.20 ms typical, 4.0 ms worst case
|
|
3294
|
-
// (a 180 KB file, 5 findings, a populated disposition store) against
|
|
3295
|
-
// the 3000 ms `HOOK_WALL_BUDGET_MS.turn_end`. A read failure yields
|
|
3296
|
-
// `undefined`: the content-free half still applies and nothing is
|
|
3297
|
-
// hidden on an I/O error (shape 48).
|
|
3298
|
-
let lateAuxContentRead = false;
|
|
3299
|
-
let lateAuxContentValue;
|
|
3300
|
-
const readLateAuxContent = () => {
|
|
3301
|
-
if (!lateAuxContentRead) {
|
|
3302
|
-
lateAuxContentRead = true;
|
|
3303
|
-
try {
|
|
3304
|
-
lateAuxContentValue = fs.readFileSync(lateAuxPath, "utf-8");
|
|
3305
|
-
}
|
|
3306
|
-
catch {
|
|
3307
|
-
lateAuxContentValue = undefined;
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
return lateAuxContentValue;
|
|
3311
|
-
};
|
|
3312
|
-
for (const pair of pairs) {
|
|
3313
|
-
const cachedEntry = cached.get(pair.serverId);
|
|
3314
|
-
if (cachedEntry === undefined) {
|
|
3315
|
-
// No live client for this server any more — best-effort probe,
|
|
3316
|
-
// drop the pair silently.
|
|
3317
|
-
lateAuxClientGone += 1;
|
|
3318
|
-
continue;
|
|
3319
|
-
}
|
|
3320
|
-
if (cachedEntry.notifyStallDemoted) {
|
|
3321
|
-
if (cachedEntry.demotedAt === undefined ||
|
|
3322
|
-
pair.markedAtMs > cachedEntry.demotedAt) {
|
|
3323
|
-
// A pair marked after teardown belongs to the missing
|
|
3324
|
-
// generation, so it follows ordinary clientGone handling.
|
|
3325
|
-
lateAuxClientGone += 1;
|
|
3326
|
-
continue;
|
|
3327
|
-
}
|
|
3328
|
-
// #2356: notify-stall teardown is a transient absence while the
|
|
3329
|
-
// breaker cools down, but only for a pair marked before teardown.
|
|
3330
|
-
lateAuxNotifyStallDemoted += 1;
|
|
3331
|
-
const pastTtl = isPendingAuxiliaryPastRearmTtl(pair);
|
|
3332
|
-
const atCeiling = (pair.rearmCount ?? 0) >= MAX_LATE_AUX_REARMS;
|
|
3333
|
-
if (!pastTtl && !atCeiling) {
|
|
3334
|
-
rearmPendingAuxiliaryCoverage(pair);
|
|
3335
|
-
lateAuxRearmed += 1;
|
|
3336
|
-
if (lateAuxStuckPairs.length < 20)
|
|
3337
|
-
lateAuxStuckPairs.push({
|
|
3338
|
-
filePath: pair.filePath,
|
|
3339
|
-
serverId: pair.serverId,
|
|
3340
|
-
});
|
|
3341
|
-
}
|
|
3342
|
-
else {
|
|
3343
|
-
if (pastTtl)
|
|
3344
|
-
lateAuxExpired += 1;
|
|
3345
|
-
else
|
|
3346
|
-
lateAuxCeilingExhausted += 1;
|
|
3347
|
-
lateAuxCoverageGapPairs.push({
|
|
3348
|
-
filePath: pair.filePath,
|
|
3349
|
-
serverId: pair.serverId,
|
|
3350
|
-
});
|
|
3351
|
-
}
|
|
3352
|
-
continue;
|
|
3353
|
-
}
|
|
3354
|
-
const rawDiags = cachedEntry.diags;
|
|
3355
|
-
if (cachedEntry.publishedAt === undefined ||
|
|
3356
|
-
cachedEntry.publishedAt <= pair.markedAtMs) {
|
|
3357
|
-
// Still scanning (or published nothing yet) — keep waiting
|
|
3358
|
-
// so a scan finishing before the NEXT turn end still
|
|
3359
|
-
// delivers. Two clocks, deliberately decoupled: the
|
|
3360
|
-
// freshness baseline (`markedAtMs`) NEVER moves — it is what
|
|
3361
|
-
// the delivery gate stats against — while the re-arm TTL is
|
|
3362
|
-
// anchored on `lastRearmedAtMs`, advanced by every successful
|
|
3363
|
-
// empty probe: the scanner is demonstrably alive, just slow.
|
|
3364
|
-
if (!isPendingAuxiliaryPastRearmTtl(pair) &&
|
|
3365
|
-
(pair.rearmCount ?? 0) < MAX_LATE_AUX_REARMS) {
|
|
3366
|
-
rearmPendingAuxiliaryCoverage(pair);
|
|
3367
|
-
lateAuxRearmed += 1;
|
|
3368
|
-
if (lateAuxStuckPairs.length < 20)
|
|
3369
|
-
lateAuxStuckPairs.push({
|
|
3370
|
-
filePath: pair.filePath,
|
|
3371
|
-
serverId: pair.serverId,
|
|
3372
|
-
});
|
|
3373
|
-
}
|
|
3374
|
-
else {
|
|
3375
|
-
if (isPendingAuxiliaryPastRearmTtl(pair))
|
|
3376
|
-
lateAuxExpired += 1;
|
|
3377
|
-
else
|
|
3378
|
-
lateAuxCeilingExhausted += 1;
|
|
3379
|
-
}
|
|
3380
|
-
continue;
|
|
3381
|
-
}
|
|
3382
|
-
// A demoted auxiliary still answers through this late path. Feed the
|
|
3383
|
-
// publication-minus-mark interval into the re-promotion streak. This is
|
|
3384
|
-
// delivery latency observed by the drain, not the scanner's total scan
|
|
3385
|
-
// latency. Cache priming is
|
|
3386
|
-
// below the freshness gate so a changed file cannot resurrect stale data.
|
|
3387
|
-
if (typeof service.observeLateAuxiliaryAnswer === "function") {
|
|
3388
|
-
await bounded(service.observeLateAuxiliaryAnswer(lateAuxPath, pair.serverId, cachedEntry.publishedAt - pair.markedAtMs), {
|
|
3389
|
-
ms: Math.max(1, lateObserverDeadline - Date.now()),
|
|
3390
|
-
signal: deps.signal /* late observer */,
|
|
3391
|
-
hook: "turn_end",
|
|
3392
|
-
label: "observeLateAuxiliaryAnswer",
|
|
3393
|
-
});
|
|
3394
|
-
}
|
|
3395
|
-
if (rawDiags.length === 0) {
|
|
3396
|
-
lateAuxCleanConfirmed += 1;
|
|
3397
|
-
continue;
|
|
3398
|
-
}
|
|
3399
|
-
// `convertLspDiagnostics` drops entries with no start line, which
|
|
3400
|
-
// would break the 1:1 index alignment `retagAuxiliaryDiagnostics`
|
|
3401
|
-
// needs. Partition first so `converted[i]` IS `anchored[i]` —
|
|
3402
|
-
// the same pre-partition `applyLspFindingPolicy` does.
|
|
3403
|
-
const anchored = rawDiags.filter((d) => d.range?.start?.line !== undefined);
|
|
3404
|
-
if (anchored.length === 0) {
|
|
3405
|
-
lateAuxMissing += rawDiags.length;
|
|
3406
|
-
lateAuxAnswered += 1;
|
|
3407
|
-
continue;
|
|
3408
|
-
}
|
|
3409
|
-
const lateAuxContent = readLateAuxContent();
|
|
3410
|
-
const converted = convertLspDiagnostics(anchored, lateAuxPath);
|
|
3411
|
-
// #3046/#3047: the auxiliary's REAL tool id (and its own native
|
|
3412
|
-
// inline suppression — opengrep's `# nosemgrep`, ast-grep's
|
|
3413
|
-
// test-file gate), from the ONE shared derivation every other
|
|
3414
|
-
// surface anchors a mark against. These diagnostics come straight
|
|
3415
|
-
// off the aux client's cache, so nothing upstream applied it.
|
|
3416
|
-
const retained = retagAuxiliaryDiagnostics(converted, anchored, lateAuxContent ?? "", { cwd, fileRole: detectFileRole(lateAuxPath, lateAuxContent) });
|
|
3417
|
-
lateAuxAuxSuppressed += converted.length - retained.length;
|
|
3418
|
-
if (retained.length === 0) {
|
|
3419
|
-
lateAuxAnswered += 1;
|
|
3420
|
-
continue;
|
|
3421
|
-
}
|
|
3422
|
-
// Freshness kernel (#1634 gated surface): stat the cited file
|
|
3423
|
-
// against the mark timestamp. Missing → drop (no remediation for
|
|
3424
|
-
// a deleted file); mtime drifted past the mark → drop too, NOT
|
|
3425
|
-
// demote — unlike the cached-blocker gates these findings were
|
|
3426
|
-
// NEVER delivered before, and the edit that drifted the file
|
|
3427
|
-
// already re-touched it (a fresh pending pair supersedes this
|
|
3428
|
-
// one), so a stale-arm replay would double-report old content.
|
|
3429
|
-
// Both drops are COUNTED here and in the latency record below —
|
|
3430
|
-
// never silent (shape 10).
|
|
3431
|
-
const { "late-auxiliary-findings": gate } = gateFindingsByPathFreshness({
|
|
3432
|
-
cwd,
|
|
3433
|
-
sources: {
|
|
3434
|
-
"late-auxiliary-findings": {
|
|
3435
|
-
findings: retained,
|
|
3436
|
-
scannedAt: pair.markedAtMs,
|
|
3437
|
-
citedPath: () => lateAuxPath,
|
|
3438
|
-
},
|
|
3439
|
-
},
|
|
3440
|
-
});
|
|
3441
|
-
lateAuxStale += gate.stale.length;
|
|
3442
|
-
lateAuxMissing +=
|
|
3443
|
-
retained.length - gate.live.length - gate.stale.length;
|
|
3444
|
-
if (gate.live.length === 0) {
|
|
3445
|
-
if (gate.stale.length > 0) {
|
|
3446
|
-
// Stale findings mean the scan predates the last edit. Re-arm
|
|
3447
|
-
// with a refreshed baseline and carry the ceiling count.
|
|
3448
|
-
if (!isPendingAuxiliaryPastRearmTtl(pair) &&
|
|
3449
|
-
(pair.rearmCount ?? 0) < MAX_LATE_AUX_REARMS) {
|
|
3450
|
-
rearmPendingAuxiliaryCoverage(pair, Date.now(), true);
|
|
3451
|
-
lateAuxRearmed += 1;
|
|
3452
|
-
}
|
|
3453
|
-
else if (isPendingAuxiliaryPastRearmTtl(pair)) {
|
|
3454
|
-
lateAuxExpired += 1;
|
|
3455
|
-
}
|
|
3456
|
-
else {
|
|
3457
|
-
lateAuxCeilingExhausted += 1;
|
|
3458
|
-
}
|
|
3459
|
-
}
|
|
3460
|
-
else {
|
|
3461
|
-
lateAuxAnswered += 1;
|
|
3462
|
-
}
|
|
3463
|
-
continue;
|
|
3464
|
-
}
|
|
3465
|
-
// #2810 round 4: this drain does NOT write the hash-bound
|
|
3466
|
-
// last-known record. The prime it used to call could only fire when
|
|
3467
|
-
// a record already existed at the pair's hash — which requires a
|
|
3468
|
-
// FULLY covered touch of those exact bytes, the one case where the
|
|
3469
|
-
// scanner's findings are already in the record — so it was a no-op
|
|
3470
|
-
// in the demoted steady state it was added for, and a #570/#1470
|
|
3471
|
-
// hazard everywhere else (an auxiliary-only array replacing the
|
|
3472
|
-
// merged one). Late findings reach the agent as the gated advisory
|
|
3473
|
-
// below; the turn-end hash-guarded fast path stays cold for a file
|
|
3474
|
-
// whose touch was partial, which is exactly what #1470 requires.
|
|
3475
|
-
// #3102: the survivors are what the agent READS, so they take the
|
|
3476
|
-
// same `clients/dispatch/finding-policy.ts` stack — inline
|
|
3477
|
-
// `pi-lens-ignore` → stored dispositions → `.pi-lens.json` rule
|
|
3478
|
-
// policy — the per-edit dispatcher, `mode=full` and the
|
|
3479
|
-
// `source=lsp` probe lane apply. Without it a finding the agent
|
|
3480
|
-
// marked `false-positive` re-reported on every turn that drained
|
|
3481
|
-
// a late pair. Runs AFTER the freshness gate, like the #1625
|
|
3482
|
-
// govulncheck/secrets filters: the anchor is derived from the
|
|
3483
|
-
// post-gate identity, never the raw pre-gate set.
|
|
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, {
|
|
3489
|
-
cwd,
|
|
3490
|
-
filePath: lateAuxPath,
|
|
3491
|
-
content: lateAuxContent,
|
|
3492
|
-
});
|
|
3493
|
-
lateAuxDispositionSuppressed += lateAuxSuppressedHere;
|
|
3494
|
-
if (lateAuxKept.length === 0) {
|
|
3495
|
-
// Every late finding was suppressed. This is a PUSH surface:
|
|
3496
|
-
// silence after a mark is the mark working, not a clean
|
|
3497
|
-
// verdict, so the count rides the bounded per-turn record below
|
|
3498
|
-
// instead of re-announcing the suppression every single turn.
|
|
3499
|
-
lateAuxAnswered += 1;
|
|
3500
|
-
continue;
|
|
3501
|
-
}
|
|
3502
|
-
const lines = lateAuxKept.map((f) => ` ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
|
|
3503
|
-
lateAuxDelivered += lateAuxKept.length;
|
|
3504
|
-
lateAuxAnswered += 1;
|
|
3505
|
-
// #1616 suppressed-bucket rule: a delivery that still has
|
|
3506
|
-
// something to say states what it dropped, once per delivery.
|
|
3507
|
-
const lateAuxSuppressedNote = lateAuxSuppressedHere > 0
|
|
3508
|
-
? `; suppressed by disposition: ${lateAuxSuppressedHere} finding(s)`
|
|
3509
|
-
: "";
|
|
3510
|
-
// @delivery-surface: runtime-turn:late-auxiliary-findings
|
|
3511
|
-
advisoryParts.push(`🕐 Late auxiliary diagnostics (${pair.serverId} answered after its grace window${lateAuxSuppressedNote}):\n${lines.join("\n")}`);
|
|
3512
|
-
}
|
|
3513
|
-
}
|
|
3514
|
-
}
|
|
3515
|
-
catch (err) {
|
|
3516
|
-
dbg(`turn_end: late-auxiliary probe failed: ${err}`);
|
|
3517
|
-
}
|
|
3518
|
-
// #2356: a demoted scanner that never gets replaced remains a coverage
|
|
3519
|
-
// gap. Re-raise it once when the existing bounded late-pair window closes,
|
|
3520
|
-
// preserving the server/file identity in both the ledger and latency row.
|
|
3521
|
-
for (const pair of lateAuxCoverageGapPairs) {
|
|
3522
|
-
const normalizedPairPath = normalizeMapKey(pair.filePath);
|
|
3523
|
-
const emitted = emitBounded("lsp_scanner_coverage_gap", `${pair.serverId}:${normalizedPairPath}`, {
|
|
3524
|
-
filePath: normalizedPairPath,
|
|
3525
|
-
durationMs: 0,
|
|
3526
|
-
metadata: {
|
|
3527
|
-
source: "late-auxiliary",
|
|
3528
|
-
serverIds: [pair.serverId],
|
|
3529
|
-
reason: "notify-stall-replacement-unavailable",
|
|
3530
|
-
reRaised: true,
|
|
3531
|
-
},
|
|
3532
|
-
}, {
|
|
3533
|
-
ledgerKind: "lsp-scanner-coverage-gap",
|
|
3534
|
-
reason: "notify-stall replacement was not available before late-coverage ceiling",
|
|
3535
|
-
capPerTurn: {
|
|
3536
|
-
limit: LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN,
|
|
3537
|
-
turnIndex: runtime.turnIndex,
|
|
3538
|
-
},
|
|
3539
|
-
});
|
|
3540
|
-
if (emitted)
|
|
3541
|
-
lateAuxCoverageGapDetailCount += 1;
|
|
3542
|
-
else
|
|
3543
|
-
lateAuxCoverageGapDropCount += 1;
|
|
3544
|
-
}
|
|
3545
|
-
logLatency({
|
|
3546
|
-
type: "phase",
|
|
3547
|
-
toolName: "turn_end",
|
|
3548
|
-
filePath: cwd,
|
|
3549
|
-
phase: "late_auxiliary_findings",
|
|
3550
|
-
durationMs: Date.now() - lateAuxStart,
|
|
3551
|
-
metadata: {
|
|
3552
|
-
pending: drainedPairs.length,
|
|
3553
|
-
pairCreated: drainedPairs.length + lateAuxCapEvicted,
|
|
3554
|
-
pendingAfter: pendingAuxiliaryCoverageSize(),
|
|
3555
|
-
delivered: lateAuxDelivered,
|
|
3556
|
-
stale: lateAuxStale,
|
|
3557
|
-
missing: lateAuxMissing,
|
|
3558
|
-
rearmed: lateAuxRearmed,
|
|
3559
|
-
clientGone: lateAuxClientGone,
|
|
3560
|
-
probeFailed: lateAuxProbeFailed,
|
|
3561
|
-
cleanConfirmed: lateAuxCleanConfirmed,
|
|
3562
|
-
expired: lateAuxExpired,
|
|
3563
|
-
ceilingExhausted: lateAuxCeilingExhausted,
|
|
3564
|
-
answered: lateAuxAnswered,
|
|
3565
|
-
dispositionSuppressed: lateAuxDispositionSuppressed,
|
|
3566
|
-
auxSuppressed: lateAuxAuxSuppressed,
|
|
3567
|
-
notifyStallDemoted: lateAuxNotifyStallDemoted,
|
|
3568
|
-
coverageGapReRaised: lateAuxCoverageGapPairs.length,
|
|
3569
|
-
coverageGapReRaisedDetailed: lateAuxCoverageGapDetailCount,
|
|
3570
|
-
coverageGapReRaisedDropped: lateAuxCoverageGapDropCount,
|
|
3571
|
-
capEvicted: lateAuxCapEvicted,
|
|
3572
|
-
stuckPairs: lateAuxStuckPairs,
|
|
3573
|
-
},
|
|
3574
|
-
});
|
|
3575
|
-
}
|
|
3576
|
-
const labeledAdvisoryParts = advisoryParts.map((p) => `ℹ️ Advisory — no action required this turn:\n${p}`);
|
|
3577
|
-
// Stale-secret parts sit between the two tiers and are NOT relabelled — they
|
|
3578
|
-
// ship the imperative preamble they were built with (#1622 review M2).
|
|
3579
|
-
const findingParts = [
|
|
3580
|
-
...blockerParts,
|
|
3581
|
-
...staleSecretParts,
|
|
3582
|
-
...labeledAdvisoryParts,
|
|
3583
|
-
];
|
|
3584
|
-
if (findingParts.length > 0) {
|
|
3585
|
-
dbg(`turn_end: ${blockerParts.length} blocker section(s), ${advisoryParts.length} advisory section(s) found, persisting for next context`);
|
|
3586
|
-
const content = capTurnEndMessage(findingParts.join("\n\n"));
|
|
3587
|
-
const signature = `${files
|
|
3588
|
-
.slice()
|
|
3589
|
-
.sort((a, b) => compareOrdinal(a, b))
|
|
3590
|
-
.join("|")}::${content}`;
|
|
3591
|
-
const last = cacheManager.readCache("turn-end-findings-last", cwd);
|
|
3592
|
-
if (last?.data?.signature === signature &&
|
|
3593
|
-
last?.data?.sessionId === runtime.telemetrySessionId) {
|
|
3594
|
-
dbg("turn_end: duplicate findings detected (same session), suppressing re-prompt");
|
|
3595
|
-
if (getFlag("lens-guard")) {
|
|
3596
|
-
const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
3597
|
-
if (existingGuard) {
|
|
3598
|
-
writeGitGuardRecord(cacheManager, runtime, cwd, {
|
|
3599
|
-
...existingGuard,
|
|
3600
|
-
content,
|
|
3601
|
-
blockerContent: blockerParts.length > 0
|
|
3602
|
-
? capTurnEndMessage(blockerParts.join("\n\n"))
|
|
3603
|
-
: undefined,
|
|
3604
|
-
hasBlockers: blockerParts.length > 0 || existingGuard.testFailures === true,
|
|
3605
|
-
blockingFiles: blockerParts.length > 0 ? existingGuard.affectedFiles : undefined,
|
|
3606
|
-
projectSeqStart: runtime.turnStartProjectSeq,
|
|
3607
|
-
projectSeqEnd: runtime.projectSeq,
|
|
3608
|
-
fileSeqByPath: Object.fromEntries(runtime
|
|
3609
|
-
.getFileSeqEntries()
|
|
3610
|
-
.map(([filePath, seq]) => [
|
|
3611
|
-
normalizeMapKey(path.resolve(filePath)),
|
|
3612
|
-
seq,
|
|
3613
|
-
])),
|
|
3614
|
-
fileContentHashes: {},
|
|
3615
|
-
consumed: false,
|
|
3616
|
-
});
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
clearOwnedTurnState();
|
|
3620
|
-
runtime.fixedThisTurn.clear();
|
|
3621
|
-
resetFormatService();
|
|
3622
|
-
return;
|
|
3623
|
-
}
|
|
3624
|
-
// #1950 fix-round F1: this turn's content is confirmed NOT suppressed —
|
|
3625
|
-
// it is about to reach the agent — so NOW commit the delivery-count
|
|
3626
|
-
// increments the per-blocker loop above only tentatively computed.
|
|
3627
|
-
for (const commit of pendingDependencyDriftDeliveries)
|
|
3628
|
-
commit();
|
|
3629
|
-
const fileSeqByPath = {};
|
|
3630
|
-
for (const [filePath, seq] of runtime.getFileSeqEntries()) {
|
|
3631
|
-
fileSeqByPath[normalizeMapKey(path.resolve(filePath))] = seq;
|
|
3632
|
-
}
|
|
3633
|
-
if (getFlag("lens-guard")) {
|
|
3634
|
-
const existingGuard = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
3635
|
-
const blockingContent = blockerParts.length > 0
|
|
3636
|
-
? capTurnEndMessage(blockerParts.join("\n\n"))
|
|
3637
|
-
: undefined;
|
|
3638
|
-
const affectedFiles = [
|
|
3639
|
-
...(existingGuard?.affectedFiles ?? []),
|
|
3640
|
-
...files.map((file) => resolveRunnerPath(cwd, file)),
|
|
3641
|
-
...cascadeResults.flatMap((result) => result.neighbors
|
|
3642
|
-
.filter((neighbor) => neighbor.diagnostics.length > 0)
|
|
3643
|
-
.map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
|
|
3644
|
-
];
|
|
3645
|
-
writeGitGuardRecord(cacheManager, runtime, cwd, {
|
|
3646
|
-
content: [content, existingGuard?.testFailureContent]
|
|
3647
|
-
.filter((value) => !!value)
|
|
3648
|
-
.join("\n\n"),
|
|
3649
|
-
blockerContent: blockingContent,
|
|
3650
|
-
blockingFiles: blockerParts.length > 0 ? affectedFiles : undefined,
|
|
3651
|
-
hasBlockers: !!blockingContent || existingGuard?.testFailures === true,
|
|
3652
|
-
affectedFiles,
|
|
3653
|
-
sessionId: runtime.telemetrySessionId,
|
|
3654
|
-
projectSeqStart: runtime.turnStartProjectSeq,
|
|
3655
|
-
projectSeqEnd: runtime.projectSeq,
|
|
3656
|
-
fileSeqByPath,
|
|
3657
|
-
fileContentHashes: {},
|
|
3658
|
-
consumed: false,
|
|
3659
|
-
testFailures: existingGuard?.testFailures,
|
|
3660
|
-
testFailureContent: existingGuard?.testFailureContent,
|
|
3661
|
-
testFailureFiles: existingGuard?.testFailureFiles,
|
|
3662
|
-
});
|
|
3663
|
-
}
|
|
3664
|
-
else {
|
|
3665
|
-
const allAffectedFiles = [
|
|
3666
|
-
...files.map((file) => resolveRunnerPath(cwd, file)),
|
|
3667
|
-
...cascadeResults.flatMap((result) => result.neighbors
|
|
3668
|
-
.filter((neighbor) => neighbor.diagnostics.length > 0)
|
|
3669
|
-
.map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath))),
|
|
3670
|
-
];
|
|
3671
|
-
const affectedFiles = [...new Set(allAffectedFiles)].slice(0, MAX_ADVISORY_AFFECTED_FILES);
|
|
3672
|
-
const affectedFilesTruncated = new Set(allAffectedFiles).size > affectedFiles.length;
|
|
3673
|
-
cacheManager.writeCache("turn-end-findings", {
|
|
3674
|
-
content,
|
|
3675
|
-
affectedFiles,
|
|
3676
|
-
affectedFilesTruncated,
|
|
3677
|
-
provenance: snapshotAdvisoryProvenance({
|
|
3678
|
-
cwd,
|
|
3679
|
-
runtime,
|
|
3680
|
-
generation: 0,
|
|
3681
|
-
files: affectedFiles.map((file) => ({
|
|
3682
|
-
path: file,
|
|
3683
|
-
role: "affected",
|
|
3684
|
-
})),
|
|
3685
|
-
truncated: affectedFilesTruncated,
|
|
3686
|
-
}),
|
|
3687
|
-
}, cwd);
|
|
3688
|
-
}
|
|
3689
|
-
cacheManager.writeCache("turn-end-findings-last", {
|
|
3690
|
-
signature,
|
|
3691
|
-
sessionId: runtime.telemetrySessionId,
|
|
3692
|
-
projectSeqStart: runtime.turnStartProjectSeq,
|
|
3693
|
-
projectSeqEnd: runtime.projectSeq,
|
|
3694
|
-
}, cwd);
|
|
3695
|
-
emitLensTurnFindings({
|
|
3696
|
-
cwd,
|
|
3697
|
-
filePaths: files.map((file) => resolveRunnerPath(cwd, file)),
|
|
3698
|
-
sessionId: runtime.telemetrySessionId,
|
|
3699
|
-
turnIndex: runtime.turnIndex,
|
|
3700
|
-
blockerSections: blockerParts.length,
|
|
3701
|
-
advisorySections: advisoryParts.length,
|
|
3702
|
-
content,
|
|
3703
|
-
});
|
|
3704
|
-
}
|
|
3705
|
-
if (blockerParts.length === 0) {
|
|
3706
|
-
clearOwnedTurnState();
|
|
3707
|
-
// `staleSecretParts` counts here too (#1622 review M2): clearing the
|
|
3708
|
-
// findings record while a stale secret is still unverified would drop the
|
|
3709
|
-
// only surviving trace of it.
|
|
3710
|
-
if (getFlag("lens-guard") &&
|
|
3711
|
-
advisoryParts.length === 0 &&
|
|
3712
|
-
staleSecretParts.length === 0 &&
|
|
3713
|
-
!runtime.gitGuardHasBlockers) {
|
|
3714
|
-
const guardRecord = cacheManager.readCache("turn-end-findings", cwd)?.data;
|
|
3715
|
-
if (guardRecord?.sessionId === runtime.telemetrySessionId &&
|
|
3716
|
-
guardRecord.testFailures !== true) {
|
|
3717
|
-
cacheManager.clearCache("turn-end-findings", cwd);
|
|
3718
|
-
}
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
|
-
runtime.fixedThisTurn.clear();
|
|
3722
|
-
runtime.clearActionableWarnings();
|
|
3723
|
-
runtime.clearCodeQualityWarnings();
|
|
3724
|
-
if (demotedFindingsRetired > 0) {
|
|
3725
|
-
// #1944: the retired payload must not survive as a SUPPRESSION key.
|
|
3726
|
-
// `turn-end-findings-last` holds a content signature used to silence a
|
|
3727
|
-
// duplicate re-prompt; live evidence found the demoted payload still on
|
|
3728
|
-
// disk 80+ minutes after the file shrank. It is not a delivery source —
|
|
3729
|
-
// `runtime-context.ts` never reads it, and the read at the top of this
|
|
3730
|
-
// function is gated on the current `sessionId`, so it cannot resurrect
|
|
3731
|
-
// the finding in a later session. It CAN, however, silence a genuinely
|
|
3732
|
-
// new report of content the store no longer holds. Drop it with the
|
|
3733
|
-
// record it describes.
|
|
3734
|
-
cacheManager.clearCache("turn-end-findings-last", cwd);
|
|
3735
|
-
}
|
|
3736
|
-
logLatency({
|
|
3737
|
-
type: "tool_result",
|
|
3738
|
-
toolName: "turn_end",
|
|
3739
|
-
filePath: cwd,
|
|
3740
|
-
durationMs: Date.now() - turnEndStart,
|
|
3741
|
-
// #1622 review M2: a pending stale secret is NOT a clean turn. It gets its
|
|
3742
|
-
// own result rather than being promoted to `blockers_found`, which would
|
|
3743
|
-
// undo the demotion the freshness gate just made.
|
|
3744
|
-
result: blockerParts.length > 0
|
|
3745
|
-
? "blockers_found"
|
|
3746
|
-
: staleSecretParts.length > 0
|
|
3747
|
-
? "stale_secrets_pending"
|
|
3748
|
-
: "clean",
|
|
3749
|
-
metadata: {
|
|
3750
|
-
fileCount: files.length,
|
|
3751
|
-
blockerSections: blockerParts.length,
|
|
3752
|
-
staleSecretSections: staleSecretParts.length,
|
|
3753
|
-
advisorySections: advisoryParts.length,
|
|
3754
|
-
// #1944 AC3: an empty advisory section on its own cannot say whether
|
|
3755
|
-
// the turn had nothing to report or dropped something. This counter
|
|
3756
|
-
// answers that from latency.log even when the payload is empty, and
|
|
3757
|
-
// the payload itself carries the retirement note when it is not.
|
|
3758
|
-
demotedFindingsRetired,
|
|
3759
|
-
// #2275: the widget-footer store's own dependency-drift retirements —
|
|
3760
|
-
// a separate surface/counter from `demotedFindingsRetired` above,
|
|
3761
|
-
// since a widget retirement never touches `advisoryParts` or the
|
|
3762
|
-
// `turn-end-findings-last` suppression cache the block below clears.
|
|
3763
|
-
widgetDemotedFindingsRetired,
|
|
3764
|
-
},
|
|
3765
|
-
});
|
|
3766
|
-
resetFormatService();
|
|
3767
|
-
}
|