@herbertgao/pi-extensions 2026.9.6 → 2026.9.7

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 (240) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  19. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  22. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  23. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  24. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  25. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  26. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  27. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  28. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  29. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  30. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  31. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  32. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  33. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  34. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  35. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  36. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  44. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  45. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  46. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  47. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  48. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  49. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  50. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  51. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  52. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  53. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  54. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  55. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  56. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  57. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  58. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  59. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  61. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  62. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  63. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  64. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  66. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  67. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  68. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  69. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  70. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  72. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  73. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  74. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  75. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  76. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  77. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  78. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  79. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  82. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  83. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  84. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  86. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  87. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  88. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  89. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  91. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  92. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  93. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  98. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  99. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  100. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  101. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  102. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  103. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  104. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  105. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  106. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  107. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  108. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  109. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  110. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  111. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  112. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  113. package/node_modules/pi-lens/dist/index.js +27416 -24153
  114. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  115. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  116. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  117. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  118. package/node_modules/pi-lens/docs/audit1.md +5 -5
  119. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  120. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  121. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  122. package/node_modules/pi-lens/docs/features.md +4 -4
  123. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  124. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  125. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  126. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  127. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  128. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  129. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  130. package/node_modules/pi-lens/docs/settings.md +7 -4
  131. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  132. package/node_modules/pi-lens/docs/usage.md +2 -0
  133. package/node_modules/pi-lens/package.json +1 -1
  134. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  135. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  136. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  137. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  138. package/package.json +5 -11
  139. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  140. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  141. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  142. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  143. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  144. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  145. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  146. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  147. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  148. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  149. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  150. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  151. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  152. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  153. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  154. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  155. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  156. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  157. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  158. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  159. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  160. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  161. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  162. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  163. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  164. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  165. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  166. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  167. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  168. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  169. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  170. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  171. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  172. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  173. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  174. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  175. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  176. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  177. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  178. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  179. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  180. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  181. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  182. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  183. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  184. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  185. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  186. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  187. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  188. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  189. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  190. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  191. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  192. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  193. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  194. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  195. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  196. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  197. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  198. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  199. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  200. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  201. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  202. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  203. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  204. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  205. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  206. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  207. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  208. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  209. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  210. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  211. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  212. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  213. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  214. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  215. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  216. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  217. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  218. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  219. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  220. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  221. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  222. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  223. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  224. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  225. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  226. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  227. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  228. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  229. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  230. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  231. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  232. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  233. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  234. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  235. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  236. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  237. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  238. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  239. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  240. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -2,8 +2,9 @@ import * as nodeCrypto from "node:crypto";
2
2
  import * as nodeFs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { noteAuthoritativeContentAttachment } from "./agent-nudge.js";
5
- import { captureFileStats, diffFileStats, getOpaqueBaselineStore, recoverOpaqueChangesViaGit, } from "./opaque-mutation-scan.js";
5
+ import { captureFileStats, diffFileContent, getOpaqueBaselineStore, recoverOpaqueChangesViaGit, } from "./opaque-mutation-scan.js";
6
6
  import { normalizeMapKey } from "./path-utils.js";
7
+ import { detectFileKind } from "./file-kinds.js";
7
8
  import { extractReadPathsFromCommand, extractDeletedPathsFromCommand, extractGrepSearchReadsFromOutput, extractWrittenPathsFromCommand, tokenizeShellCommand, } from "./bash-file-access.js";
8
9
  import { registerSearchReads, } from "./search-read-registration.js";
9
10
  import { createFileTime } from "./file-time.js";
@@ -21,6 +22,7 @@ import { replayThroughMutationBridge, storedLineHashesFor, } from "./observed-mu
21
22
  import { getAmbientAbortSignal } from "./safe-spawn.js";
22
23
  import { resolveToolCallCorrelationId } from "./tool-event.js";
23
24
  import { boundedIndexesForCount, createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
25
+ import { countFileLines } from "./read-guard-tool-lines.js";
24
26
  import { notifyExternalFileChange, resyncGitChangedFiles, } from "./lsp/index.js";
25
27
  import { runPipeline } from "./pipeline.js";
26
28
  import { renderPostAutofixNotice, } from "./post-autofix-notice.js";
@@ -28,7 +30,14 @@ import { syncGitGuardRecord } from "./git-guard.js";
28
30
  import { scheduleWordIndexPersist } from "./word-index.js";
29
31
  import { RUNTIME_CONFIG } from "./runtime-config.js";
30
32
  import { getActiveSessionId } from "./session-lifecycle.js";
33
+ import { requestBootstrapClients } from "./bootstrap.js";
34
+ import { bounded } from "./deadline-utils.js";
35
+ import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
36
+ import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
31
37
  const AUTHORITATIVE_CONTENT_MAX_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
38
+ // Keep same-turn analysis below the observed-directory capture bound so one
39
+ // opaque codemod cannot monopolize the hook or analyzer fleet.
40
+ const OBSERVED_DISPATCH_MAX_PATHS = 32;
32
41
  /**
33
42
  * Git subcommands that import ANOTHER commit's content into the index. The
34
43
  * whole family, with a verdict for each (#2060):
@@ -93,6 +102,29 @@ export function isFailedGitIntegrationCommand(command, isError) {
93
102
  return false;
94
103
  });
95
104
  }
105
+ function ensureToolResultClients(deps) {
106
+ if (deps.biomeClient && deps.ruffClient && deps.metricsClient)
107
+ return true;
108
+ const request = {
109
+ reason: "tool-result-analysis",
110
+ hook: "tool_result_edit",
111
+ timeoutMs: HOOK_WALL_BUDGET_MS.tool_result_edit,
112
+ ...(deps.signal === undefined ? {} : { signal: deps.signal }),
113
+ };
114
+ return bounded(requestBootstrapClients(request), {
115
+ ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
116
+ signal: deps.signal,
117
+ hook: "tool_result_edit",
118
+ label: "tool-result-bootstrap-demand",
119
+ }).then((clients) => {
120
+ if (!clients)
121
+ return false;
122
+ deps.biomeClient = clients.biomeClient;
123
+ deps.ruffClient = clients.ruffClient;
124
+ deps.metricsClient = clients.metricsClient;
125
+ return true;
126
+ });
127
+ }
96
128
  function parseDiffRanges(diff) {
97
129
  const changedLines = [];
98
130
  for (const line of diff.split("\n")) {
@@ -144,13 +176,11 @@ export function clearLastAnalyzedStateCache() {
144
176
  * with one owner (#2464 review round 3, F1) rather than two open-coded blocks
145
177
  * ~80 lines apart whose invariants only line up if a reader checks both.
146
178
  *
147
- * Exported for the isolation red in
148
- * `tests/clients/observed-mutation-integration.test.ts`: with
149
- * `claimPipelineDispatch` below now consulted by BOTH dispatch call sites, the
150
- * identity guard in the release has no reachable production trigger left, so
151
- * the only honest way to prove it is to drive the seam directly.
179
+ * Kept private because both dispatch call sites now use the shared claim before
180
+ * registration; publishing this seam without the claim obligation would make
181
+ * the identity guard's precondition easy to violate again.
152
182
  */
153
- export function registerInFlightPipeline(filePath, stateHash, pipeline) {
183
+ function registerInFlightPipeline(filePath, stateHash, pipeline) {
154
184
  let filePipelines = inFlightPipelines.get(filePath);
155
185
  if (!filePipelines) {
156
186
  filePipelines = new Map();
@@ -172,7 +202,7 @@ export function registerInFlightPipeline(filePath, stateHash, pipeline) {
172
202
  * deduping and its `participantIds`/`participantTotal` under-count. Delete only
173
203
  * when the outer map still points at the very map this registration went into.
174
204
  */
175
- export function releaseInFlightPipeline(filePath, stateHash, registered) {
205
+ function releaseInFlightPipeline(filePath, stateHash, registered) {
176
206
  registered.delete(stateHash);
177
207
  if (registered.size === 0 && inFlightPipelines.get(filePath) === registered) {
178
208
  inFlightPipelines.delete(filePath);
@@ -208,7 +238,7 @@ export function releaseInFlightPipeline(filePath, stateHash, registered) {
208
238
  * already analysed this turn — the duplicate-recording inversion #2464 exists
209
239
  * to remove.
210
240
  */
211
- export function claimPipelineDispatch(args) {
241
+ function claimPipelineDispatch(args) {
212
242
  const { filePath, stateHash, turnIndex, participantId, dbg } = args;
213
243
  // Deduplicate concurrent calls for the same final file state (pi can fire one
214
244
  // tool_result per edit hunk). Do not dedupe by file alone: a distinct later
@@ -454,9 +484,10 @@ function refreshCachedExports(runtime, filePath) {
454
484
  * pipeline AND record" block could not express.
455
485
  */
456
486
  async function dispatchPipelineAnalysis(args) {
457
- const { deps, runtime, filePath, dispatchCwd, turnStateCwd, autofixMode, modifiedRanges, writeIndex, initialStateHash, readGuardCorrelationId, requestedEditIndexes, requestedEditTotal, isPartialApplyResult, toolResultStart, nativeAppliedPairs, } = args;
487
+ const { deps, runtime, filePath, dispatchCwd, turnStateCwd, autofixMode, modifiedRanges, writeIndex, initialStateHash, readGuardCorrelationId, requestedEditIndexes, requestedEditTotal, isPartialApplyResult, toolResultStart, nativeAppliedPairs, allowReadGuardWrites, } = args;
458
488
  const { event, getFlag, getFlagSource, dbg, biomeClient, ruffClient, metricsClient, resetLSPService, } = deps;
459
489
  const pipelinePromise = runPipeline({
490
+ signal: deps.signal,
460
491
  filePath,
461
492
  cwd: dispatchCwd,
462
493
  projectRoot: turnStateCwd,
@@ -497,9 +528,9 @@ async function dispatchPipelineAnalysis(args) {
497
528
  scheduleWordIndexPersist(dispatchCwd, index, dbg);
498
529
  },
499
530
  }, {
500
- biomeClient,
501
- ruffClient,
502
- metricsClient,
531
+ biomeClient: biomeClient,
532
+ ruffClient: ruffClient,
533
+ metricsClient: metricsClient,
503
534
  getFormatService,
504
535
  fixedThisTurn: runtime.fixedThisTurn,
505
536
  });
@@ -632,11 +663,20 @@ async function dispatchPipelineAnalysis(args) {
632
663
  // ladder (the #2402 after-write hash was never stamped), and a duplicate
633
664
  // tool_result for the same bytes analysed the file a second time (the
634
665
  // already-analysed latch was never set).
635
- const finalStateHash = getFileStateHash(filePath);
636
- lastAnalyzedStateByFile.set(filePath, {
637
- turnIndex: runtime.turnIndex,
638
- stateHash: finalStateHash,
639
- });
666
+ // The latch identifies the bytes this pipeline actually analysed. A pipeline
667
+ // that reports no write analysed its input state, even if another writer
668
+ // changed disk while the await was parked. A pipeline-reported write selects
669
+ // only the identity captured by runPipeline before analysis awaits (#2499).
670
+ const pipelineOwnedWriteHash = result.fileModified
671
+ ? result.postWriteStateHash
672
+ : undefined;
673
+ const finalStateHash = pipelineOwnedWriteHash ?? initialStateHash;
674
+ if (!result.fileModified || pipelineOwnedWriteHash !== undefined) {
675
+ lastAnalyzedStateByFile.set(filePath, {
676
+ turnIndex: runtime.turnIndex,
677
+ stateHash: finalStateHash,
678
+ });
679
+ }
640
680
  // #2402: pi-lens' own immediate format/autofix may have rewritten the file
641
681
  // after the native edit was recorded by the caller. Stamp the post-pipeline
642
682
  // state so an identical retry against the formatted bytes is still
@@ -645,7 +685,7 @@ async function dispatchPipelineAnalysis(args) {
645
685
  // `initialStateHash` IS the post-write hash at both call sites — the
646
686
  // classified chain passes `postWriteStateHash` verbatim, the observed path
647
687
  // passes the same pre-settle digest.
648
- if (finalStateHash !== initialStateHash) {
688
+ if (pipelineOwnedWriteHash !== undefined) {
649
689
  for (const pair of nativeAppliedPairs) {
650
690
  runtime.partialApplyRecords.noteAfterWriteHash(filePath, pair.oldText, pair.newText, finalStateHash);
651
691
  }
@@ -653,7 +693,7 @@ async function dispatchPipelineAnalysis(args) {
653
693
  // The model's write/edit and pi-lens' own immediate format/autofix are now
654
694
  // reflected on disk. Refresh read-guard staleness stamps so a follow-up edit
655
695
  // is judged by read-range coverage, not by our own previous write.
656
- if (!getFlag("no-read-guard")) {
696
+ if (!getFlag("no-read-guard") && allowReadGuardWrites) {
657
697
  const changedForReadGuard = new Set([
658
698
  path.resolve(filePath),
659
699
  ...(result.changedFiles ?? []).map((changedFile) => path.resolve(changedFile)),
@@ -670,6 +710,7 @@ export async function handleToolResult(deps) {
670
710
  const { event, getFlag, dbg, runtime, cacheManager, agentBehaviorRecord, formatBehaviorWarnings, } = deps;
671
711
  const rawFilePath = event.input.path;
672
712
  const workspaceRoot = runtime.projectRoot || process.cwd();
713
+ let bashAuthorshipConfirmed = deps._readGuardAuthorship ?? event.toolName !== "bash";
673
714
  // #1642: a gitignored worktree edit got re-attributed onto a
674
715
  // same-relative-path file in the parent checkout because this handler
675
716
  // used to always resolve a relative path against `workspaceRoot`
@@ -791,10 +832,16 @@ export async function handleToolResult(deps) {
791
832
  // THIS set, not raw recognized - otherwise a redirect target dropped
792
833
  // by the isError filter would be subtracted from recovery AND
793
834
  // excluded here, attributed nowhere.
794
- const recognizedWritten = event.isError !== true
835
+ let recognizedWritten = event.isError !== true
795
836
  ? recognized.filter((wp) => !isExternalOrVendorFile(wp, workspaceRoot) &&
796
837
  !isPathIgnoredByProject(wp, workspaceRoot, false))
797
838
  : [];
839
+ // Fence the mtime-based session-authored fallback before any async
840
+ // observation. Only later content evidence may clear this fence.
841
+ if (!getFlag("no-read-guard")) {
842
+ for (const recognizedPath of recognizedWritten)
843
+ deps.readGuard?.recordUnchanged?.(recognizedPath);
844
+ }
798
845
  // #2000 phase 2: when the extractor recognizes NOTHING, the command is
799
846
  // opaque-candidate — recover its actual changed set by diffing the pre
800
847
  // snapshot taken at tool_call. Partial writes that landed before a
@@ -802,6 +849,8 @@ export async function handleToolResult(deps) {
802
849
  // them) — a deliberate divergence from the isError filter above, which
803
850
  // exists for restore semantics where attribution would lie.
804
851
  let opaquePaths = [];
852
+ let observedChangedKeys;
853
+ let recognizedAuthored = [];
805
854
  // Recovery runs for EVERY bash command with a pending baseline - not
806
855
  // only recognized-empty ones. A mixed command (`python x.py > out.ts`
807
856
  // plus script-internal writes) previously skipped observation entirely
@@ -829,6 +878,7 @@ export async function handleToolResult(deps) {
829
878
  if (recovery.verdict === "recovered") {
830
879
  opaquePaths = recovery.paths.filter((p) => !isExternalOrVendorFile(p, scanRoot) &&
831
880
  !isPathIgnoredByProject(p, scanRoot, false));
881
+ observedChangedKeys = new Set(opaquePaths);
832
882
  }
833
883
  else if (recovery.verdict === "unknown") {
834
884
  // #2060: deliberately WIDER than the old `recognized.length > 0`
@@ -837,6 +887,12 @@ export async function handleToolResult(deps) {
837
887
  // nothing at all.
838
888
  unknownReason = recovery.unknownReason;
839
889
  }
890
+ else {
891
+ // A clean git verdict is complete evidence that no path
892
+ // changed; it is not missing evidence. Keep recognized writes
893
+ // out of authorship because git found no changed bytes.
894
+ observedChangedKeys = new Set();
895
+ }
840
896
  // #2060: both counts are bounded (one record per tool_result, no
841
897
  // per-path logging) and exist because filtering is invisible in
842
898
  // production otherwise - the dropped paths simply never appear.
@@ -862,9 +918,11 @@ export async function handleToolResult(deps) {
862
918
  else if (pending.stats) {
863
919
  const outcome = await captureFileStats(scanRoot, {
864
920
  withHashes: true,
921
+ ...deps._opaqueCaptureOptions,
865
922
  });
866
923
  if (outcome.snapshot && !outcome.unknownReason) {
867
- opaquePaths = diffFileStats(pending.stats, outcome.snapshot);
924
+ opaquePaths = diffFileContent(pending.stats, outcome.snapshot);
925
+ observedChangedKeys = new Set(opaquePaths);
868
926
  }
869
927
  else {
870
928
  unknownReason =
@@ -880,6 +938,16 @@ export async function handleToolResult(deps) {
880
938
  const survivingKeys = new Set(recognizedWritten.map((p) => normalizeMapKey(path.resolve(p))));
881
939
  opaquePaths = opaquePaths.filter((p) => !survivingKeys.has(p));
882
940
  }
941
+ if (observedChangedKeys) {
942
+ recognizedAuthored = recognizedWritten.filter((file) => observedChangedKeys.has(normalizeMapKey(path.resolve(file))));
943
+ if (recognizedAuthored.length === 0)
944
+ recognizedWritten = [];
945
+ }
946
+ else if (unknownReason) {
947
+ recognizedAuthored = [];
948
+ // The bounded opaque_mutation_coverage_unknown record below remains
949
+ // the production proof that authorship evidence was unavailable.
950
+ }
883
951
  if (unknownReason) {
884
952
  logLatency({
885
953
  type: "phase",
@@ -906,9 +974,15 @@ export async function handleToolResult(deps) {
906
974
  // set must hold those exact strings - no re-resolution.
907
975
  const opaqueSet = new Set(opaquePaths);
908
976
  const written = [...recognizedWritten, ...opaquePaths];
977
+ const recognizedAuthoredSet = new Set(recognizedAuthored);
978
+ bashAuthorshipConfirmed =
979
+ recognizedAuthored.length > 0 || opaquePaths.length > 0;
909
980
  for (const wp of written) {
910
- if (!getFlag("no-read-guard"))
981
+ if (!getFlag("no-read-guard") &&
982
+ (opaqueSet.has(wp) || recognizedAuthoredSet.has(wp)))
911
983
  deps.readGuard?.recordWritten(wp);
984
+ else if (!getFlag("no-read-guard") && recognizedWritten.includes(wp))
985
+ deps.readGuard?.recordUnchanged?.(wp);
912
986
  const receipt = runtime
913
987
  .recordMutationToolReceipt;
914
988
  const autofixMode = receipt
@@ -939,6 +1013,7 @@ export async function handleToolResult(deps) {
939
1013
  _autofixMode: autofixMode,
940
1014
  _attachmentBudget: syntheticAttachmentBudget,
941
1015
  _mutationSourceOverride: isOpaque ? "opaque-script" : undefined,
1016
+ _readGuardAuthorship: opaqueSet.has(wp) || recognizedAuthoredSet.has(wp),
942
1017
  });
943
1018
  if (syntheticResult) {
944
1019
  // #1590: forward verbatim. The synthetic call already charged the
@@ -949,7 +1024,57 @@ export async function handleToolResult(deps) {
949
1024
  }
950
1025
  }
951
1026
  if (event.isError !== true && !getFlag("no-read-guard")) {
952
- for (const span of extractReadPathsFromCommand(command, workspaceRoot)) {
1027
+ const truncation = event.details?.truncation;
1028
+ const parsedSpans = extractReadPathsFromCommand(command, workspaceRoot);
1029
+ let spans = parsedSpans;
1030
+ if (truncation?.truncated === true &&
1031
+ typeof truncation.outputLines === "number") {
1032
+ if (parsedSpans.length === 1) {
1033
+ const span = parsedSpans[0];
1034
+ if (span) {
1035
+ // countFileLines intentionally preserves the split-based guard
1036
+ // convention, where a trailing newline contributes an empty final
1037
+ // element. The host's output line count does not include that
1038
+ // element, so remove it from the span basis before taking the tail.
1039
+ const hasTrailingNewline = nodeFs
1040
+ .readFileSync(span.filePath, "utf8")
1041
+ .endsWith("\n");
1042
+ const spanLineCount = hasTrailingNewline
1043
+ ? Math.max(1, span.limit - 1)
1044
+ : span.limit;
1045
+ const shown = Math.min(truncation.outputLines, spanLineCount);
1046
+ spans =
1047
+ shown > 0
1048
+ ? [
1049
+ {
1050
+ ...span,
1051
+ offset: span.offset + spanLineCount - shown,
1052
+ limit: shown,
1053
+ },
1054
+ ]
1055
+ : [];
1056
+ }
1057
+ }
1058
+ else if (parsedSpans.length > 1) {
1059
+ recordDegradationOnce({
1060
+ kind: "bash-view-clipped",
1061
+ subject: "multi-span",
1062
+ reason: "truncated bash view contained multiple spans",
1063
+ });
1064
+ spans = [];
1065
+ }
1066
+ if (parsedSpans.length === 1 &&
1067
+ spans.length === 1 &&
1068
+ (spans[0]?.offset !== parsedSpans[0]?.offset ||
1069
+ spans[0]?.limit !== parsedSpans[0]?.limit)) {
1070
+ recordDegradationOnce({
1071
+ kind: "bash-view-clipped",
1072
+ subject: parsedSpans[0]?.filePath ?? "unknown",
1073
+ reason: "truncated bash view narrowed a single span",
1074
+ });
1075
+ }
1076
+ }
1077
+ for (const span of spans) {
953
1078
  if (isExternalOrVendorFile(span.filePath, workspaceRoot))
954
1079
  continue;
955
1080
  if (isPathIgnoredByProject(span.filePath, workspaceRoot, false))
@@ -1029,6 +1154,74 @@ export async function handleToolResult(deps) {
1029
1154
  });
1030
1155
  }
1031
1156
  }
1157
+ // Native read results are the authoritative read boundary. The tool_call
1158
+ // input can request past EOF, and the host can cap bytes or lines before the
1159
+ // result reaches the model. Register only the delivered range (#2802).
1160
+ if (deps.readGuard &&
1161
+ event.toolName === "read" &&
1162
+ event.isError !== true &&
1163
+ filePath &&
1164
+ !getFlag("no-read-guard") &&
1165
+ !isExternalOrVendorFile(filePath, workspaceRoot)) {
1166
+ const deliveredFilePath = attribution?.resolvedPath && nodeFs.existsSync(attribution.resolvedPath)
1167
+ ? attribution.resolvedPath
1168
+ : filePath;
1169
+ if (nodeFs.existsSync(deliveredFilePath)) {
1170
+ const nativeReadToolCallId = resolveToolCallCorrelationId(event);
1171
+ const input = event.input;
1172
+ const requestedOffset = input.offset ?? 1;
1173
+ const requestedLimit = input.limit;
1174
+ const truncation = event.details?.truncation;
1175
+ const available = Math.max(0, countFileLines(deliveredFilePath) - requestedOffset + 1);
1176
+ const deliveredLimit = Math.min(available, truncation?.outputLines ?? requestedLimit ?? available);
1177
+ if (deliveredLimit > 0) {
1178
+ if (truncation?.truncated === true && deliveredLimit < available) {
1179
+ recordDegradationOnce({
1180
+ kind: "native-read-clipped",
1181
+ subject: deliveredFilePath,
1182
+ reason: "native read result was clipped before delivery",
1183
+ });
1184
+ }
1185
+ logReadGuardEvent({
1186
+ event: "read_pattern",
1187
+ sessionId: runtime.telemetrySessionId,
1188
+ filePath: deliveredFilePath,
1189
+ requestedOffset,
1190
+ requestedLimit: requestedLimit ?? deliveredLimit,
1191
+ effectiveOffset: requestedOffset,
1192
+ effectiveLimit: deliveredLimit,
1193
+ metadata: {
1194
+ totalLines: countFileLines(deliveredFilePath),
1195
+ isPartial: deliveredLimit < available,
1196
+ fileKind: detectFileKind(deliveredFilePath) ?? "unknown",
1197
+ fractionRead: available > 0
1198
+ ? Math.round((deliveredLimit / available) * 100) / 100
1199
+ : 1,
1200
+ expandedByTs: false,
1201
+ },
1202
+ });
1203
+ const deliveredRecord = {
1204
+ filePath: deliveredFilePath,
1205
+ requestedOffset,
1206
+ requestedLimit: requestedLimit ?? deliveredLimit,
1207
+ effectiveOffset: requestedOffset,
1208
+ effectiveLimit: deliveredLimit,
1209
+ expandedByLsp: false,
1210
+ turnIndex: runtime.turnIndex,
1211
+ writeIndex: runtime.peekWriteIndex(),
1212
+ timestamp: Date.now(),
1213
+ };
1214
+ if (nativeReadToolCallId) {
1215
+ deps.readGuard.recordRead(deliveredRecord, {
1216
+ supersedes: { toolCallId: nativeReadToolCallId },
1217
+ });
1218
+ }
1219
+ else {
1220
+ deps.readGuard.recordRead(deliveredRecord);
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1032
1225
  // #2423: THE inbound gate. Everything below this line is the mutation
1033
1226
  // bookkeeping chain — read-guard staleness stamp, turn state, change-log
1034
1227
  // receipt, deferred autofix and format. Before the seam this compared
@@ -1198,14 +1391,36 @@ export async function handleToolResult(deps) {
1198
1391
  // "edit", so the un-receipted default is "deferred" either way.
1199
1392
  const observedAutofixMode = receiptOutcome?.autofixMode ??
1200
1393
  (observedKind === "edit" ? "deferred" : "immediate");
1201
- // Analyse the file the observation actually RECORDED, not merely the
1202
- // one the tool named: a directory-target tool names a path that is not
1203
- // a file at all, and `runPipeline` on it is meaningless. `changedPaths`
1204
- // is already filtered by the `isRecordable` predicate handed to the
1205
- // settle above, so membership here also implies not-vendored and
1206
- // not-gitignored — the same two gates the classified chain applies
1207
- // before its own dispatch.
1208
- if (observedChangedPaths.some((candidate) => pathsEqual(candidate, filePath))) {
1394
+ // Analyse the files the observation actually RECORDED, not merely the
1395
+ // path the tool named. A directory is never a pipeline target.
1396
+ const observedDispatchPaths = observedChangedPaths
1397
+ .filter((candidate) => {
1398
+ try {
1399
+ return !nodeFs.statSync(candidate).isDirectory();
1400
+ }
1401
+ catch {
1402
+ return false;
1403
+ }
1404
+ })
1405
+ .slice(0, OBSERVED_DISPATCH_MAX_PATHS);
1406
+ const observedDispatchDropped = observedChangedPaths.length - observedDispatchPaths.length;
1407
+ if (observedDispatchDropped > 0) {
1408
+ incrementDegradationCount({
1409
+ kind: "observed-mutation-dispatch-cap",
1410
+ subject: event.toolName,
1411
+ reason: `observed mutation dispatch capped at ${OBSERVED_DISPATCH_MAX_PATHS}; ${observedDispatchDropped} path(s) not dispatched`,
1412
+ });
1413
+ }
1414
+ if (observedDispatchPaths.length > 0) {
1415
+ const observedClients = ensureToolResultClients(deps);
1416
+ if (observedClients !== true &&
1417
+ !(await bounded(Promise.resolve(observedClients), {
1418
+ ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
1419
+ signal: deps.signal ?? undefined,
1420
+ hook: "tool_result_edit",
1421
+ label: "observed-tool-result-analysis",
1422
+ })))
1423
+ return;
1209
1424
  const observedReadGuardCorrelationId = getReadGuardCorrelationId(event);
1210
1425
  // #2464 review round 3 (F1): the SAME two pre-conditions the
1211
1426
  // classified site consults, through the same shared claim. Round 2
@@ -1215,60 +1430,72 @@ export async function handleToolResult(deps) {
1215
1430
  // second overwrote the first's registry entry, and the first's
1216
1431
  // release then evicted the outer entry a live, unrelated classified
1217
1432
  // pipeline had since re-created under it.
1218
- const observedClaim = claimPipelineDispatch({
1219
- filePath,
1220
- stateHash: observedStateHash,
1221
- turnIndex: runtime.turnIndex,
1222
- participantId: observedReadGuardCorrelationId,
1223
- dbg,
1224
- });
1225
- if (!observedClaim.proceed) {
1226
- if (observedClaim.joined) {
1227
- await observedClaim.joined;
1433
+ for (const observedPath of observedDispatchPaths) {
1434
+ const observedDispatchSignal = deps.signal;
1435
+ const observedJoinSignal = deps.signal;
1436
+ const observedStateHashForPath = getFileStateHash(observedPath);
1437
+ const observedClaim = claimPipelineDispatch({
1438
+ filePath: observedPath,
1439
+ stateHash: observedStateHashForPath,
1440
+ turnIndex: runtime.turnIndex,
1441
+ participantId: observedReadGuardCorrelationId,
1442
+ dbg,
1443
+ });
1444
+ if (!observedClaim.proceed) {
1445
+ if (observedClaim.joined) {
1446
+ await bounded(observedClaim.joined, {
1447
+ ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
1448
+ signal: observedJoinSignal,
1449
+ hook: "tool_result_edit",
1450
+ label: "observed-tool-result-join",
1451
+ });
1452
+ }
1453
+ continue;
1454
+ }
1455
+ const observedDispatchOutcome = await bounded(dispatchPipelineAnalysis({
1456
+ deps,
1457
+ runtime,
1458
+ filePath: observedPath,
1459
+ dispatchCwd: resolveLanguageRootForFile(observedPath, workspaceRoot),
1460
+ turnStateCwd: path.resolve(workspaceRoot),
1461
+ autofixMode: observedAutofixMode,
1462
+ modifiedRanges: undefined,
1463
+ writeIndex: runtime.nextWriteIndex(),
1464
+ initialStateHash: observedStateHashForPath,
1465
+ readGuardCorrelationId: observedReadGuardCorrelationId,
1466
+ requestedEditIndexes: getRequestedEditIndexes(event, observedKind),
1467
+ requestedEditTotal: getRequestedEditCount(event, observedKind),
1468
+ isPartialApplyResult: (event.details ?? {})
1469
+ .piLensPartialApply === true,
1470
+ participantIds: [observedReadGuardCorrelationId],
1471
+ participantTotal: 1,
1472
+ toolResultStart,
1473
+ nativeAppliedPairs: observedAppliedPairs,
1474
+ // #2802 authorship rule (master): the observational
1475
+ // settle already recorded this edit through the
1476
+ // bridge, so the dispatch's read-guard write refresh
1477
+ // carries evidence and stays enabled on every
1478
+ // per-path dispatch.
1479
+ allowReadGuardWrites: true,
1480
+ }), {
1481
+ ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
1482
+ signal: observedDispatchSignal,
1483
+ hook: "tool_result_edit",
1484
+ label: "observed-tool-result-analysis",
1485
+ });
1486
+ if (observedDispatchOutcome?.crashed) {
1487
+ // #2464 review round 2, S6: parity with the classified chain — a
1488
+ // pipeline crash surfaces its notice to the agent instead of being
1489
+ // swallowed into a dbg line the model never sees. The recorded
1490
+ // edit stands either way; only the analysis was lost.
1491
+ dbg(`tool_result: pipeline analysis crashed for the observed mutation on ${observedPath}; the recorded edit stands, analysis did not run this turn`);
1492
+ return observedDispatchOutcome.response;
1228
1493
  }
1229
- // Same terminal value as the block's own exit below: the bridge
1230
- // already recorded this edit, and the analysis it would have asked
1231
- // for is either running or already done.
1232
- return syntheticWriteContent.length > 0
1233
- ? { content: [...event.content, ...syntheticWriteContent] }
1234
- : undefined;
1235
- }
1236
- const observedDispatchOutcome = await dispatchPipelineAnalysis({
1237
- deps,
1238
- runtime,
1239
- filePath,
1240
- dispatchCwd: resolveLanguageRootForFile(filePath, workspaceRoot),
1241
- turnStateCwd: path.resolve(workspaceRoot),
1242
- autofixMode: observedAutofixMode,
1243
- // #2423: no adapter/diff ranges exist for a "learned" (unnamed)
1244
- // tool — the classified chain below hits the same gap for this
1245
- // provenance and also leaves `modifiedRanges` undefined, so the
1246
- // dispatch runs unscoped (whole-file) exactly as it would there.
1247
- modifiedRanges: undefined,
1248
- writeIndex: runtime.nextWriteIndex(),
1249
- initialStateHash: observedStateHash,
1250
- readGuardCorrelationId: observedReadGuardCorrelationId,
1251
- requestedEditIndexes: getRequestedEditIndexes(event, observedKind),
1252
- requestedEditTotal: getRequestedEditCount(event, observedKind),
1253
- isPartialApplyResult: (event.details ?? {})
1254
- .piLensPartialApply === true,
1255
- participantIds: [observedReadGuardCorrelationId],
1256
- participantTotal: 1,
1257
- toolResultStart,
1258
- nativeAppliedPairs: observedAppliedPairs,
1259
- });
1260
- if (observedDispatchOutcome.crashed) {
1261
- // #2464 review round 2, S6: parity with the classified chain — a
1262
- // pipeline crash surfaces its notice to the agent instead of being
1263
- // swallowed into a dbg line the model never sees. The recorded
1264
- // edit stands either way; only the analysis was lost.
1265
- dbg(`tool_result: pipeline analysis crashed for the observed mutation on ${filePath}; the recorded edit stands, analysis did not run this turn`);
1266
- return observedDispatchOutcome.response;
1267
1494
  }
1268
1495
  dbg(`tool_result: the observational settle already recorded ${observedReplayed} mutation(s) for "${event.toolName}"; kept the applied-edit records, mutation receipt, cachedExports refresh, and ran the pipeline dispatch (#2464); skipped the bridge-covered staleness stamp / turn-state ranges / change-log receipt / deferred autofix+format`);
1269
1496
  }
1270
1497
  else {
1271
- dbg(`tool_result: the observational settle recorded ${observedReplayed} mutation(s) for "${event.toolName}", none of them ${filePath} — nothing to analyse under the named path`);
1498
+ dbg(`tool_result: the observational settle recorded ${observedReplayed} mutation(s) for "${event.toolName}", but no dispatchable changed paths remained`);
1272
1499
  }
1273
1500
  }
1274
1501
  return syntheticWriteContent.length > 0
@@ -1379,7 +1606,8 @@ export async function handleToolResult(deps) {
1379
1606
  }
1380
1607
  // Refresh the read-guard's FileTime stamp so that the model's own write
1381
1608
  // doesn't trigger a spurious "file_modified" block on the next edit.
1382
- deps.readGuard?.recordWritten(filePath);
1609
+ if (bashAuthorshipConfirmed)
1610
+ deps.readGuard?.recordWritten(filePath);
1383
1611
  // Keep cachedExports in sync after each write/edit so the pre-write STOP
1384
1612
  // check doesn't fire on names that were removed from this file this session.
1385
1613
  refreshCachedExports(runtime, filePath);
@@ -1411,7 +1639,7 @@ export async function handleToolResult(deps) {
1411
1639
  // tool_result is emitted after write/edit has already been applied.
1412
1640
  // Asserting pre-write stamps here produces false positives on rapid edits.
1413
1641
  sessionFileTime.read(filePath);
1414
- if (!getFlag("no-read-guard")) {
1642
+ if (!getFlag("no-read-guard") && bashAuthorshipConfirmed) {
1415
1643
  const readGuard = runtime.readGuard;
1416
1644
  readGuard?.recordWritten?.(filePath);
1417
1645
  }
@@ -1532,7 +1760,16 @@ export async function handleToolResult(deps) {
1532
1760
  // (defined above `handleToolResult`), shared with the observed-mutation
1533
1761
  // early return. This call site is otherwise unchanged — same arguments, same
1534
1762
  // crash-then-return / success-then-continue shape as before the split.
1535
- const dispatchOutcome = await dispatchPipelineAnalysis({
1763
+ const classifiedClients = ensureToolResultClients(deps);
1764
+ if (classifiedClients !== true &&
1765
+ !(await bounded(Promise.resolve(classifiedClients), {
1766
+ ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
1767
+ signal: deps.signal,
1768
+ hook: "tool_result_edit",
1769
+ label: "classified-tool-result-analysis",
1770
+ })))
1771
+ return;
1772
+ const dispatchOutcome = await bounded(dispatchPipelineAnalysis({
1536
1773
  deps,
1537
1774
  runtime,
1538
1775
  filePath,
@@ -1550,7 +1787,15 @@ export async function handleToolResult(deps) {
1550
1787
  participantTotal,
1551
1788
  toolResultStart,
1552
1789
  nativeAppliedPairs,
1790
+ allowReadGuardWrites: bashAuthorshipConfirmed,
1791
+ }), {
1792
+ ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
1793
+ signal: deps.signal || undefined,
1794
+ hook: "tool_result_edit",
1795
+ label: "pipeline-analysis",
1553
1796
  });
1797
+ if (!dispatchOutcome)
1798
+ return;
1554
1799
  if (dispatchOutcome.crashed) {
1555
1800
  return dispatchOutcome.response;
1556
1801
  }
@@ -1684,7 +1929,7 @@ export async function handleToolResult(deps) {
1684
1929
  // #1561: stamp the verdict with THIS dispatch's write token — the same
1685
1930
  // counter `lsp_diagnostics`' reconciliation seam draws from — so a later
1686
1931
  // confirmed-clean result can be ordered against it instead of racing it.
1687
- runtime.recordInlineBlockers(filePath, result.inlineBlockerSummary, writeIndex, result.inlineBlockerSources, result.inlineBlockerLines);
1932
+ runtime.recordInlineBlockers(filePath, result.inlineBlockerSummary, writeIndex, result.inlineBlockerSources, result.inlineBlockerLines, result.inlineBlockerFileContent);
1688
1933
  }
1689
1934
  else {
1690
1935
  runtime.clearInlineBlockers(filePath);