@herbertgao/pi-extensions 2026.9.15 → 2026.9.16

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.
Files changed (195) hide show
  1. package/README.md +5 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  7. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
  8. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
  9. package/node_modules/pi-lens/CHANGELOG.md +498 -0
  10. package/node_modules/pi-lens/README.md +45 -2
  11. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
  12. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
  13. package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
  14. package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
  15. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  16. package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
  17. package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
  18. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
  19. package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
  20. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
  21. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
  22. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
  23. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
  24. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
  25. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
  26. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
  27. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
  28. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
  29. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
  30. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  31. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
  32. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  33. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
  34. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
  64. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
  65. package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
  66. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
  67. package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
  68. package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
  69. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
  70. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
  71. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
  72. package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
  73. package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
  74. package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
  75. package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
  76. package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
  77. package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
  78. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
  79. package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
  80. package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
  81. package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
  82. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
  83. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
  84. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
  85. package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
  86. package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
  87. package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
  88. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  89. package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
  90. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
  91. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
  92. package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
  93. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
  98. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
  99. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
  100. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
  101. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  102. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
  103. package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
  104. package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
  105. package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
  106. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
  107. package/node_modules/pi-lens/dist/index.js +5695 -3733
  108. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
  109. package/node_modules/pi-lens/dist/mcp/server.js +33 -21
  110. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  111. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
  112. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
  113. package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  114. package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  115. package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
  116. package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
  117. package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  118. package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  119. package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  120. package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
  121. package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
  122. package/node_modules/pi-lens/docs/adr/README.md +26 -0
  123. package/node_modules/pi-lens/docs/agent-tools.md +18 -1
  124. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
  125. package/node_modules/pi-lens/docs/configuration.md +55 -1
  126. package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
  127. package/node_modules/pi-lens/docs/environment-variables.md +61 -4
  128. package/node_modules/pi-lens/docs/features.md +2 -2
  129. package/node_modules/pi-lens/docs/globalconfig.md +6 -1
  130. package/node_modules/pi-lens/docs/language-coverage.md +17 -0
  131. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
  132. package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
  133. package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
  134. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
  135. package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
  136. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  137. package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
  138. package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
  139. package/node_modules/pi-lens/docs/settings.md +12 -3
  140. package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
  141. package/node_modules/pi-lens/package.json +5 -3
  142. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  143. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  144. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  145. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  146. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  147. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  148. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  149. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  150. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  151. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  152. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  153. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  154. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  155. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  156. package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
  157. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  158. package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
  159. package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
  160. package/node_modules/pi-mcp-adapter/README.md +12 -5
  161. package/node_modules/pi-mcp-adapter/config.ts +31 -8
  162. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
  163. package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
  164. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  165. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
  166. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
  167. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
  168. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
  169. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
  170. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
  171. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  172. package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
  173. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
  174. package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
  175. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
  176. package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
  177. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  178. package/node_modules/pi-mcp-adapter/index.ts +37 -20
  179. package/node_modules/pi-mcp-adapter/init.ts +14 -15
  180. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
  181. package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
  182. package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
  183. package/node_modules/pi-mcp-adapter/package.json +3 -2
  184. package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
  185. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
  186. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
  187. package/node_modules/pi-mcp-adapter/types.ts +2 -2
  188. package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
  189. package/node_modules/pi-mcp-adapter/utils.ts +11 -3
  190. package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
  191. package/node_modules/pi-next-cue/LICENSE +21 -0
  192. package/node_modules/pi-next-cue/README.md +81 -0
  193. package/node_modules/pi-next-cue/index.ts +358 -0
  194. package/node_modules/pi-next-cue/package.json +28 -0
  195. package/package.json +8 -5
@@ -30,7 +30,7 @@ import { getSharedTreeSitterClient } from "../tree-sitter-shared.js";
30
30
  import { symbolExtractionGrammar, TreeSitterSymbolExtractor, } from "../tree-sitter-symbol-extractor.js";
31
31
  import { withTreeSitterRoot } from "../tree-sitter-shared.js";
32
32
  import { incrementDegradationCount } from "../degradation-ledger.js";
33
- import { resolveGitIdentity } from "./git-identity.js";
33
+ import { createGitIdentityResolver, resolveGitIdentity, } from "./git-identity.js";
34
34
  import { formatReviewGraphRevisionDriftNote, } from "./revision-drift.js";
35
35
  import { buildSymbolId } from "./symbol-id.js";
36
36
  import { clearReviewGraphFileIr, getFreshReviewGraphFileIr, reviewGraphIrContentHash, } from "./shared-extraction-ir.js";
@@ -670,18 +670,18 @@ export function getReviewGraphCacheIdentity(cwd, graph) {
670
670
  // path, that assumption silently breaks. This doesn't hard-fail (the issue
671
671
  // is explicit: log-once observability is enough) — it just makes the
672
672
  // assumption visible. The Set records every cwd whose check has RUN (not just
673
- // mismatches), so resolveGitIdentity's fs reads happen once per cwd per
674
- // process — zero per-build cost after the first, mismatch or not.
673
+ // mismatches), so a build-local resolver keeps repeated checks in one build
674
+ // bounded without retaining repository presence across builds.
675
675
  const _cwdWorktreeCheckedCwds = new Set();
676
676
  export function _resetCwdWorktreeMismatchLogForTests() {
677
677
  _cwdWorktreeCheckedCwds.clear();
678
678
  }
679
- function logCwdWorktreeMismatchOnce(cwd) {
679
+ function logCwdWorktreeMismatchOnce(cwd, resolveIdentity = resolveGitIdentity) {
680
680
  const key = normalizeMapKey(cwd);
681
681
  if (_cwdWorktreeCheckedCwds.has(key))
682
682
  return;
683
683
  _cwdWorktreeCheckedCwds.add(key);
684
- const identity = resolveGitIdentity(cwd);
684
+ const identity = resolveIdentity(cwd);
685
685
  if (!identity)
686
686
  return; // not a git repo — nothing to compare against
687
687
  if (identity.worktreeRoot === normalizeFilePath(path.resolve(cwd)))
@@ -1345,7 +1345,7 @@ function logSnapshotReadVerdict(cwd, phase, reason) {
1345
1345
  return;
1346
1346
  logReviewGraph({ cwd, phase, reason });
1347
1347
  }
1348
- function loadPersistedGraph(cwd, opts) {
1348
+ function loadPersistedGraph(cwd, opts, resolveIdentity = resolveGitIdentity) {
1349
1349
  const cacheDir = path.join(getProjectDataDir(cwd), "cache");
1350
1350
  const cachePath = path.join(cacheDir, GRAPH_CACHE_FILENAME);
1351
1351
  const legacyPath = path.join(cacheDir, LEGACY_GRAPH_CACHE_FILENAME);
@@ -1382,7 +1382,7 @@ function loadPersistedGraph(cwd, opts) {
1382
1382
  // "can't verify," not a mismatch, so it does not drop the snapshot.
1383
1383
  let stampedHead;
1384
1384
  if (opts?.verifyWorktreeIdentity && data.gitStamp) {
1385
- const current = resolveGitIdentity(cwd);
1385
+ const current = resolveIdentity(cwd);
1386
1386
  if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
1387
1387
  logSnapshotReadVerdict(cwd, "snapshot_read_dropped", "worktree_mismatch");
1388
1388
  return null;
@@ -2198,11 +2198,11 @@ function persistGraph(cwd, signature, fileSignatures, fileHashes, graph, options
2198
2198
  const cacheDir = path.join(getProjectDataDir(cwd), "cache");
2199
2199
  const cachePath = path.join(cacheDir, GRAPH_CACHE_FILENAME);
2200
2200
  sweepStaleStageFiles(cacheDir);
2201
- // #300: resolve the git stamp fresh at persist time (HEAD changes on
2202
- // commit/checkout, so it isn't cached like the gitdir location — but these
2203
- // are plain fs reads, cheap even called per-persist). undefined for
2201
+ // #300: resolve the git stamp at persist time. The build-local resolver
2202
+ // deduplicates repeated checks within this build, while a later build gets a
2203
+ // fresh lifecycle view. undefined for
2204
2204
  // non-git cwds, which serializes as `gitStamp: undefined` → omitted key.
2205
- const gitStamp = resolveGitIdentity(cwd);
2205
+ const gitStamp = (options.resolveIdentity ?? resolveGitIdentity)(cwd);
2206
2206
  // Retain the immutable snapshot inputs and build their O(graph) serializable
2207
2207
  // arrays only after the quiet window. Replacing a pending entry during an edit
2208
2208
  // burst now avoids both serialization and the pre-serialization full copies.
@@ -2353,7 +2353,7 @@ function hashIgnoredIds(ignoredIds) {
2353
2353
  }
2354
2354
  /** Assemble the checkpoint DTO from the current PRE-resolution graph. Isolated
2355
2355
  * so both the offloaded and synchronous writers serialize identical bytes. */
2356
- function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds) {
2356
+ function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity = resolveGitIdentity) {
2357
2357
  return {
2358
2358
  version: REVIEW_GRAPH_VERSION,
2359
2359
  builtAt: new Date().toISOString(),
@@ -2363,7 +2363,7 @@ function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileC
2363
2363
  ignoredIdsHash: hashIgnoredIds(ignoredIds),
2364
2364
  nodes: Array.from(graph.nodes.entries()),
2365
2365
  edges: graph.edges,
2366
- gitStamp: resolveGitIdentity(cwd),
2366
+ gitStamp: resolveIdentity(cwd),
2367
2367
  };
2368
2368
  }
2369
2369
  /**
@@ -2375,12 +2375,12 @@ function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileC
2375
2375
  * resurrect one over a completed build; it falls back to a synchronous write
2376
2376
  * when the worker is unavailable. A lost checkpoint only costs a cold rebuild.
2377
2377
  */
2378
- function writeReviewGraphCheckpoint(cwd, graph, processedHashes, targetFileCount, ignoredIds) {
2378
+ function writeReviewGraphCheckpoint(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity = resolveGitIdentity) {
2379
2379
  const generation = (_checkpointGenerations.get(cwd) ?? 0) + 1;
2380
2380
  _checkpointGenerations.set(cwd, generation);
2381
2381
  let data;
2382
2382
  try {
2383
- data = buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds);
2383
+ data = buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity);
2384
2384
  }
2385
2385
  catch {
2386
2386
  return; // best-effort — building the DTO failed, skip this stride
@@ -2488,7 +2488,7 @@ function deleteReviewGraphCheckpoint(cwd) {
2488
2488
  * The returned graph carries `persistCoverage.inProgress` so it can never be
2489
2489
  * mistaken for a complete graph if it escapes the resume path.
2490
2490
  */
2491
- function loadReviewGraphCheckpoint(cwd) {
2491
+ function loadReviewGraphCheckpoint(cwd, resolveIdentity = resolveGitIdentity) {
2492
2492
  const checkpointPath = reviewGraphCheckpointPath(cwd);
2493
2493
  let data;
2494
2494
  try {
@@ -2515,7 +2515,7 @@ function loadReviewGraphCheckpoint(cwd) {
2515
2515
  return null;
2516
2516
  }
2517
2517
  if (data.gitStamp) {
2518
- const current = resolveGitIdentity(cwd);
2518
+ const current = resolveIdentity(cwd);
2519
2519
  if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
2520
2520
  logReviewGraph({
2521
2521
  cwd,
@@ -2598,8 +2598,8 @@ function pruneOrphanNonFileNodes(graph) {
2598
2598
  * - The ignored-id fingerprint must match (import-edge resolution depends on
2599
2599
  * it), else fail open.
2600
2600
  */
2601
- async function tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds) {
2602
- const loaded = loadReviewGraphCheckpoint(cwd);
2601
+ async function tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds, resolveIdentity = resolveGitIdentity) {
2602
+ const loaded = loadReviewGraphCheckpoint(cwd, resolveIdentity);
2603
2603
  if (!loaded)
2604
2604
  return null;
2605
2605
  if (loaded.ignoredIdsHash !== hashIgnoredIds(ignoredIds)) {
@@ -4153,6 +4153,7 @@ async function tryIncrementalFromCache(cached, ctx) {
4153
4153
  projectSeq: ctx.seqAtBuildStart,
4154
4154
  seqHint: ctx.seqHint,
4155
4155
  mode: ctx.mode,
4156
+ resolveIdentity: ctx.resolveIdentity,
4156
4157
  });
4157
4158
  setGraphBuildInfo(graph, {
4158
4159
  reused: true,
@@ -4179,7 +4180,7 @@ function hasGraphKindExtension(file) {
4179
4180
  * `_lastGraphBuildInfo` (the caller's full sweep sets the mode and stamps the
4180
4181
  * fallback reason). Correctness bar is HIGH: any doubt ⇒ fall back.
4181
4182
  */
4182
- async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, ignoredIds, cacheEpoch) {
4183
+ async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, ignoredIds, cacheEpoch, resolveIdentity) {
4183
4184
  const cached = _workspaceGraphCache.get(normalizedCwd);
4184
4185
  // Condition 2: need an in-process entry that recorded a build seq and whose
4185
4186
  // SOURCE WALK was complete. An entry-budget-truncated or checkpoint graph has
@@ -4326,6 +4327,7 @@ async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, fa
4326
4327
  projectSeq: seqAtBuildStart,
4327
4328
  seqHint: true,
4328
4329
  mode: "seq-fastpath",
4330
+ resolveIdentity,
4329
4331
  });
4330
4332
  // #459: filesToUpdate was non-empty — this fastpath re-extracted real files,
4331
4333
  // so (unlike the no-op branch above) the graph object did change.
@@ -4343,6 +4345,7 @@ async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, fa
4343
4345
  return { graph };
4344
4346
  }
4345
4347
  async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4348
+ const resolveIdentity = createGitIdentityResolver();
4346
4349
  const normalizedCwd = normalizeMapKey(cwd);
4347
4350
  const cacheEpoch = workspaceCacheEpoch(normalizedCwd);
4348
4351
  // `await undefined` still yields a microtask, which reorders overlapping
@@ -4352,7 +4355,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4352
4355
  await _reviewGraphBuildGateForTests();
4353
4356
  const normalizedChanged = changedFiles.map((file) => normalizeMapKey(file));
4354
4357
  const normalizedChangedSet = new Set(normalizedChanged);
4355
- logCwdWorktreeMismatchOnce(cwd);
4358
+ logCwdWorktreeMismatchOnce(cwd, resolveIdentity);
4356
4359
  // #622: reject a cwd that IS (or is an ancestor of) $HOME before any walk is
4357
4360
  // attempted. The 3 real per-edit callers (dispatch/integration.ts's
4358
4361
  // computeCascadeForFile, mcp/analyze.ts, tree-sitter.ts's
@@ -4405,7 +4408,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4405
4408
  // cascade.log can watch the fast-path hit/miss rate.
4406
4409
  let seqFastpathFallback;
4407
4410
  if (seqHint && seqAtBuildStart !== undefined && seqFastpathEnabled()) {
4408
- const fast = await trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, await ignoredIdsPromise, cacheEpoch);
4411
+ const fast = await trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, await ignoredIdsPromise, cacheEpoch, resolveIdentity);
4409
4412
  if ("graph" in fast)
4410
4413
  return fast.graph;
4411
4414
  seqFastpathFallback = fast.fallback;
@@ -4550,6 +4553,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4550
4553
  seqAtBuildStart,
4551
4554
  ignoredIds,
4552
4555
  cacheEpoch,
4556
+ resolveIdentity,
4553
4557
  });
4554
4558
  if (incremental) {
4555
4559
  updateGraphBuildInfo(incremental, { seqFastpathFallback });
@@ -4561,7 +4565,9 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4561
4565
  // #202 content-hash confirm below already content-verify the load, and
4562
4566
  // dropping on every HEAD move would force a full whole-repo rebuild after
4563
4567
  // each plain `git commit` (HEAD moves, files unchanged).
4564
- const diskCached = sourceFilesTruncated ? null : loadPersistedGraph(cwd);
4568
+ const diskCached = sourceFilesTruncated
4569
+ ? null
4570
+ : loadPersistedGraph(cwd, undefined, resolveIdentity);
4565
4571
  if (diskCached?.signature === signature) {
4566
4572
  const graph = cloneGraph(diskCached.graph);
4567
4573
  rebuildIndexes(graph);
@@ -4613,6 +4619,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4613
4619
  facts,
4614
4620
  seqAtBuildStart,
4615
4621
  ignoredIds,
4622
+ resolveIdentity,
4616
4623
  });
4617
4624
  if (incremental) {
4618
4625
  updateGraphBuildInfo(incremental, { seqFastpathFallback });
@@ -4623,7 +4630,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4623
4630
  // present and still current (#936 limit 2), else cold from an empty graph.
4624
4631
  const resumed = sourceFilesTruncated
4625
4632
  ? null
4626
- : await tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds);
4633
+ : await tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds, resolveIdentity);
4627
4634
  const graph = resumed?.graph ?? createEmptyGraph();
4628
4635
  const filesToExtract = resumed?.remaining ?? filesToBuild;
4629
4636
  const treeSitterClient = getSharedTreeSitterClient();
@@ -4674,7 +4681,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4674
4681
  if (Number.isFinite(testStopAfter) &&
4675
4682
  testStopAfter > 0 &&
4676
4683
  extractedCount >= testStopAfter) {
4677
- writeReviewGraphCheckpointSync(cwd, buildReviewGraphCheckpointData(cwd, graph, fileHashes, filesToBuild.length, ignoredIds), {
4684
+ writeReviewGraphCheckpointSync(cwd, buildReviewGraphCheckpointData(cwd, graph, fileHashes, filesToBuild.length, ignoredIds, resolveIdentity), {
4678
4685
  nodes: graph.nodes.size,
4679
4686
  edges: graph.edges.length,
4680
4687
  processed: fileHashes.size,
@@ -4686,7 +4693,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4686
4693
  if (filesSinceCheckpoint >= checkpointEvery &&
4687
4694
  remainingAfter >= checkpointEvery &&
4688
4695
  Date.now() - lastCheckpointMs >= checkpointMinIntervalMs) {
4689
- writeReviewGraphCheckpoint(cwd, graph, fileHashes, filesToBuild.length, ignoredIds);
4696
+ writeReviewGraphCheckpoint(cwd, graph, fileHashes, filesToBuild.length, ignoredIds, resolveIdentity);
4690
4697
  filesSinceCheckpoint = 0;
4691
4698
  lastCheckpointMs = Date.now();
4692
4699
  }
@@ -4776,6 +4783,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
4776
4783
  mode: "full",
4777
4784
  sourceFileCount,
4778
4785
  sourceFilesTruncated,
4786
+ resolveIdentity,
4779
4787
  }); // fire-and-forget
4780
4788
  setGraphBuildInfo(graph, {
4781
4789
  reused: false,
@@ -14,14 +14,24 @@
14
14
  */
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
- import { BoundedLruCache } from "../bounded-cache.js";
18
17
  import { normalizeFilePath, walkUpDirs } from "../path-utils.js";
19
- // Per-process cache of resolved gitdir location keyed by cwd — the gitdir
20
- // location itself is stable for the life of the process; only HEAD's
21
- // CONTENT changes (on commit/checkout), so that's re-read fresh every call.
22
- const _gitDirCache = new BoundedLruCache(32);
18
+ /** A build-local memo: it is discarded when the build settles. */
19
+ export function createGitIdentityResolver() {
20
+ const memo = new Map();
21
+ return (cwd) => {
22
+ if (memo.has(cwd))
23
+ return memo.get(cwd);
24
+ const identity = resolveGitIdentity(cwd);
25
+ memo.set(cwd, identity);
26
+ return identity;
27
+ };
28
+ }
29
+ // Kept as a compatibility no-op for tests that reset the former memo. Git
30
+ // repository creation/removal is a process-lifecycle change, so the lookup
31
+ // below must walk the filesystem on every call rather than cache presence or
32
+ // absence by cwd.
23
33
  export function _resetGitIdentityCacheForTests() {
24
- _gitDirCache.clear();
34
+ return;
25
35
  }
26
36
  function readCommonDir(gitDir) {
27
37
  try {
@@ -84,9 +94,6 @@ function resolveGitDir(cwd) {
84
94
  return null;
85
95
  }
86
96
  function getResolvedGitDir(cwd) {
87
- const key = normalizeFilePath(path.resolve(cwd));
88
- if (_gitDirCache.has(key))
89
- return _gitDirCache.get(key) ?? null;
90
97
  let resolved;
91
98
  try {
92
99
  resolved = resolveGitDir(cwd);
@@ -94,7 +101,6 @@ function getResolvedGitDir(cwd) {
94
101
  catch {
95
102
  resolved = null;
96
103
  }
97
- _gitDirCache.set(key, resolved);
98
104
  return resolved;
99
105
  }
100
106
  function resolveRefToSha(commonDir, ref) {
@@ -12,6 +12,7 @@ import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
14
14
  import { isFileKind } from "./file-kinds.js";
15
+ import { pathsEqual } from "./path-utils.js";
15
16
  import { safeSpawnAsync } from "./safe-spawn.js";
16
17
  import { resolveToolCwd } from "./tool-cwd.js";
17
18
  import { ruffConfigArgs } from "./tool-policy.js";
@@ -95,7 +96,7 @@ export class RuffClient {
95
96
  absolutePath,
96
97
  ], { timeout: 10000, cwd: ruffCwd });
97
98
  const beforeDiags = pre.stdout?.trim()
98
- ? this.parseOutput(pre.stdout, absolutePath)
99
+ ? this.parseOutput(pre.stdout, ruffCwd, absolutePath)
99
100
  : [];
100
101
  const fixableCount = beforeDiags.filter((d) => d.fixable).length;
101
102
  const fix = await safeSpawnAsync(this.ruffCommand, ["check", "--fix", ...configArgs, absolutePath], { timeout: 15000, cwd: ruffCwd });
@@ -119,15 +120,41 @@ export class RuffClient {
119
120
  }
120
121
  }
121
122
  // --- Internal ---
122
- parseOutput(output, filterFile) {
123
+ /**
124
+ * `cwd` is the directory ruff RAN in, and it is REQUIRED: a default would
125
+ * hide the wrong base at a call site that forgot it (#3284's D11).
126
+ *
127
+ * ruff's JSON `filename` is the `SourceFile`'s name verbatim — the JSON
128
+ * renderer reads `span.file().path(resolver)` (ruff 0.16.8
129
+ * `crates/ruff_db/src/diagnostic/render/json.rs:58,120`), which for a ruff
130
+ * (not `ty`) diagnostic is `file.name()` (`crates/ruff_db/src/diagnostic/
131
+ * mod.rs:1187-1193`), NOT the cwd-relative `relative_path` the text
132
+ * renderers use (`mod.rs:1195-1204`). That name is
133
+ * `SourceFileBuilder::new(path.to_string_lossy(), …)`
134
+ * (`crates/ruff_linter/src/linter.rs:1049`) over a path the resolver has
135
+ * already absolutized ("Normalize every path (e.g., convert from relative to
136
+ * absolute)", `crates/ruff_workspace/src/resolver.rs:484`). So ruff echoes
137
+ * the absolute path we hand it as argv: the BASE was already right here and
138
+ * the fold deletes a `path.resolve` with no base — which silently meant the
139
+ * EXTENSION's `process.cwd()`, not ruff's — rather than leaving it for a
140
+ * future relative invocation to re-enable.
141
+ */
142
+ parseOutput(output, cwd, filterFile) {
123
143
  if (!output.trim())
124
144
  return [];
125
145
  try {
126
146
  const items = JSON.parse(output);
127
147
  const diagnostics = [];
128
148
  for (const item of items) {
129
- // Filter to single file if requested
130
- if (filterFile && path.resolve(item.filename) !== filterFile)
149
+ // Filter to single file if requested. #3278/#3286: one seam answers
150
+ // "is this reported diagnostic about the file I ran for?" — resolve
151
+ // the tool's spelling against the cwd the tool RAN in and compare
152
+ // through `pathsEqual`, the repo's on-disk identity predicate. A bare
153
+ // `!==` treats a spelling that differs only in case (the SAME file on
154
+ // Windows and on a case-folding POSIX mount) as a different file and
155
+ // drops every finding for the edited file (#209, #3277).
156
+ if (filterFile &&
157
+ !pathsEqual(path.resolve(cwd, item.filename), filterFile))
131
158
  continue;
132
159
  diagnostics.push({
133
160
  line: item.location.row - 1, // ruff is 1-indexed
@@ -184,12 +184,45 @@ export class RuntimeCoordinator {
184
184
  this._cascadeSessionStats.diagnosticsSurfaced += diagnosticsSurfaced;
185
185
  this._cascadeSessionStats.coldSnapshotTouches += coldSnapshotTouches;
186
186
  }
187
+ /**
188
+ * Record the turn-end policy verdict on every live inline-blocker record
189
+ * (#3248): `true` for a file whose every blocker the policy suppressed,
190
+ * `false` for every other record in the map.
191
+ *
192
+ * ONE writer, rewriting the WHOLE axis each turn end, so the verdict can
193
+ * never outlive the pass that made it: a record demoted since, or one whose
194
+ * survivors came back because the file's bytes moved under a content-bound
195
+ * anchor, is cleared by the same call that sets its sibling. A fresh
196
+ * dispatch replaces the record wholesale (`recordInlineBlockers`), so a NEW
197
+ * blocker is never born pre-suppressed (#1198 ordering).
198
+ *
199
+ * @returns how many records this call CHANGED — 0 means the gate's view of
200
+ * the map is already what the policy just decided.
201
+ */
202
+ applyInlineBlockerPolicyVerdicts(suppressedFilePaths) {
203
+ const suppressed = new Set(suppressedFilePaths.map((filePath) => path.resolve(filePath)));
204
+ let changed = 0;
205
+ for (const [key, entry] of this._pendingInlineBlockers.entries()) {
206
+ const policySuppressed = suppressed.has(path.resolve(entry.filePath));
207
+ if (!!entry.policySuppressed === policySuppressed)
208
+ continue;
209
+ this._pendingInlineBlockers.set(key, { ...entry, policySuppressed });
210
+ changed += 1;
211
+ }
212
+ return changed;
213
+ }
187
214
  updateGitGuardStatus(hasBlockers, output) {
188
215
  // The status is an aggregate over the current per-file map. A clean B
189
216
  // result must not erase an unresolved A result; the pipeline records/clears
190
217
  // the edited file immediately before this method runs.
191
- this._gitGuardHasBlockers =
192
- hasBlockers || this.getInlineBlockersSnapshot().length > 0;
218
+ // #3248: every live record except one whose whole blocker set the
219
+ // turn-end disposition policy suppressed. The record itself stays in the
220
+ // map — the policy is content-bound and re-derived from the record's
221
+ // diagnostics at every turn end, so dropping it there would be silencing
222
+ // rather than filtering (AGENTS.md shape 10); it just stops counting for
223
+ // the gate the agent was shown nothing for.
224
+ const blocking = this.getInlineBlockersSnapshot().filter((entry) => !entry.policySuppressed);
225
+ this._gitGuardHasBlockers = hasBlockers || blocking.length > 0;
193
226
  if (!this._gitGuardHasBlockers) {
194
227
  this._gitGuardSummary = "";
195
228
  return;
@@ -198,7 +231,7 @@ export class RuntimeCoordinator {
198
231
  .split("\n")
199
232
  .map((line) => line.trim())
200
233
  .find((line) => line.length > 0);
201
- const summaries = this.getInlineBlockersSnapshot()
234
+ const summaries = blocking
202
235
  .map((entry) => entry.summary.trim())
203
236
  .filter(Boolean);
204
237
  this._gitGuardSummary = (summaries[0] ??
@@ -665,7 +698,7 @@ export class RuntimeCoordinator {
665
698
  // that actually settles it delivers it.
666
699
  return this._cascadeRuns.length > 0;
667
700
  }
668
- recordInlineBlockers(filePath, summary, writeIndex, sources, lines, contentBaseline) {
701
+ recordInlineBlockers(filePath, summary, writeIndex, sources, lines, contentBaseline, diagnostics) {
669
702
  const recordedAtMs = Date.now();
670
703
  this._pendingInlineBlockers.set(path.resolve(filePath), {
671
704
  filePath,
@@ -673,6 +706,7 @@ export class RuntimeCoordinator {
673
706
  writeIndex,
674
707
  sources,
675
708
  lines,
709
+ diagnostics,
676
710
  recordedAtMs,
677
711
  stale: false,
678
712
  ...(contentBaseline
@@ -8,7 +8,7 @@ import { evaluateGitGuard, isGitCommitOrPushAttempt } from "./git-guard.js";
8
8
  import { dropHashlineAnchorMemo } from "./hashline-anchor.js";
9
9
  import { evaluateSharedCheckoutGuard } from "./shared-checkout-guard.js";
10
10
  import { logLatency } from "./latency-logger.js";
11
- import { normalizeMapKey } from "./path-utils.js";
11
+ import { normalizeMapKey, toPosix } from "./path-utils.js";
12
12
  import { captureFileStats, getOpaqueBaselineStore, isGitWorktree, } from "./opaque-mutation-scan.js";
13
13
  import { normalizeForGuardMatch } from "./host-edit-normalize.js";
14
14
  import { retargetReplacementIndentation } from "./indent-retarget.js";
@@ -133,7 +133,12 @@ function isLspCapableFile(filePath) {
133
133
  return LANGUAGE_POLICY[kind]?.lspCapable !== false;
134
134
  }
135
135
  function shouldSkipLspAutoTouch(filePath, projectRoot) {
136
- const normalized = path.resolve(filePath).replace(/\\/g, "/").toLowerCase();
136
+ // #1193 P3: the separator fold is `toPosix`, not a fourth inline copy of
137
+ // `.replace(/\\/g, "/")`. The `toLowerCase` stays and is NOT a path-key case
138
+ // fold: `normalized` is never a map key, only the haystack for the
139
+ // lowercase marker substrings below, which must match a mis-cased spelling
140
+ // of the same marker directory on a case-insensitive filesystem.
141
+ const normalized = toPosix(path.resolve(filePath)).toLowerCase();
137
142
  const base = path.basename(filePath).toLowerCase();
138
143
  if (normalized.includes("/.pi-lens/"))
139
144
  return true;
@@ -792,9 +797,15 @@ async function handleToolCallImpl(deps) {
792
797
  // budgeted families rather than leaving this site to spell its
793
798
  // own axis value (#2557 review F7).
794
799
  hook: "tool_call",
795
- // The ambient slot is populated by tool_result, after this hook has
796
- // already run. Use the live tool_call signal so Escape can release
797
- // this await (#2523 AC4).
800
+ // The ambient slot is populated by tool_result, AFTER this hook has
801
+ // already run, so the live `tool_call` signal is the only one that
802
+ // can release this await when the user presses Escape (#2523 AC4).
803
+ // #2939 round 2 restored this after round 2's own measurement: with
804
+ // the signal absent, an aborted caller waits the demand's whole
805
+ // `BOOTSTRAP_LOAD_TIMEOUT_MS` out and the cancel then surfaces on the
806
+ // ledger as a `timeout` degradation — the exact inversion
807
+ // `requestBootstrapClients`'s `unavailableReason !== "aborted"` guard
808
+ // exists to prevent.
798
809
  signal: deps.ctx.signal,
799
810
  }))?.complexityClient;
800
811
  const baseline = await complexityClient?.analyzeFile(filePath);
@@ -484,7 +484,7 @@ function refreshCachedExports(runtime, filePath) {
484
484
  * pipeline AND record" block could not express.
485
485
  */
486
486
  async function dispatchPipelineAnalysis(args) {
487
- const { deps, runtime, filePath, dispatchCwd, turnStateCwd, autofixMode, modifiedRanges, writeIndex, initialStateHash, readGuardCorrelationId, requestedEditIndexes, requestedEditTotal, isPartialApplyResult, toolResultStart, nativeAppliedPairs, allowReadGuardWrites, } = args;
487
+ const { deps, runtime, filePath, dispatchCwd, turnStateCwd, autofixMode, modifiedRanges, writeIndex, initialStateHash, readGuardCorrelationId, requestedEditIndexes, requestedEditTotal, isPartialApplyResult, toolResultStart, nativeAppliedPairs, allowAutonomousWriters, } = args;
488
488
  const { event, getFlag, getFlagSource, dbg, biomeClient, ruffClient, metricsClient, resetLSPService, } = deps;
489
489
  const pipelinePromise = runPipeline({
490
490
  signal: deps.signal,
@@ -493,6 +493,7 @@ async function dispatchPipelineAnalysis(args) {
493
493
  projectRoot: turnStateCwd,
494
494
  toolName: event.toolName,
495
495
  autofixMode,
496
+ allowAutonomousWriters,
496
497
  modifiedRanges,
497
498
  telemetry: {
498
499
  model: runtime.telemetryModel,
@@ -693,7 +694,7 @@ async function dispatchPipelineAnalysis(args) {
693
694
  // The model's write/edit and pi-lens' own immediate format/autofix are now
694
695
  // reflected on disk. Refresh read-guard staleness stamps so a follow-up edit
695
696
  // is judged by read-range coverage, not by our own previous write.
696
- if (!getFlag("no-read-guard") && allowReadGuardWrites) {
697
+ if (!getFlag("no-read-guard") && allowAutonomousWriters) {
697
698
  const changedForReadGuard = new Set([
698
699
  path.resolve(filePath),
699
700
  ...(result.changedFiles ?? []).map((changedFile) => path.resolve(changedFile)),
@@ -710,7 +711,9 @@ export async function handleToolResult(deps) {
710
711
  const { event, getFlag, dbg, runtime, cacheManager, agentBehaviorRecord, formatBehaviorWarnings, } = deps;
711
712
  const rawFilePath = event.input.path;
712
713
  const workspaceRoot = runtime.projectRoot || process.cwd();
713
- let bashAuthorshipConfirmed = deps._readGuardAuthorship ?? event.toolName !== "bash";
714
+ let bashAuthorshipConfirmed = deps._allowAutonomousWriters ??
715
+ deps._readGuardAuthorship ??
716
+ event.toolName !== "bash";
714
717
  // #1642: a gitignored worktree edit got re-attributed onto a
715
718
  // same-relative-path file in the parent checkout because this handler
716
719
  // used to always resolve a relative path against `workspaceRoot`
@@ -975,11 +978,9 @@ export async function handleToolResult(deps) {
975
978
  const opaqueSet = new Set(opaquePaths);
976
979
  const written = [...recognizedWritten, ...opaquePaths];
977
980
  const recognizedAuthoredSet = new Set(recognizedAuthored);
978
- bashAuthorshipConfirmed =
979
- recognizedAuthored.length > 0 || opaquePaths.length > 0;
981
+ bashAuthorshipConfirmed = recognizedAuthored.length > 0;
980
982
  for (const wp of written) {
981
- if (!getFlag("no-read-guard") &&
982
- (opaqueSet.has(wp) || recognizedAuthoredSet.has(wp)))
983
+ if (!getFlag("no-read-guard") && recognizedAuthoredSet.has(wp))
983
984
  deps.readGuard?.recordWritten(wp);
984
985
  else if (!getFlag("no-read-guard") && recognizedWritten.includes(wp))
985
986
  deps.readGuard?.recordUnchanged?.(wp);
@@ -1013,7 +1014,11 @@ export async function handleToolResult(deps) {
1013
1014
  _autofixMode: autofixMode,
1014
1015
  _attachmentBudget: syntheticAttachmentBudget,
1015
1016
  _mutationSourceOverride: isOpaque ? "opaque-script" : undefined,
1016
- _readGuardAuthorship: opaqueSet.has(wp) || recognizedAuthoredSet.has(wp),
1017
+ _readGuardAuthorship: recognizedAuthoredSet.has(wp),
1018
+ // Opaque recovery is mutation evidence only. The synthetic call still
1019
+ // records freshness and runs diagnostics, but it cannot format/autofix
1020
+ // or issue an edit-directed blocker/actionable instruction.
1021
+ _allowAutonomousWriters: recognizedAuthoredSet.has(wp),
1017
1022
  });
1018
1023
  if (syntheticResult) {
1019
1024
  // #1590: forward verbatim. The synthetic call already charged the
@@ -1476,7 +1481,7 @@ export async function handleToolResult(deps) {
1476
1481
  // bridge, so the dispatch's read-guard write refresh
1477
1482
  // carries evidence and stays enabled on every
1478
1483
  // per-path dispatch.
1479
- allowReadGuardWrites: true,
1484
+ allowAutonomousWriters: true,
1480
1485
  }), {
1481
1486
  ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
1482
1487
  signal: observedDispatchSignal,
@@ -1787,7 +1792,7 @@ export async function handleToolResult(deps) {
1787
1792
  participantTotal,
1788
1793
  toolResultStart,
1789
1794
  nativeAppliedPairs,
1790
- allowReadGuardWrites: bashAuthorshipConfirmed,
1795
+ allowAutonomousWriters: bashAuthorshipConfirmed,
1791
1796
  }), {
1792
1797
  ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
1793
1798
  signal: deps.signal || undefined,
@@ -1808,6 +1813,7 @@ export async function handleToolResult(deps) {
1808
1813
  const result = dispatchOutcome.result;
1809
1814
  let autofixNewlyQueued = false;
1810
1815
  if (!result.isError &&
1816
+ bashAuthorshipConfirmed &&
1811
1817
  autofixMode === "deferred" &&
1812
1818
  nodeFs.existsSync(filePath)) {
1813
1819
  autofixNewlyQueued =
@@ -1816,6 +1822,7 @@ export async function handleToolResult(deps) {
1816
1822
  }
1817
1823
  let formatQueued = false;
1818
1824
  if (!result.isError &&
1825
+ bashAuthorshipConfirmed &&
1819
1826
  !getFlag("no-autoformat", filePath) &&
1820
1827
  (autofixMode === "deferred" || !getFlag("immediate-format")) &&
1821
1828
  nodeFs.existsSync(filePath)) {
@@ -1869,7 +1876,11 @@ export async function handleToolResult(deps) {
1869
1876
  source: "autofix",
1870
1877
  dbg,
1871
1878
  });
1872
- if (resolvedChanged === path.resolve(filePath))
1879
+ // Workspace-edit paths come from fileURLToPath and are normally absolute,
1880
+ // so the turn-state cwd is inert for those values. Keep it in the resolve
1881
+ // call because the producer's contract is cwd-relative URI resolution, and
1882
+ // let pathsEqual ask the filesystem-aware path identity question (#3294).
1883
+ if (pathsEqual(path.resolve(turnStateCwd, changedFile), path.resolve(filePath)))
1873
1884
  continue;
1874
1885
  try {
1875
1886
  const content = nodeFs.readFileSync(resolvedChanged, "utf-8");
@@ -1929,7 +1940,11 @@ export async function handleToolResult(deps) {
1929
1940
  // #1561: stamp the verdict with THIS dispatch's write token — the same
1930
1941
  // counter `lsp_diagnostics`' reconciliation seam draws from — so a later
1931
1942
  // confirmed-clean result can be ordered against it instead of racing it.
1932
- runtime.recordInlineBlockers(filePath, result.inlineBlockerSummary, writeIndex, result.inlineBlockerSources, result.inlineBlockerLines, result.inlineBlockerFileContent);
1943
+ runtime.recordInlineBlockers(filePath, result.inlineBlockerSummary, writeIndex, result.inlineBlockerSources, result.inlineBlockerLines, result.inlineBlockerFileContent,
1944
+ // #3246: the structured blockers the summary was rendered from, so a
1945
+ // later `lens_diagnostic_mark` can be applied to this record at turn
1946
+ // end instead of replaying pre-mark text.
1947
+ result.inlineBlockerDiagnostics);
1933
1948
  }
1934
1949
  else {
1935
1950
  runtime.clearInlineBlockers(filePath);