@herbertgao/pi-extensions 2026.8.5 → 2026.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +24 -17
- package/node_modules/@narumitw/pi-btw/package.json +5 -5
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +95 -0
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +230 -50
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
- package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
- package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
- package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
- package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
- package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
- package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
- package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +29 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
- package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
- package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
- package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
- package/node_modules/pi-lens/dist/index.js +4827 -1633
- package/node_modules/pi-lens/dist/mcp/server.js +8 -2
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +38 -13
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
- package/node_modules/pi-lens/docs/features.md +14 -1
- package/node_modules/pi-lens/docs/globalconfig.md +11 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
- package/node_modules/pi-lens/docs/settings.md +6 -0
- package/node_modules/pi-lens/docs/usage.md +23 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +79 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
- package/node_modules/pi-mcp-adapter/README.md +4 -1
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
- package/node_modules/pi-mcp-adapter/types.ts +93 -10
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +9 -9
|
@@ -25,25 +25,38 @@ import { CacheManager, MCP_TURN_STATE_OWNER_ID, } from "../cache-manager.js";
|
|
|
25
25
|
import { resetDispatchBaselines } from "../dispatch/integration.js";
|
|
26
26
|
import { resetFormatService } from "../format-service.js";
|
|
27
27
|
import { getLSPService, resetLSPService } from "../lsp/index.js";
|
|
28
|
-
import { consumeSessionStartGuidance, consumeTestFindings, consumeTurnEndFindings, peekTestFindings, peekTurnEndFindings, } from "../runtime-context.js";
|
|
28
|
+
import { acknowledgeTestFindings, acknowledgeTurnEndFindings, consumeSessionStartGuidance, consumeTestFindings, consumeTurnEndFindings, peekTestFindings, peekTurnEndFindings, } from "../runtime-context.js";
|
|
29
29
|
import { RuntimeCoordinator } from "../runtime-coordinator.js";
|
|
30
30
|
import { handleSessionStart } from "../runtime-session.js";
|
|
31
31
|
import { handleTurnEnd } from "../runtime-turn.js";
|
|
32
32
|
import { createMcpHost } from "./host-shim.js";
|
|
33
33
|
let contextPromise;
|
|
34
|
+
let resolvedRuntime;
|
|
34
35
|
/** Lazily build (once) the persistent session context shared across MCP calls. */
|
|
35
36
|
export function getMcpSessionContext() {
|
|
36
|
-
contextPromise ??= (async () =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
contextPromise ??= (async () => {
|
|
38
|
+
const context = {
|
|
39
|
+
runtime: new RuntimeCoordinator(),
|
|
40
|
+
cacheManager: new CacheManager(),
|
|
41
|
+
astGrepClient: new AstGrepClient(),
|
|
42
|
+
clients: await loadBootstrapClients(),
|
|
43
|
+
};
|
|
44
|
+
resolvedRuntime = context.runtime;
|
|
45
|
+
return context;
|
|
46
|
+
})();
|
|
42
47
|
return contextPromise;
|
|
43
48
|
}
|
|
49
|
+
/** Synchronous peek at the session runtime, for construction sites that cannot
|
|
50
|
+
* await (e.g. the MCP server's tool wiring). Returns undefined until a session
|
|
51
|
+
* context has actually been built — advisory validation then honestly skips
|
|
52
|
+
* the session-identity check instead of comparing against a phantom session. */
|
|
53
|
+
export function peekMcpSessionRuntime() {
|
|
54
|
+
return resolvedRuntime;
|
|
55
|
+
}
|
|
44
56
|
/** Test hook — drop the cached context so a fresh one is built next call. */
|
|
45
57
|
export function _resetMcpSessionContext() {
|
|
46
58
|
contextPromise = undefined;
|
|
59
|
+
resolvedRuntime = undefined;
|
|
47
60
|
}
|
|
48
61
|
const noop = () => { };
|
|
49
62
|
function joinMessages(consumed) {
|
|
@@ -219,13 +232,13 @@ async function runTurnEndNow(cwd, files = [], deferredDelivery = false) {
|
|
|
219
232
|
});
|
|
220
233
|
const outcome = deferredDelivery
|
|
221
234
|
? {
|
|
222
|
-
turnEnd: joinMessages(peekTurnEndFindings(ctx.cacheManager, cwd)),
|
|
223
|
-
tests: joinMessages(peekTestFindings(ctx.cacheManager, cwd)),
|
|
235
|
+
turnEnd: joinMessages(peekTurnEndFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
|
|
236
|
+
tests: joinMessages(peekTestFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
|
|
224
237
|
filesRegistered: registered,
|
|
225
238
|
}
|
|
226
239
|
: {
|
|
227
|
-
turnEnd: joinMessages(consumeTurnEndFindings(ctx.cacheManager, cwd)),
|
|
228
|
-
tests: joinMessages(consumeTestFindings(ctx.cacheManager, cwd)),
|
|
240
|
+
turnEnd: joinMessages(consumeTurnEndFindings(ctx.cacheManager, cwd, ctx.runtime)),
|
|
241
|
+
tests: joinMessages(consumeTestFindings(ctx.cacheManager, cwd, ctx.runtime)),
|
|
229
242
|
filesRegistered: registered,
|
|
230
243
|
};
|
|
231
244
|
return {
|
|
@@ -233,8 +246,8 @@ async function runTurnEndNow(cwd, files = [], deferredDelivery = false) {
|
|
|
233
246
|
commit: () => {
|
|
234
247
|
if (!deferredDelivery)
|
|
235
248
|
return;
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
acknowledgeTurnEndFindings(ctx.cacheManager, cwd);
|
|
250
|
+
acknowledgeTestFindings(ctx.cacheManager, cwd);
|
|
238
251
|
},
|
|
239
252
|
};
|
|
240
253
|
}
|
|
@@ -306,16 +319,16 @@ function runTurnEndForIpcNow(cwd) {
|
|
|
306
319
|
}
|
|
307
320
|
const ctx = await getMcpSessionContext();
|
|
308
321
|
const cachedOutcome = {
|
|
309
|
-
turnEnd: joinMessages(peekTurnEndFindings(ctx.cacheManager, cwd)),
|
|
310
|
-
tests: joinMessages(peekTestFindings(ctx.cacheManager, cwd)),
|
|
322
|
+
turnEnd: joinMessages(peekTurnEndFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
|
|
323
|
+
tests: joinMessages(peekTestFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
|
|
311
324
|
filesRegistered: 0,
|
|
312
325
|
};
|
|
313
326
|
const transaction = hasTurnEndFindings(cachedOutcome)
|
|
314
327
|
? {
|
|
315
328
|
outcome: cachedOutcome,
|
|
316
329
|
commit: () => {
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
acknowledgeTurnEndFindings(ctx.cacheManager, cwd);
|
|
331
|
+
acknowledgeTestFindings(ctx.cacheManager, cwd);
|
|
319
332
|
},
|
|
320
333
|
}
|
|
321
334
|
: await runTurnEndNow(cwd, [], true);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider derivation for worklog/disposition attribution (#1448).
|
|
3
|
+
*
|
|
4
|
+
* The host reports `provider` and `model` as separate fields on its telemetry
|
|
5
|
+
* events (see `updateRuntimeIdentityFromEvent` in index.ts and the
|
|
6
|
+
* `tool_result` handler in clients/runtime-tool-result.ts) MOST of the time —
|
|
7
|
+
* so the common case needs no parsing at all. This parser exists only for the
|
|
8
|
+
* fallback: a model id supplied without an explicit provider.
|
|
9
|
+
*
|
|
10
|
+
* Derivation rule (conservative — returns "" rather than guess):
|
|
11
|
+
* 1. A single `/` or `:` separator splits the id into `<provider>/<rest>`
|
|
12
|
+
* or `<provider>:<rest>` — e.g. "anthropic/claude-sonnet-4-5",
|
|
13
|
+
* "openai:gpt-5". The left segment is lowercased and returned AS-IS
|
|
14
|
+
* (no allowlist check) since an explicit separator is deliberate host
|
|
15
|
+
* structure, not a guess.
|
|
16
|
+
* 2. No separator: match a known model-id PREFIX against a small table of
|
|
17
|
+
* vendor naming conventions (claude- -> anthropic; gpt-, o1-, o3- ->
|
|
18
|
+
* openai; gemini- -> google; llama- -> meta; mistral-, mixtral- ->
|
|
19
|
+
* mistral; command- -> cohere). First match wins.
|
|
20
|
+
* 3. Anything else (unrecognized prefix, multiple separators, empty
|
|
21
|
+
* string) is ambiguous -> "".
|
|
22
|
+
*/
|
|
23
|
+
const KNOWN_PREFIXES = [
|
|
24
|
+
["claude-", "anthropic"],
|
|
25
|
+
["gpt-", "openai"],
|
|
26
|
+
["o1-", "openai"],
|
|
27
|
+
["o3-", "openai"],
|
|
28
|
+
["gemini-", "google"],
|
|
29
|
+
["llama-", "meta"],
|
|
30
|
+
["mistral-", "mistral"],
|
|
31
|
+
["mixtral-", "mistral"],
|
|
32
|
+
["command-", "cohere"],
|
|
33
|
+
];
|
|
34
|
+
export function deriveProviderFromModelId(modelId) {
|
|
35
|
+
const id = modelId?.trim().toLowerCase();
|
|
36
|
+
if (!id)
|
|
37
|
+
return "";
|
|
38
|
+
const separators = [...id].filter((c) => c === "/" || c === ":").length;
|
|
39
|
+
if (separators === 1) {
|
|
40
|
+
const sep = id.includes("/") ? "/" : ":";
|
|
41
|
+
const [providerPart, ...rest] = id.split(sep);
|
|
42
|
+
if (providerPart && rest.join(sep).length > 0)
|
|
43
|
+
return providerPart;
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
if (separators > 1)
|
|
47
|
+
return "";
|
|
48
|
+
for (const [prefix, provider] of KNOWN_PREFIXES) {
|
|
49
|
+
if (id.startsWith(prefix))
|
|
50
|
+
return provider;
|
|
51
|
+
}
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
@@ -843,7 +843,25 @@ export async function runPipeline(ctx, deps) {
|
|
|
843
843
|
});
|
|
844
844
|
// --- 3. Auto-fix ---
|
|
845
845
|
phase.start("autofix");
|
|
846
|
-
|
|
846
|
+
let fixedCount = 0;
|
|
847
|
+
let autofixTools = [];
|
|
848
|
+
let attemptedTools = [];
|
|
849
|
+
let autofixChangedFiles = [];
|
|
850
|
+
let fixRefresh = false;
|
|
851
|
+
let autofixSkipReason;
|
|
852
|
+
if (ctx.autofixMode === "deferred") {
|
|
853
|
+
autofixSkipReason = "deferred_to_agent_end";
|
|
854
|
+
dbg(`autofix: deferred until agent_end for ${filePath}`);
|
|
855
|
+
}
|
|
856
|
+
else
|
|
857
|
+
({
|
|
858
|
+
fixedCount,
|
|
859
|
+
autofixTools,
|
|
860
|
+
attemptedTools,
|
|
861
|
+
changedFiles: autofixChangedFiles,
|
|
862
|
+
needsContentRefresh: fixRefresh,
|
|
863
|
+
skipReason: autofixSkipReason,
|
|
864
|
+
} = await runAutofix(filePath, cwd, getFlag, dbg, deps, getFlagSource));
|
|
847
865
|
for (const changedFile of autofixChangedFiles) {
|
|
848
866
|
piChangedFiles.add(path.resolve(changedFile));
|
|
849
867
|
}
|
|
@@ -903,6 +921,8 @@ export async function runPipeline(ctx, deps) {
|
|
|
903
921
|
}, {
|
|
904
922
|
projectRoot: ctx.projectRoot,
|
|
905
923
|
writeIndex: ctx.telemetry?.writeIndex,
|
|
924
|
+
telemetryModel: ctx.telemetry?.modelId,
|
|
925
|
+
telemetryProvider: ctx.telemetry?.provider,
|
|
906
926
|
});
|
|
907
927
|
recordDiagnostics(filePath, dispatchResult.diagnostics, ctx.telemetry?.writeIndex);
|
|
908
928
|
// #502: emit the write batch's FINAL diagnostic state immediately after
|
|
@@ -999,7 +1019,11 @@ export async function runPipeline(ctx, deps) {
|
|
|
999
1019
|
const fileList = changedList.length
|
|
1000
1020
|
? "\nModified files:\n" + topFiles + overflow
|
|
1001
1021
|
: "";
|
|
1002
|
-
|
|
1022
|
+
const targetHasAuthoritativeAttachment = ctx.autofixMode !== "deferred" &&
|
|
1023
|
+
autofixChangedFiles.some((changedFile) => path.resolve(changedFile) === path.resolve(filePath));
|
|
1024
|
+
output += targetHasAuthoritativeAttachment
|
|
1025
|
+
? `\n\n⚠️ **The attached full content for ${toRunnerDisplayPath(cwd, filePath)} is authoritative after autofix. You MUST re-read any other modified side-effect files before editing them.**${fileList}`
|
|
1026
|
+
: `\n\n⚠️ **File was modified by auto-format/fix. You MUST re-read modified file(s) before making any further edits — the content on disk has changed (whitespace, indentation, quotes, or code). Editing from memory will produce mismatches.**${fileList}`;
|
|
1003
1027
|
}
|
|
1004
1028
|
phase.end("dispatch_lint", {
|
|
1005
1029
|
hasOutput: !!dispatchResult.output,
|
|
@@ -1088,5 +1112,8 @@ export async function runPipeline(ctx, deps) {
|
|
|
1088
1112
|
formattersUsed,
|
|
1089
1113
|
fixedCount,
|
|
1090
1114
|
autofixTools,
|
|
1115
|
+
postMutation: ctx.autofixMode !== "deferred" && autofixChangedFiles.some((f) => path.resolve(f) === path.resolve(filePath)) && fileContent !== undefined
|
|
1116
|
+
? { filePath: path.resolve(filePath), content: fileContent, source: "autofix" }
|
|
1117
|
+
: undefined,
|
|
1091
1118
|
};
|
|
1092
1119
|
}
|
|
@@ -383,7 +383,7 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
|
|
|
383
383
|
cold.push("test-runner");
|
|
384
384
|
return;
|
|
385
385
|
}
|
|
386
|
-
record("test-runner", testRunnerFindingsToProjectDiagnostics(cached.data), Date.now() - startMs);
|
|
386
|
+
record("test-runner", testRunnerFindingsToProjectDiagnostics(cached.data, analysisRoot, options.runtime), Date.now() - startMs);
|
|
387
387
|
}),
|
|
388
388
|
];
|
|
389
389
|
// Swallow any later rejection so an aborted-and-abandoned task can never
|
package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { advisoryPathKey, validateAdvisoryProvenance, } from "../../advisory-provenance.js";
|
|
1
4
|
function failureMessage(failure) {
|
|
2
5
|
const firstLine = failure.message.split("\n")[0]?.slice(0, 300) ?? "";
|
|
3
6
|
return firstLine ? `${failure.name}: ${firstLine}` : failure.name;
|
|
@@ -71,8 +74,26 @@ export function testResultToProjectDiagnostics(result, stale = false) {
|
|
|
71
74
|
},
|
|
72
75
|
];
|
|
73
76
|
}
|
|
74
|
-
export function testRunnerFindingsToProjectDiagnostics(cache) {
|
|
77
|
+
export function testRunnerFindingsToProjectDiagnostics(cache, cwd, runtime) {
|
|
75
78
|
if (!cache.results || cache.results.length === 0)
|
|
76
79
|
return [];
|
|
77
|
-
|
|
80
|
+
const validation = cwd
|
|
81
|
+
? validateAdvisoryProvenance(cache, cwd, runtime)
|
|
82
|
+
: { status: "unknown", reasons: ["validation-context-missing"] };
|
|
83
|
+
const root = cwd ?? process.cwd();
|
|
84
|
+
const missingKeys = new Set((cache.provenance?.files ?? [])
|
|
85
|
+
.filter((file) => !fs.existsSync(path.resolve(root, file.path)))
|
|
86
|
+
.map((file) => advisoryPathKey(file.path, root)));
|
|
87
|
+
const historical = cache.superseded === true || cache.stale === true || validation.status !== "current";
|
|
88
|
+
return cache.results
|
|
89
|
+
.filter((result) => !missingKeys.has(advisoryPathKey(result.file, root)))
|
|
90
|
+
.flatMap((result) => testResultToProjectDiagnostics(result, historical))
|
|
91
|
+
.map((diagnostic) => historical ? {
|
|
92
|
+
...diagnostic,
|
|
93
|
+
severity: "info",
|
|
94
|
+
semantic: "none",
|
|
95
|
+
message: diagnostic.message.startsWith("[stale")
|
|
96
|
+
? diagnostic.message
|
|
97
|
+
: `[historical — re-run to confirm] ${diagnostic.message}`,
|
|
98
|
+
} : diagnostic);
|
|
78
99
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { detectFileRole } from "../file-role.js";
|
|
1
2
|
import { normalizeMapKey } from "../path-utils.js";
|
|
2
3
|
import { findModuleForPath, getDownstreamModules, getModuleSourceFiles, } from "./workspace-modules.js";
|
|
3
4
|
function dedupe(items) {
|
|
@@ -76,6 +77,29 @@ export function computeImpactCascade(graph, changedFile, moduleGraph) {
|
|
|
76
77
|
const normalizedFile = normalizeMapKey(changedFile);
|
|
77
78
|
const fileNodeId = graph.fileNodes.get(normalizedFile);
|
|
78
79
|
if (!fileNodeId) {
|
|
80
|
+
// #1445: a missing node has two causes that look identical here but mean
|
|
81
|
+
// opposite things — "the graph SHOULD know this file but doesn't" (a real
|
|
82
|
+
// gap) versus "this file's role is excluded from the graph by design"
|
|
83
|
+
// (test files, #260 — every graph-admission chokepoint in builder.ts
|
|
84
|
+
// guards on this exact condition, `detectFileRole(file) === "test"`;
|
|
85
|
+
// reused verbatim here rather than a second role list). A test edit not
|
|
86
|
+
// cascading is expected behavior, not a graph failure, so it must not
|
|
87
|
+
// produce the "review graph was unavailable" advisory that misled agents
|
|
88
|
+
// into distrusting a healthy graph 19% of the time in dogfooding.
|
|
89
|
+
if (detectFileRole(normalizedFile) === "test") {
|
|
90
|
+
return {
|
|
91
|
+
filePath: normalizedFile,
|
|
92
|
+
changedSymbols: [],
|
|
93
|
+
directImporters: [],
|
|
94
|
+
directCallers: [],
|
|
95
|
+
neighborFiles: [],
|
|
96
|
+
riskFlags: [],
|
|
97
|
+
indeterminate: {
|
|
98
|
+
reason: "excluded_by_role",
|
|
99
|
+
detail: "test-role file — excluded from the review graph by design (#260)",
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
79
103
|
// #1023: the changed file has no node in the graph — we CANNOT enumerate
|
|
80
104
|
// its dependents, so this empty result is "couldn't compute", NOT "nothing
|
|
81
105
|
// depends on it". Mark it indeterminate so callers surface an honest
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1479/#1480: one place decides whether a test run was actually timed, and
|
|
3
|
+
* one place renders the answer.
|
|
4
|
+
*
|
|
5
|
+
* `TestResult.duration` is optional, and the two states are different facts.
|
|
6
|
+
* A present number is a MEASUREMENT — including `0`, because pytest really
|
|
7
|
+
* does print `in 0.00s` and a suite whose `startTime` equals its `endTime`
|
|
8
|
+
* really did run in under a millisecond. An absent value means no
|
|
9
|
+
* runner-reported figure was found at all.
|
|
10
|
+
*
|
|
11
|
+
* That contract was written down once, in the doc comment on
|
|
12
|
+
* `TestResult.duration`, and then re-derived at every site that reads or
|
|
13
|
+
* produces a duration. A reader of any one of those sites cannot see the
|
|
14
|
+
* contract, and a producer that reaches for `0` when it means "did not
|
|
15
|
+
* measure" reintroduces exactly the defect #1479 removed. These three
|
|
16
|
+
* functions are the contract in executable form: `isMeasuredDuration`
|
|
17
|
+
* classifies, `toMeasuredDurationMs` normalises a freshly parsed figure into
|
|
18
|
+
* it, and `formatRunDurationMs` renders it.
|
|
19
|
+
*
|
|
20
|
+
* NOTE FOR ANYONE EDITING `isMeasuredDuration`: the predicate is `>= 0`, not
|
|
21
|
+
* `> 0`. A measured zero is real. `> 0` looks like a harmless tightening and
|
|
22
|
+
* is the single change that would silently restore the bug.
|
|
23
|
+
*
|
|
24
|
+
* Kept in its own module rather than exported from `test-runner-client.ts`:
|
|
25
|
+
* `runtime-turn.ts` needs it on the hot turn-end path, and the client is
|
|
26
|
+
* deliberately loaded lazily (`bootstrap.ts` dynamic-imports it), so a value
|
|
27
|
+
* import from there would drag the whole runner into startup.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* True only for a value that represents a real measurement.
|
|
31
|
+
*
|
|
32
|
+
* `0` is measured. `undefined`, `null`, `NaN`, the infinities, and a negative
|
|
33
|
+
* figure are not: the last three can only come from a garbled parse, and a
|
|
34
|
+
* garbled parse must degrade to "we did not measure this", never to a wrong
|
|
35
|
+
* number.
|
|
36
|
+
*/
|
|
37
|
+
export function isMeasuredDuration(duration) {
|
|
38
|
+
return (typeof duration === "number" && Number.isFinite(duration) && duration >= 0);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalise a parsed elapsed time to the `TestResult.duration` contract.
|
|
42
|
+
*
|
|
43
|
+
* Returns whole milliseconds for a real reading — `0` included — and
|
|
44
|
+
* `undefined` for anything that is not one, so a producer never has to spell
|
|
45
|
+
* out the absent case itself.
|
|
46
|
+
*/
|
|
47
|
+
export function toMeasuredDurationMs(duration) {
|
|
48
|
+
return isMeasuredDuration(duration) ? Math.round(duration) : undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Render a duration for a log line: `123ms`, or `unmeasured` when absent. */
|
|
51
|
+
export function formatRunDurationMs(duration) {
|
|
52
|
+
return isMeasuredDuration(duration)
|
|
53
|
+
? `${Math.round(duration)}ms`
|
|
54
|
+
: "unmeasured";
|
|
55
|
+
}
|
|
@@ -7,9 +7,10 @@ import { logLatency } from "./latency-logger.js";
|
|
|
7
7
|
import { isPathIgnoredByProject } from "./file-utils.js";
|
|
8
8
|
import { newLspMutationCorrelationId } from "./lsp-mutation.js";
|
|
9
9
|
import { getGlobalActionableWarningMaxFixes, } from "./lens-config.js";
|
|
10
|
-
import { resyncLspFile, runFormatPhase } from "./pipeline.js";
|
|
10
|
+
import { resyncLspFile, runAutofix, runFormatPhase } from "./pipeline.js";
|
|
11
11
|
import { getAmbientAbortSignal } from "./safe-spawn.js";
|
|
12
12
|
import { appendProjectChange, } from "./project-changes.js";
|
|
13
|
+
import { getAutofixPolicyForFile, hasBiomeConfig, hasDetektConfig, hasEslintConfig, hasGolangciConfig, hasKtfmtConfig, hasKtlintConfig, hasOxlintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, } from "./tool-policy.js";
|
|
13
14
|
/**
|
|
14
15
|
* A queued file is claimed by any flush once it has sat unclaimed by its own
|
|
15
16
|
* owning session for this long — recovery for an orphaned record whose
|
|
@@ -40,13 +41,49 @@ function recordProjectChange(args) {
|
|
|
40
41
|
args.dbg(`project change log append failed for ${args.filePath}: ${err}`);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, dbg, runtime, cacheManager, getFormatService, currentSessionId, staleAfterMs = DEFERRED_FORMAT_STALE_AFTER_MS, }) {
|
|
44
|
+
export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, dbg, runtime, cacheManager, getFormatService, biomeClient, ruffClient, getAutofixClients, currentSessionId, staleAfterMs = DEFERRED_FORMAT_STALE_AFTER_MS, }) {
|
|
44
45
|
// #791: ownership-filtered drain — records queued by a DIFFERENT known
|
|
45
46
|
// session (e.g. a concurrent in-process secondary/subagent) stay queued
|
|
46
47
|
// for their owner's own agent_end, unless they've been stale long enough
|
|
47
48
|
// to fall back to "claim as orphaned" (see claimDeferredFormatFiles).
|
|
48
|
-
const { claimed, staleClaimed, deferredToOwner } = runtime.
|
|
49
|
+
const { claimed, staleClaimed, deferredToOwner } = runtime.claimDeferredMutations(currentSessionId, Date.now(), staleAfterMs);
|
|
49
50
|
const records = [...claimed, ...staleClaimed];
|
|
51
|
+
const requeuedKinds = new Set();
|
|
52
|
+
// S2d (gap 4, #1432 review): the aggregate `agent_end_deferred_mutation_drain`
|
|
53
|
+
// row only carried a coalesced `requeuedKinds` set with no reason, per-call
|
|
54
|
+
// file count, or tool provenance — every requeue (abort mid-drain, missing
|
|
55
|
+
// autofix clients, an autofix/format failure) collapsed into one
|
|
56
|
+
// indistinguishable bucket. Log one bounded record PER requeue call instead,
|
|
57
|
+
// so a real incident can tell "aborted with N files still queued" apart
|
|
58
|
+
// from "M files failed to format".
|
|
59
|
+
const requeue = (pending, reason) => {
|
|
60
|
+
if (pending.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
const kinds = new Set();
|
|
63
|
+
const toolNames = new Set();
|
|
64
|
+
for (const record of pending) {
|
|
65
|
+
for (const kind of record.kinds) {
|
|
66
|
+
requeuedKinds.add(kind);
|
|
67
|
+
kinds.add(kind);
|
|
68
|
+
}
|
|
69
|
+
for (const toolName of record.toolNames)
|
|
70
|
+
toolNames.add(toolName);
|
|
71
|
+
}
|
|
72
|
+
logLatency({
|
|
73
|
+
type: "phase",
|
|
74
|
+
toolName: "agent_end",
|
|
75
|
+
filePath: ctxCwd ?? runtime.projectRoot,
|
|
76
|
+
phase: "agent_end_deferred_mutation_requeue",
|
|
77
|
+
durationMs: 0,
|
|
78
|
+
metadata: {
|
|
79
|
+
reason,
|
|
80
|
+
kinds: [...kinds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
81
|
+
fileCount: pending.length,
|
|
82
|
+
toolNames: [...toolNames].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
runtime.requeueDeferredMutations(pending);
|
|
86
|
+
};
|
|
50
87
|
const rootActionableAutofixEnabled = !!getFlag("lens-actionable-warning-autofix");
|
|
51
88
|
// A path-aware source resolver signals that nested configs may re-enable
|
|
52
89
|
// actionable autofix even when the root default is off. Legacy/test hosts
|
|
@@ -97,6 +134,86 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
97
134
|
// publishFilesTouched call so consumers can tell "pi-lens formatted this"
|
|
98
135
|
// from an agent edit.
|
|
99
136
|
const deferredFormatFixes = [];
|
|
137
|
+
const deferredAutofixFixes = [];
|
|
138
|
+
const deferredAutofixChanged = new Set();
|
|
139
|
+
// Mutation ordering is intentional: lint --write may disturb wrapping, so
|
|
140
|
+
// autofix reaches the final edited state first and formatting stabilizes it.
|
|
141
|
+
const ambientSignal = getAmbientAbortSignal();
|
|
142
|
+
const executedAutofixScopes = new Set();
|
|
143
|
+
const autofixRecords = records.filter((candidate) => candidate.kinds.has("autofix"));
|
|
144
|
+
if (autofixRecords.length > 0 && (!biomeClient || !ruffClient) && getAutofixClients) {
|
|
145
|
+
({ biomeClient, ruffClient } = await getAutofixClients());
|
|
146
|
+
}
|
|
147
|
+
for (let autofixIndex = 0; autofixIndex < autofixRecords.length; autofixIndex++) {
|
|
148
|
+
const record = autofixRecords[autofixIndex];
|
|
149
|
+
if (ambientSignal?.aborted) {
|
|
150
|
+
requeue(autofixRecords.slice(autofixIndex).map((pending) => ({
|
|
151
|
+
...pending,
|
|
152
|
+
kinds: new Set(["autofix"]),
|
|
153
|
+
toolNames: new Set(pending.toolNames),
|
|
154
|
+
})), "abort");
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
const filePath = path.resolve(record.filePath);
|
|
158
|
+
if (!nodeFs.existsSync(filePath)) {
|
|
159
|
+
summary.skipped.push({ filePath, reason: "missing" });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!biomeClient || !ruffClient) {
|
|
163
|
+
dbg(`agent_end deferred_autofix: clients unavailable for ${filePath}`);
|
|
164
|
+
requeue([{ ...record, kinds: new Set(["autofix"]), toolNames: new Set(record.toolNames) }], "clients-unavailable");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const policy = getAutofixPolicyForFile(filePath, {
|
|
168
|
+
hasEslintConfig: hasEslintConfig(record.cwd),
|
|
169
|
+
hasStylelintConfig: hasStylelintConfig(record.cwd),
|
|
170
|
+
hasSqlfluffConfig: hasSqlfluffConfig(record.cwd),
|
|
171
|
+
hasRubocopConfig: hasRubocopConfig(record.cwd),
|
|
172
|
+
hasBiomeConfig: hasBiomeConfig(record.cwd),
|
|
173
|
+
hasGolangciConfig: hasGolangciConfig(record.cwd),
|
|
174
|
+
hasDetektConfig: hasDetektConfig(record.cwd),
|
|
175
|
+
hasOxlintConfig: hasOxlintConfig(record.cwd),
|
|
176
|
+
hasKtfmtConfig: hasKtfmtConfig(record.cwd),
|
|
177
|
+
hasKtlintConfig: hasKtlintConfig(record.cwd),
|
|
178
|
+
});
|
|
179
|
+
const scopeKey = policy?.scope && policy.scope !== "file"
|
|
180
|
+
? `${policy.defaultTool ?? "unknown"}:${path.resolve(record.cwd)}`
|
|
181
|
+
: `${policy?.defaultTool ?? "unknown"}:${filePath}`;
|
|
182
|
+
if (executedAutofixScopes.has(scopeKey))
|
|
183
|
+
continue;
|
|
184
|
+
executedAutofixScopes.add(scopeKey);
|
|
185
|
+
try {
|
|
186
|
+
const result = await runAutofix(filePath, record.cwd, getFlag, dbg, { biomeClient, ruffClient, fixedThisTurn: runtime.fixedThisTurn }, getFlagSource);
|
|
187
|
+
const tools = result.autofixTools.map((label) => label.split(":")[0]);
|
|
188
|
+
for (const changed of result.changedFiles) {
|
|
189
|
+
const changedPath = path.resolve(changed);
|
|
190
|
+
deferredAutofixChanged.add(changedPath);
|
|
191
|
+
for (const tool of tools)
|
|
192
|
+
deferredAutofixFixes.push({ path: changedPath, tool, kind: "autofix" });
|
|
193
|
+
if (!nodeFs.existsSync(changedPath))
|
|
194
|
+
continue;
|
|
195
|
+
recordProjectChange({ runtime, cwd: record.turnStateCwd, filePath: changedPath, source: "autofix", dbg });
|
|
196
|
+
if (!getFlag("no-read-guard"))
|
|
197
|
+
runtime.readGuard.recordWritten(changedPath);
|
|
198
|
+
const content = nodeFs.readFileSync(changedPath, "utf-8");
|
|
199
|
+
cacheManager.addModifiedRange(changedPath, { start: 1, end: content.split("\n").length }, /^import\s/m.test(content), record.turnStateCwd, currentSessionId ?? runtime.telemetrySessionId, "pi");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
+
summary.failed.push({ filePath, errors: [message] });
|
|
205
|
+
requeue([{ ...record, kinds: new Set(["autofix"]), toolNames: new Set(record.toolNames) }], "autofix-failed");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (deferredAutofixFixes.length > 0) {
|
|
209
|
+
publishFilesTouched({
|
|
210
|
+
reason: "autofix",
|
|
211
|
+
paths: [...new Set(deferredAutofixFixes.map((fix) => fix.path))],
|
|
212
|
+
cwd: ctxCwd ?? runtime.projectRoot,
|
|
213
|
+
dbg,
|
|
214
|
+
fixes: deferredAutofixFixes,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
100
217
|
dbg(`agent_end deferred_format: ${records.length} file(s)`);
|
|
101
218
|
logLatency({
|
|
102
219
|
type: "phase",
|
|
@@ -127,9 +244,10 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
127
244
|
cwd: ctxCwd ?? runtime.projectRoot,
|
|
128
245
|
paths: records.map((r) => r.filePath),
|
|
129
246
|
dbg,
|
|
247
|
+
kinds: [...new Set(records.flatMap((record) => [...record.kinds]))],
|
|
130
248
|
});
|
|
131
249
|
}
|
|
132
|
-
const formatRecords = records.filter((record) => {
|
|
250
|
+
const formatRecords = records.filter((record) => record.kinds.has("format")).filter((record) => {
|
|
133
251
|
const disabled = !!getFlag("no-autoformat", record.filePath);
|
|
134
252
|
if (disabled) {
|
|
135
253
|
const source = getFlagSource?.("no-autoformat", record.filePath);
|
|
@@ -145,7 +263,6 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
145
263
|
const work = new Array(formatRecords.length);
|
|
146
264
|
const started = new Set();
|
|
147
265
|
let nextIndex = 0;
|
|
148
|
-
const ambientSignal = getAmbientAbortSignal();
|
|
149
266
|
const worker = async () => {
|
|
150
267
|
while (nextIndex < formatRecords.length) {
|
|
151
268
|
const index = nextIndex++;
|
|
@@ -179,7 +296,11 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
179
296
|
};
|
|
180
297
|
await Promise.all(Array.from({ length: Math.min(DEFERRED_FORMAT_CONCURRENCY, formatRecords.length) }, () => worker()));
|
|
181
298
|
if (ambientSignal?.aborted) {
|
|
182
|
-
|
|
299
|
+
requeue(formatRecords.filter((_, index) => !started.has(index)).map((record) => ({
|
|
300
|
+
...record,
|
|
301
|
+
kinds: new Set(["format"]),
|
|
302
|
+
toolNames: new Set(record.toolNames),
|
|
303
|
+
})), "abort");
|
|
183
304
|
}
|
|
184
305
|
for (const entry of work) {
|
|
185
306
|
if (!entry)
|
|
@@ -188,12 +309,15 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
188
309
|
const { record, filePath, fileStart } = entry;
|
|
189
310
|
if (entry.missing) {
|
|
190
311
|
dbg(`agent_end deferred_format skipped missing file: ${filePath}`);
|
|
191
|
-
summary.skipped.
|
|
312
|
+
if (!summary.skipped.some((item) => item.filePath === filePath && item.reason === "missing")) {
|
|
313
|
+
summary.skipped.push({ filePath, reason: "missing" });
|
|
314
|
+
}
|
|
192
315
|
continue;
|
|
193
316
|
}
|
|
194
317
|
if (entry.error) {
|
|
195
318
|
dbg(`agent_end deferred_format failed for ${filePath}: ${entry.error}`);
|
|
196
319
|
summary.failed.push({ filePath, errors: [entry.error] });
|
|
320
|
+
requeue([{ ...record, kinds: new Set(["format"]), toolNames: new Set(record.toolNames) }], "format-failed");
|
|
197
321
|
continue;
|
|
198
322
|
}
|
|
199
323
|
const result = entry.result;
|
|
@@ -202,6 +326,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
202
326
|
summary.formatted++;
|
|
203
327
|
if (result.formatFailures.length > 0) {
|
|
204
328
|
summary.failed.push({ filePath, errors: result.formatFailures });
|
|
329
|
+
requeue([{ ...record, kinds: new Set(["format"]), toolNames: new Set(record.toolNames) }], "format-failed");
|
|
205
330
|
}
|
|
206
331
|
if (result.formatChanged) {
|
|
207
332
|
summary.changed.push(filePath);
|
|
@@ -268,6 +393,14 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
268
393
|
});
|
|
269
394
|
}
|
|
270
395
|
}
|
|
396
|
+
// LSP sees only authoritative content after both mutation phases. In
|
|
397
|
+
// particular, never publish the autofix intermediate state before format.
|
|
398
|
+
for (const changedPath of deferredAutofixChanged) {
|
|
399
|
+
if (!nodeFs.existsSync(changedPath))
|
|
400
|
+
continue;
|
|
401
|
+
const content = nodeFs.readFileSync(changedPath, "utf-8");
|
|
402
|
+
await resyncLspFile(changedPath, content, true, false, getFlag, dbg);
|
|
403
|
+
}
|
|
271
404
|
if (inspectActionableReport) {
|
|
272
405
|
const actionReport = cacheManager.readCache("actionable-warnings", ctxCwd ?? runtime.projectRoot, 10 * 60_000);
|
|
273
406
|
if (!actionReport?.data) {
|
|
@@ -384,6 +517,19 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
384
517
|
}
|
|
385
518
|
}
|
|
386
519
|
}
|
|
520
|
+
logLatency({
|
|
521
|
+
type: "phase",
|
|
522
|
+
toolName: "agent_end",
|
|
523
|
+
filePath: ctxCwd ?? runtime.projectRoot,
|
|
524
|
+
phase: "agent_end_deferred_mutation_drain",
|
|
525
|
+
durationMs: Date.now() - startedAt,
|
|
526
|
+
metadata: {
|
|
527
|
+
autofixRecords: autofixRecords.length,
|
|
528
|
+
formatRecords: formatRecords.length,
|
|
529
|
+
coalescedPaths: records.length,
|
|
530
|
+
requeuedKinds: [...requeuedKinds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
531
|
+
},
|
|
532
|
+
});
|
|
387
533
|
logLatency({
|
|
388
534
|
type: "tool_result",
|
|
389
535
|
toolName: "agent_end",
|