@herbertgao/pi-extensions 2026.9.15 → 2026.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
- package/node_modules/pi-lens/CHANGELOG.md +498 -0
- package/node_modules/pi-lens/README.md +45 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
- package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
- package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
- package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
- package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
- package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
- package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
- package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
- package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
- package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
- package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
- package/node_modules/pi-lens/dist/index.js +5695 -3733
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
- package/node_modules/pi-lens/dist/mcp/server.js +33 -21
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
- package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
- package/node_modules/pi-lens/docs/adr/README.md +26 -0
- package/node_modules/pi-lens/docs/agent-tools.md +18 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
- package/node_modules/pi-lens/docs/configuration.md +55 -1
- package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
- package/node_modules/pi-lens/docs/environment-variables.md +61 -4
- package/node_modules/pi-lens/docs/features.md +2 -2
- package/node_modules/pi-lens/docs/globalconfig.md +6 -1
- package/node_modules/pi-lens/docs/language-coverage.md +17 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
- package/node_modules/pi-lens/docs/settings.md +12 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
- package/node_modules/pi-lens/package.json +5 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
- package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
- package/node_modules/pi-mcp-adapter/README.md +12 -5
- package/node_modules/pi-mcp-adapter/config.ts +31 -8
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +37 -20
- package/node_modules/pi-mcp-adapter/init.ts +14 -15
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +3 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
- package/node_modules/pi-mcp-adapter/types.ts +2 -2
- package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
- package/node_modules/pi-mcp-adapter/utils.ts +11 -3
- package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
- package/node_modules/pi-next-cue/LICENSE +21 -0
- package/node_modules/pi-next-cue/README.md +81 -0
- package/node_modules/pi-next-cue/index.ts +358 -0
- package/node_modules/pi-next-cue/package.json +28 -0
- package/package.json +9 -6
|
@@ -25,6 +25,7 @@ import { loadDispatchIntegration } from "./dispatch/lazy.js";
|
|
|
25
25
|
import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
26
26
|
import { classifyProbeFailure, createAvailabilityChecker, createAvailabilityLatch, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, resolveAvailableOrInstall, resolveCommandArgsWithInstallFallback, resolveToolCommand, resolveToolCommandWithInstallFallback, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
27
27
|
import { findDetektConfig } from "./dispatch/runners/detekt.js";
|
|
28
|
+
import { formatDiagnostics } from "./dispatch/utils/format-utils.js";
|
|
28
29
|
import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
|
|
29
30
|
import { detectFileChangedAfterCommand, getProjectIgnoreMatcher, isExcludedDirName, } from "./file-utils.js";
|
|
30
31
|
import { logLatency } from "./latency-logger.js";
|
|
@@ -40,12 +41,11 @@ import { probeToolAsync } from "./tool-probe.js";
|
|
|
40
41
|
import { bounded } from "./deadline-utils.js";
|
|
41
42
|
import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
|
|
42
43
|
import { enabledAuxiliaryLspServerIds } from "./dispatch/auxiliary-lsp.js";
|
|
43
|
-
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
44
|
+
import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
44
45
|
import { establishToolAgreement } from "./tool-agreement.js";
|
|
45
46
|
import { dropFindingsForMissingPaths } from "./advisory-provenance.js";
|
|
46
47
|
import { getAutofixPolicyForFile, getPreferredAutofixTools, getRubocopCommand, hasBiomeConfig, hasDetektConfig, hasEslintConfig, hasGolangciConfig, hasKtfmtConfig, hasKtlintConfig, hasOxlintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, markdownlintConfigArgs, } from "./tool-policy.js";
|
|
47
|
-
|
|
48
|
-
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
48
|
+
import { exceedsLspSyncLimits } from "./lsp/content-limits.js";
|
|
49
49
|
const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
|
|
50
50
|
const AUTOFIX_CHANGED_FILE_SCAN_LIMIT = 5000;
|
|
51
51
|
/**
|
|
@@ -60,10 +60,6 @@ function lspSyncBudgetMs() {
|
|
|
60
60
|
const raw = Number(process.env.PI_LENS_LSP_SYNC_BUDGET_MS);
|
|
61
61
|
return Number.isFinite(raw) && raw > 0 ? raw : 3000;
|
|
62
62
|
}
|
|
63
|
-
// Scan one directory's entries into `snapshot`, pushing walkable subdirs onto
|
|
64
|
-
// `stack`. Extracted from the walk loop to keep each function's cognitive
|
|
65
|
-
// complexity low. Excluded/ignored dirs are not descended; ignored/vanished
|
|
66
|
-
// files are skipped.
|
|
67
63
|
// Files stat'd between event-loop yields. The walk stays on the tool_result
|
|
68
64
|
// hot path; yielding every N keeps its longest synchronous stretch well under
|
|
69
65
|
// the <50ms hook-burst budget even at the AUTOFIX_CHANGED_FILE_SCAN_LIMIT cap.
|
|
@@ -144,23 +140,6 @@ async function diffProjectSnapshot(root, before) {
|
|
|
144
140
|
}
|
|
145
141
|
return [...changed].sort((a, b) => a.localeCompare(b));
|
|
146
142
|
}
|
|
147
|
-
function exceedsLspSyncLimits(_filePath, content) {
|
|
148
|
-
const sizeBytes = Buffer.byteLength(content, "utf-8");
|
|
149
|
-
if (sizeBytes > LSP_MAX_FILE_BYTES) {
|
|
150
|
-
return {
|
|
151
|
-
tooLarge: true,
|
|
152
|
-
reason: `${Math.round(sizeBytes / 1024)}KB exceeds ${Math.round(LSP_MAX_FILE_BYTES / 1024)}KB`,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
const lineCount = content.split("\n").length;
|
|
156
|
-
if (lineCount > LSP_MAX_FILE_LINES) {
|
|
157
|
-
return {
|
|
158
|
-
tooLarge: true,
|
|
159
|
-
reason: `${lineCount} lines exceeds ${LSP_MAX_FILE_LINES}`,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
return { tooLarge: false, reason: "" };
|
|
163
|
-
}
|
|
164
143
|
function createPhaseTracker(toolName, filePath) {
|
|
165
144
|
const phases = [];
|
|
166
145
|
return {
|
|
@@ -677,7 +656,13 @@ export async function resyncLspFile(filePath, fileContent, needsContentRefresh,
|
|
|
677
656
|
return;
|
|
678
657
|
if (!needsContentRefresh && lspSyncCompleted)
|
|
679
658
|
return;
|
|
680
|
-
|
|
659
|
+
// #3405 r2: the bound is `clients/lsp/content-limits.ts` now, shared with the
|
|
660
|
+
// dispatch runner and the didSave payload. THIS call stays: a file past the
|
|
661
|
+
// bound is not synced to the server AT ALL, which is a stricter policy than
|
|
662
|
+
// the save seam's (drop the redundant text, still send the save) and the one
|
|
663
|
+
// a post-write sync owes — removing it would start writing whole-file
|
|
664
|
+
// didOpen frames for files this pipeline has always refused.
|
|
665
|
+
const limitCheck = exceedsLspSyncLimits(fileContent);
|
|
681
666
|
if (limitCheck.tooLarge)
|
|
682
667
|
return;
|
|
683
668
|
try {
|
|
@@ -711,6 +696,11 @@ export async function resyncLspFile(filePath, fileContent, needsContentRefresh,
|
|
|
711
696
|
source: "lsp_sync",
|
|
712
697
|
clientScope: "primary",
|
|
713
698
|
maxClientWaitMs: LSP_SPAWN_BUDGET_MS,
|
|
699
|
+
// #3405: pi-lens just wrote these bytes to disk, so this is the one
|
|
700
|
+
// touch that is a save. A save-triggered server (Expert recompiles
|
|
701
|
+
// the project on didSave and on nothing else) publishes for the edit
|
|
702
|
+
// only because of this flag.
|
|
703
|
+
saved: true,
|
|
714
704
|
})
|
|
715
705
|
.then(() => "done")
|
|
716
706
|
.catch((err) => {
|
|
@@ -961,6 +951,14 @@ function buildEnrichedBlockerOutput(blockers, fileContent = "") {
|
|
|
961
951
|
export async function runPipeline(ctx, deps) {
|
|
962
952
|
const { filePath, cwd, toolName, getFlag, getFlagSource, dbg } = ctx;
|
|
963
953
|
const { getFormatService } = deps;
|
|
954
|
+
const allowAutonomousWriters = ctx.allowAutonomousWriters !== false;
|
|
955
|
+
if (!allowAutonomousWriters) {
|
|
956
|
+
incrementDegradationCount({
|
|
957
|
+
kind: "opaque-mutation-ownership-boundary",
|
|
958
|
+
subject: "pipeline",
|
|
959
|
+
reason: `observed mutation is not evidence of agent authorship (${filePath})`,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
964
962
|
admitWidgetDiagnosticsWrite(filePath, ctx.telemetry?.writeIndex);
|
|
965
963
|
const phase = createPhaseTracker(toolName, filePath);
|
|
966
964
|
const pipelineStart = Date.now();
|
|
@@ -984,8 +982,14 @@ export async function runPipeline(ctx, deps) {
|
|
|
984
982
|
const piChangedFiles = new Set();
|
|
985
983
|
const autoformatDisabled = !!getFlag("no-autoformat", filePath);
|
|
986
984
|
const immediateFormat = !!getFlag("immediate-format");
|
|
987
|
-
const formatDeferred =
|
|
988
|
-
|
|
985
|
+
const formatDeferred = allowAutonomousWriters &&
|
|
986
|
+
!autoformatDisabled &&
|
|
987
|
+
!immediateFormat &&
|
|
988
|
+
!!fileContent;
|
|
989
|
+
if (allowAutonomousWriters &&
|
|
990
|
+
!autoformatDisabled &&
|
|
991
|
+
immediateFormat &&
|
|
992
|
+
fileContent) {
|
|
989
993
|
const formatResult = await runFormatPhase(filePath, getFormatService, dbg, ctx.signal);
|
|
990
994
|
formatChanged = formatResult.formatChanged;
|
|
991
995
|
formattersUsed = formatResult.formattersUsed;
|
|
@@ -1010,6 +1014,9 @@ export async function runPipeline(ctx, deps) {
|
|
|
1010
1014
|
else if (formatDeferred) {
|
|
1011
1015
|
dbg(`autoformat: deferred until agent_end for ${filePath}`);
|
|
1012
1016
|
}
|
|
1017
|
+
else if (!allowAutonomousWriters) {
|
|
1018
|
+
dbg(`autoformat: skipped for ${filePath} (mutation ownership is unproven)`);
|
|
1019
|
+
}
|
|
1013
1020
|
else if (autoformatDisabled) {
|
|
1014
1021
|
const source = getFlagSource?.("no-autoformat", filePath);
|
|
1015
1022
|
dbg(`autoformat: skipped for ${filePath} (--no-autoformat${source ? `, source=${source}` : ""})`);
|
|
@@ -1027,7 +1034,11 @@ export async function runPipeline(ctx, deps) {
|
|
|
1027
1034
|
let autofixChangedFiles = [];
|
|
1028
1035
|
let fixRefresh = false;
|
|
1029
1036
|
let autofixSkipReason;
|
|
1030
|
-
if (
|
|
1037
|
+
if (!allowAutonomousWriters) {
|
|
1038
|
+
autofixSkipReason = "mutation_ownership_unproven";
|
|
1039
|
+
dbg(`autofix: skipped for ${filePath} (mutation ownership is unproven)`);
|
|
1040
|
+
}
|
|
1041
|
+
else if (ctx.autofixMode === "deferred") {
|
|
1031
1042
|
autofixSkipReason = "deferred_to_agent_end";
|
|
1032
1043
|
dbg(`autofix: deferred until agent_end for ${filePath}`);
|
|
1033
1044
|
}
|
|
@@ -1166,34 +1177,22 @@ export async function runPipeline(ctx, deps) {
|
|
|
1166
1177
|
});
|
|
1167
1178
|
}
|
|
1168
1179
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1180
|
+
// Opaque recovery is a valid analysis input, but its bytes are not an agent
|
|
1181
|
+
// authored surface. Keep diagnostics and advisory output visible while
|
|
1182
|
+
// withholding the edit-directed channels that would tell the agent to mutate
|
|
1183
|
+
// the observed artifact (#3226).
|
|
1184
|
+
const hasBlockers = allowAutonomousWriters && dispatchResult.hasBlockers;
|
|
1185
|
+
const actionableWarnings = allowAutonomousWriters
|
|
1186
|
+
? dispatchResult.warnings
|
|
1187
|
+
.map((diagnostic) => recordFromDispatchDiagnostic(diagnostic, cwd))
|
|
1188
|
+
.filter((warning) => Boolean(warning))
|
|
1189
|
+
: [];
|
|
1173
1190
|
const codeQualityWarnings = dispatchResult.warnings
|
|
1174
1191
|
.map((diagnostic) => recordFromCodeQualityDiagnostic(diagnostic, cwd))
|
|
1175
1192
|
.filter((warning) => Boolean(warning));
|
|
1176
1193
|
if (dispatchResult.diagnostics.length > 0) {
|
|
1177
|
-
const logger = getDiagnosticLogger();
|
|
1178
1194
|
const tracker = getDiagnosticTracker();
|
|
1179
1195
|
tracker.trackShown(dispatchResult.diagnostics);
|
|
1180
|
-
const toKey = (d) => [
|
|
1181
|
-
d.tool || "",
|
|
1182
|
-
d.id || "",
|
|
1183
|
-
d.rule || "",
|
|
1184
|
-
d.filePath || "",
|
|
1185
|
-
d.line || 0,
|
|
1186
|
-
d.column || 0,
|
|
1187
|
-
].join("|");
|
|
1188
|
-
const inlineKeys = new Set([...dispatchResult.blockers, ...dispatchResult.fixed].map(toKey));
|
|
1189
|
-
for (const d of dispatchResult.diagnostics) {
|
|
1190
|
-
logger.logCaught(d, {
|
|
1191
|
-
model: ctx.telemetry?.model ?? "unknown",
|
|
1192
|
-
sessionId: ctx.telemetry?.sessionId ?? "unknown",
|
|
1193
|
-
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
1194
|
-
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
1195
|
-
}, inlineKeys.has(toKey(d)));
|
|
1196
|
-
}
|
|
1197
1196
|
}
|
|
1198
1197
|
if (fixedCount > 0)
|
|
1199
1198
|
getDiagnosticTracker().trackAutoFixed(fixedCount);
|
|
@@ -1207,15 +1206,48 @@ export async function runPipeline(ctx, deps) {
|
|
|
1207
1206
|
// the deleted file's content), so it is dropped here rather than re-asserted.
|
|
1208
1207
|
// One bounded stat per unique cited path, only when blockers exist — zero
|
|
1209
1208
|
// cost on the clean/fast paths.
|
|
1210
|
-
const deliverableBlockers =
|
|
1209
|
+
const deliverableBlockers = hasBlockers
|
|
1211
1210
|
? dropFindingsForMissingPaths({
|
|
1212
1211
|
store: "stop-blocker",
|
|
1213
1212
|
findings: dispatchResult.blockers,
|
|
1214
1213
|
cwd,
|
|
1215
1214
|
citedPath: (b) => b.filePath || undefined,
|
|
1216
1215
|
})
|
|
1217
|
-
:
|
|
1218
|
-
|
|
1216
|
+
: [];
|
|
1217
|
+
// #3190: ONE gate, applied once here, for EVERY surface. The durable record
|
|
1218
|
+
// this function hands `runtime.recordInlineBlockers` (and through it the
|
|
1219
|
+
// turn-end `Unresolved from this turn` re-serve) used to be built from the
|
|
1220
|
+
// UNGATED `dispatchResult.blockerOutput`/`blockers`, so a blocker retracted
|
|
1221
|
+
// from the tool result above was re-delivered at turn end — and the ungated
|
|
1222
|
+
// verdict still latched the commit gate through `updateGitGuardStatus`. It is
|
|
1223
|
+
// exactly `hasBlockers && nothing-survived-the-gate` inverted: the gate
|
|
1224
|
+
// returns `[]` whenever `hasBlockers` is false, so this one expression is the
|
|
1225
|
+
// whole condition.
|
|
1226
|
+
const hasDeliverableBlockers = deliverableBlockers.length > 0;
|
|
1227
|
+
if (dispatchResult.diagnostics.length > 0) {
|
|
1228
|
+
const logger = getDiagnosticLogger();
|
|
1229
|
+
const toKey = (d) => [
|
|
1230
|
+
d.tool || "",
|
|
1231
|
+
d.id || "",
|
|
1232
|
+
d.rule || "",
|
|
1233
|
+
d.filePath || "",
|
|
1234
|
+
d.line || 0,
|
|
1235
|
+
d.column || 0,
|
|
1236
|
+
].join("|");
|
|
1237
|
+
const inlineKeys = new Set([...deliverableBlockers, ...dispatchResult.fixed].map(toKey));
|
|
1238
|
+
for (const d of dispatchResult.diagnostics) {
|
|
1239
|
+
logger.logCaught(d, {
|
|
1240
|
+
model: ctx.telemetry?.model ?? "unknown",
|
|
1241
|
+
sessionId: ctx.telemetry?.sessionId ?? "unknown",
|
|
1242
|
+
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
1243
|
+
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
1244
|
+
}, inlineKeys.has(toKey(d)));
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const deliveredOutput = !allowAutonomousWriters && dispatchResult.hasBlockers
|
|
1248
|
+
? dispatchResult.output.slice(dispatchResult.blockerOutput.length)
|
|
1249
|
+
: dispatchResult.output;
|
|
1250
|
+
if (hasBlockers && fileContent) {
|
|
1219
1251
|
// Enrich blocker output with a code snippet so the agent can see the
|
|
1220
1252
|
// exact line it wrote that caused each violation — no re-read needed.
|
|
1221
1253
|
if (deliverableBlockers.length > 0) {
|
|
@@ -1227,11 +1259,11 @@ export async function runPipeline(ctx, deps) {
|
|
|
1227
1259
|
if (rest)
|
|
1228
1260
|
output += rest;
|
|
1229
1261
|
}
|
|
1230
|
-
else if (
|
|
1262
|
+
else if (deliveredOutput) {
|
|
1231
1263
|
// #2028 review P3: this path fires when readback FAILED - raw output
|
|
1232
1264
|
// still cites possibly-deleted files. Re-render from the gated set
|
|
1233
1265
|
// (no snippets without fileContent) and keep the post-blocker slice.
|
|
1234
|
-
if (
|
|
1266
|
+
if (hasBlockers &&
|
|
1235
1267
|
deliverableBlockers.length !== dispatchResult.blockers.length) {
|
|
1236
1268
|
let gatedOut = buildEnrichedBlockerOutput(deliverableBlockers);
|
|
1237
1269
|
const rest = dispatchResult.output.slice(dispatchResult.blockerOutput.length);
|
|
@@ -1245,7 +1277,7 @@ export async function runPipeline(ctx, deps) {
|
|
|
1245
1277
|
output += `\n\n${gatedOut}`;
|
|
1246
1278
|
}
|
|
1247
1279
|
else {
|
|
1248
|
-
output += `\n\n${
|
|
1280
|
+
output += `\n\n${deliveredOutput}`;
|
|
1249
1281
|
}
|
|
1250
1282
|
}
|
|
1251
1283
|
if (fixedCount > 0) {
|
|
@@ -1340,7 +1372,7 @@ export async function runPipeline(ctx, deps) {
|
|
|
1340
1372
|
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
1341
1373
|
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
1342
1374
|
diagnostics: dispatchResult.diagnostics,
|
|
1343
|
-
blockers: dispatchResult.blockers,
|
|
1375
|
+
blockers: hasBlockers ? dispatchResult.blockers : [],
|
|
1344
1376
|
warnings: dispatchResult.warnings,
|
|
1345
1377
|
fixed: dispatchResult.fixed,
|
|
1346
1378
|
resolvedCount: dispatchResult.resolvedCount,
|
|
@@ -1351,28 +1383,44 @@ export async function runPipeline(ctx, deps) {
|
|
|
1351
1383
|
});
|
|
1352
1384
|
return {
|
|
1353
1385
|
output,
|
|
1354
|
-
|
|
1386
|
+
// #3190: the verdict every consumer reads — `updateGitGuardStatus`'s commit
|
|
1387
|
+
// latch above all — is what was DELIVERED, not what the dispatch found. The
|
|
1388
|
+
// two disagreed whenever the deleted-path gate retracted every blocker: the
|
|
1389
|
+
// tool result said `✓ clean` while the commit gate stayed latched, its
|
|
1390
|
+
// summary falling back to that very all-clear line. `emitLensAnalysisComplete`
|
|
1391
|
+
// above deliberately keeps the raw dispatch counts — it records what was
|
|
1392
|
+
// observed, not what was served.
|
|
1393
|
+
hasBlockers: hasDeliverableBlockers,
|
|
1355
1394
|
cascadePromise,
|
|
1356
1395
|
isError: false,
|
|
1357
1396
|
fileModified,
|
|
1358
1397
|
postWriteStateHash,
|
|
1359
1398
|
changedFiles,
|
|
1360
|
-
|
|
1361
|
-
|
|
1399
|
+
// #3190: re-rendered from the GATED set with `formatDiagnostics(...,
|
|
1400
|
+
// "blocking")` — the very expression `dispatcher.ts:1409` builds
|
|
1401
|
+
// `blockerOutput` with, over a subset of the very array it rendered. When
|
|
1402
|
+
// the gate drops nothing the two are byte-identical (same renderer, same
|
|
1403
|
+
// array, same display cap), so an unretracted record is unchanged; when it
|
|
1404
|
+
// drops something the stored text can no longer disagree with the tool
|
|
1405
|
+
// result, and `inline-blocker-dispositions.ts`' re-render at turn end still
|
|
1406
|
+
// matches this string exactly.
|
|
1407
|
+
inlineBlockerSummary: hasDeliverableBlockers
|
|
1408
|
+
? formatDiagnostics(deliverableBlockers, "blocking").trim() || undefined
|
|
1362
1409
|
: undefined,
|
|
1363
1410
|
// #1561 F1: taken from the very diagnostics `blockerOutput` was rendered
|
|
1364
1411
|
// from, so the provenance can never disagree with the text it guards. An
|
|
1365
1412
|
// untagged diagnostic contributes the literal "unknown", which no verdict
|
|
1366
1413
|
// claims coverage for — it pins the entry rather than silently widening
|
|
1367
1414
|
// what an LSP check is allowed to clear.
|
|
1368
|
-
inlineBlockerSources:
|
|
1415
|
+
inlineBlockerSources: hasDeliverableBlockers
|
|
1369
1416
|
? [
|
|
1370
|
-
...new Set(
|
|
1417
|
+
...new Set(deliverableBlockers.map((d) => d.tool?.trim() || "unknown")),
|
|
1371
1418
|
]
|
|
1372
1419
|
: undefined,
|
|
1373
|
-
inlineBlockerLines:
|
|
1374
|
-
?
|
|
1375
|
-
// #1641 review F2:
|
|
1420
|
+
inlineBlockerLines: hasDeliverableBlockers
|
|
1421
|
+
? deliverableBlockers
|
|
1422
|
+
// #1641 review F2: the blocker array (#3190: the deliverable subset
|
|
1423
|
+
// of `dispatchResult.blockers`) is NOT guaranteed to be
|
|
1376
1424
|
// scoped to THIS file — a chart-wide runner (helm-lint, helm-render)
|
|
1377
1425
|
// reports blocking diagnostics against other files in the chart
|
|
1378
1426
|
// (e.g. `values.yaml`) alongside `ctx.filePath`. The precedent every
|
|
@@ -1418,8 +1466,15 @@ export async function runPipeline(ctx, deps) {
|
|
|
1418
1466
|
source: "autofix",
|
|
1419
1467
|
}
|
|
1420
1468
|
: undefined,
|
|
1421
|
-
inlineBlockerFileContent:
|
|
1469
|
+
inlineBlockerFileContent: hasBlockers
|
|
1422
1470
|
? inlineBlockerFileContent
|
|
1423
1471
|
: undefined,
|
|
1472
|
+
// #3246: the very array `blockerOutput` above was rendered from, so the
|
|
1473
|
+
// turn-end policy filter and its re-render can never disagree with the
|
|
1474
|
+
// text they guard — the same provenance argument `inlineBlockerSources`
|
|
1475
|
+
// makes one field up.
|
|
1476
|
+
inlineBlockerDiagnostics: hasDeliverableBlockers
|
|
1477
|
+
? deliverableBlockers
|
|
1478
|
+
: undefined,
|
|
1424
1479
|
};
|
|
1425
1480
|
}
|
|
@@ -87,7 +87,7 @@ import { applyDispositionsMultiFile } from "../diagnostic-dispositions.js";
|
|
|
87
87
|
import { getKnipIgnorePatterns } from "../file-utils.js";
|
|
88
88
|
import { isAtOrAboveHomeDir, realpathOrResolve } from "../path-utils.js";
|
|
89
89
|
import { isSameOrWithin } from "../lsp/server.js";
|
|
90
|
-
import { incrementDegradationCount } from "../degradation-ledger.js";
|
|
90
|
+
import { incrementDegradationCount, recordDegradationOnce, } from "../degradation-ledger.js";
|
|
91
91
|
import { GitleaksClient } from "../gitleaks-client.js";
|
|
92
92
|
import { GovulncheckClient } from "../govulncheck-client.js";
|
|
93
93
|
import { isToolInstallAllowedByTrust, projectTrustDenialReason, } from "../project-trust.js";
|
|
@@ -281,8 +281,15 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
|
|
|
281
281
|
function record(id, adapted, elapsedMs, analysedRoot, analysis) {
|
|
282
282
|
if (analysedRoot) {
|
|
283
283
|
pushUnique(analyzed, id);
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
// #2962: the entry is pushed on the DECLARATION, not on its size. A
|
|
285
|
+
// coverage producer that analysed the root and proved zero files is a
|
|
286
|
+
// third state, distinct from both "covered, file absent from the set"
|
|
287
|
+
// and "this runner declares no coverage" — and it is the only one of
|
|
288
|
+
// the three that used to fall through to the whole-root id-only arm in
|
|
289
|
+
// `runnerRetirementDecision`, retiring findings for files nothing
|
|
290
|
+
// scanned. The eight non-producer runners pass no `analyzedFiles` at
|
|
291
|
+
// all and are untouched by this.
|
|
292
|
+
if (analysis?.analyzedFiles !== undefined) {
|
|
286
293
|
const root = realpathOrResolve(analysisRoot);
|
|
287
294
|
authoritativeCoverage.push({
|
|
288
295
|
runnerId: id,
|
|
@@ -291,6 +298,17 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
|
|
|
291
298
|
return realpathOrResolve(file);
|
|
292
299
|
})),
|
|
293
300
|
});
|
|
301
|
+
if (analysis.analyzedFiles.length === 0) {
|
|
302
|
+
// Bounded at one row per (producer, root) per session: the kept
|
|
303
|
+
// finding is otherwise indistinguishable from a healthy run, and
|
|
304
|
+
// "the only coverage producer proved nothing" is the fact an
|
|
305
|
+
// operator needs when mode=full keeps showing a stale finding.
|
|
306
|
+
recordDegradationOnce({
|
|
307
|
+
kind: "runner-coverage-empty",
|
|
308
|
+
subject: `${id}:${root}`,
|
|
309
|
+
reason: `${id} analysed this root but declared zero scanned files, so retained ${id} findings are kept rather than retired`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
294
312
|
}
|
|
295
313
|
}
|
|
296
314
|
timings[id] = (timings[id] ?? 0) + elapsedMs;
|
|
@@ -64,11 +64,11 @@ import * as path from "node:path";
|
|
|
64
64
|
import { toPositiveFinite } from "./env-utils.js";
|
|
65
65
|
import { FRESHNESS_CADENCE_MS } from "./freshness-cadence.js";
|
|
66
66
|
import { assignFlagConfigSection, flagConfigSectionKeys, GLOBAL_NON_FLAG_CONFIG_SECTIONS, GLOBAL_ONLY_NON_FLAG_KEYS, hasFlagConfigPath, LENS_FLAGS, PROJECT_FOREIGN_CONFIG_NAMESPACES, PROJECT_FOREIGN_NAMESPACE_HONORED_KEYS, PROJECT_NON_FLAG_CONFIG_SECTIONS, PROJECT_SCOPED_LENS_FLAGS, readFlagConfigValue, } from "./lens-flag-registry.js";
|
|
67
|
-
import { configSearchDirs, getPiLensGlobalConfigPath, PROJECT_CONFIG_BASENAMES, PROJECT_CONFIG_LOCATIONS, } from "./config-locations.js";
|
|
67
|
+
import { configSearchDirs, getPiLensGlobalConfigPath, isResolvedGlobalConfigPath, PROJECT_CONFIG_BASENAMES, PROJECT_CONFIG_LOCATIONS, } from "./config-locations.js";
|
|
68
68
|
import { deprecationRecords, ignoredRecordCollector, projectLocationFor, readConfigDocument, reportConfigReadFailure, reportPiLensConfigRecords, resolveOnePiLensConfigDocument, } from "./config-resolve.js";
|
|
69
69
|
import { finalizeRecords, } from "./config-core/records.js";
|
|
70
70
|
import { homeRelativePath, isAtOrAboveHomeDir, walkUpDirs, } from "./path-utils.js";
|
|
71
|
-
import { findPiLensConfigMarkerInDir } from "./workspace-topology.js";
|
|
71
|
+
import { dirMtimesStillFresh, findPiLensConfigMarkerInDir, } from "./workspace-topology.js";
|
|
72
72
|
import { readToolConfig } from "./tool-config.js";
|
|
73
73
|
/**
|
|
74
74
|
* Project config basenames, in DESCENDING precedence (first match wins), for
|
|
@@ -289,7 +289,7 @@ function safeDirMtimeMs(dir) {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
function discoveryCacheStillFresh(entry) {
|
|
292
|
-
if (!entry.dirMtimes
|
|
292
|
+
if (!dirMtimesStillFresh(entry.dirMtimes)) {
|
|
293
293
|
return false;
|
|
294
294
|
}
|
|
295
295
|
// #2483 round 2. When no config was found anywhere, `bearingDirMtimes`'s
|
|
@@ -344,6 +344,12 @@ function discoverPiLensProjectConfig(startDir) {
|
|
|
344
344
|
if (!info) {
|
|
345
345
|
for (const name of PROJECT_CONFIG_BASENAMES) {
|
|
346
346
|
const candidate = path.join(dir, name);
|
|
347
|
+
// The global tier reads its file by absolute path; a candidate that
|
|
348
|
+
// IS that file is refused here rather than adopted as a project
|
|
349
|
+
// config, which would double-read and double-validate it
|
|
350
|
+
// (global-config-location PR, refs #2457).
|
|
351
|
+
if (isResolvedGlobalConfigPath(candidate))
|
|
352
|
+
continue;
|
|
347
353
|
const stat = safeFileStat(candidate);
|
|
348
354
|
if (stat?.isFile()) {
|
|
349
355
|
info = {
|
|
@@ -358,6 +364,8 @@ function discoverPiLensProjectConfig(startDir) {
|
|
|
358
364
|
}
|
|
359
365
|
for (const location of LEGACY_PROJECT_LOCATIONS) {
|
|
360
366
|
const file = path.join(dir, location.relativePath);
|
|
367
|
+
if (isResolvedGlobalConfigPath(file))
|
|
368
|
+
continue;
|
|
361
369
|
const stat = safeFileStat(file);
|
|
362
370
|
if (!stat?.isFile())
|
|
363
371
|
continue;
|