@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
@@ -7,7 +7,9 @@
7
7
  * Approach (inspired by OpenCode's Filesystem.normalizePath):
8
8
  * - On Windows: try realpathSync.native() for canonical casing
9
9
  * - Falls back to lowercase for files that don't exist yet
10
- * - On non-Windows: return path as-is (case-sensitive filesystem)
10
+ * - On non-Windows: adopt the on-disk casing of an existing path (#3098 — a
11
+ * case-insensitive POSIX filesystem otherwise derives two Map keys for one
12
+ * file); a path that does not exist is returned as-is, case-preserving
11
13
  * - Always convert backslashes to forward slashes for Map key consistency
12
14
  */
13
15
  import { existsSync, readFileSync, realpathSync, statSync, } from "node:fs";
@@ -91,6 +93,64 @@ export function isAbsoluteFilePath(filePath) {
91
93
  export function splitPathSegments(filePath) {
92
94
  return filePath.split(/[\\/]+/).filter(Boolean);
93
95
  }
96
+ /**
97
+ * Adopt `canonical`'s CASING for the trailing segments of `held`, stopping at
98
+ * the first segment that differs by more than case.
99
+ *
100
+ * `realpathSync.native` answers a different question than the one the POSIX
101
+ * arm of `normalizeFilePath` asks: it resolves symlinks AND reports on-disk
102
+ * casing, and we want only the second half. Walking from the tail and halting
103
+ * at the first structural divergence separates them with no extra syscall:
104
+ *
105
+ * held /var/folders/T/x/SUB/a.ts (macOS tmpdir, mis-cased segment)
106
+ * canonical /private/var/folders/T/x/sub/a.ts
107
+ * result /var/folders/T/x/sub/a.ts (case fixed, symlink prefix kept)
108
+ *
109
+ * The symlink prefix matters: macOS's `os.tmpdir()` is `/var/folders/...`,
110
+ * a symlink into `/private/var`, so a whole-string "is this a case variant"
111
+ * test would decline to canonicalize exactly the paths the #1024 regression
112
+ * test (and any macOS temp-dir workflow) runs on. Keeping the prefix is also
113
+ * what bounds this change: on POSIX, `normalizeFilePath` still never resolves
114
+ * a symlink, so a symlinked monorepo package keeps keying under the path the
115
+ * caller held (refs #2490 — a cwd fold for its own sake broke every monorepo).
116
+ *
117
+ * PURE string algebra: this pins what the rewrite DOES, never what the kernel
118
+ * reports — and from two strings alone it cannot tell "the same file, spelled
119
+ * with different case" from "a different file whose name happens to be a case
120
+ * variant". Any symlink whose BASENAME is a case variant of its target's
121
+ * basename (`<root>/MyProject` → `work/myproject`, `node_modules/Foo` →
122
+ * `../pkgs/foo`) has its name replaced while its own parent is kept, so the
123
+ * result names a different place — or no place at all. Measured in #3159
124
+ * review round 2 (F1): two different inodes collapsed onto ONE key (the #1024
125
+ * defect inverted — false suppression), and a symlinked package keyed under a
126
+ * path that does not exist on disk, breaking the #2490 bound above. The caller
127
+ * therefore CONFIRMS every rewrite against the filesystem before adopting it
128
+ * (see `normalizeFilePath`); do not use this function without that step.
129
+ */
130
+ function adoptCanonicalCasing(held, canonical) {
131
+ const heldParts = held.split("/");
132
+ const realParts = canonical.split("/");
133
+ let i = heldParts.length - 1;
134
+ let j = realParts.length - 1;
135
+ let changed = false;
136
+ while (i >= 0 && j >= 0) {
137
+ const a = heldParts[i];
138
+ const b = realParts[j];
139
+ if (a !== b) {
140
+ if (a.toLowerCase() !== b.toLowerCase())
141
+ break;
142
+ heldParts[i] = b;
143
+ changed = true;
144
+ }
145
+ i--;
146
+ j--;
147
+ }
148
+ // Identity-preserving when nothing moved — this runs on every POSIX map-key
149
+ // derivation and the overwhelmingly common answer is "already canonical".
150
+ // NOT a behavioural branch (both arms produce an equal string): it is the
151
+ // same allocation guard `normalizeEphemeralMapKey` documents below.
152
+ return changed ? heldParts.join("/") : held;
153
+ }
94
154
  /**
95
155
  * Normalize a file path for consistent Map key usage.
96
156
  *
@@ -100,7 +160,33 @@ export function splitPathSegments(filePath) {
100
160
  * - If the file doesn't exist: resolves the path and lowercases
101
161
  * (needed for new files where we haven't written yet)
102
162
  *
103
- * On non-Windows: returns path as-is (case-sensitive filesystem).
163
+ * On POSIX:
164
+ * - If the file exists: adopts the on-disk CASING of the trailing segments
165
+ * (see `adoptCanonicalCasing`) — no lowercasing, no symlink resolution.
166
+ * - If the file doesn't exist: returns the path as-is, case-PRESERVING. On a
167
+ * case-sensitive filesystem `SUB/a.ts` and `sub/a.ts` are two different
168
+ * files, and nothing may fold one into the other; only the filesystem's own
169
+ * answer for a path that EXISTS can tell the two apart, and for a path that
170
+ * does not exist there is no such answer to ask for.
171
+ *
172
+ * Why POSIX canonicalizes casing at all (#3098, the #1024 defect's live half):
173
+ * a case-insensitive POSIX filesystem — macOS's default APFS, `nocase` vfat /
174
+ * ntfs3 / cifs mounts — makes `SUB/a.ts` and `sub/a.ts` ONE file, so a raw
175
+ * mis-cased write (`lens_diagnostic_mark` anchors under `path.resolve(cwd,
176
+ * arg)`) and a `normalizeMapKey` read derived two anchors for one file and the
177
+ * agent's own disposition mark silently never applied. A case-preserving POSIX
178
+ * arm cannot close that: the two keys only become one if the normalizer asks
179
+ * the filesystem which name is really on disk.
180
+ *
181
+ * `realpathSync.native` is `realpath(3)`. On Darwin it rebuilds every
182
+ * component from the filesystem's own `ATTR_CMN_NAME` — Libc-1669.0.4
183
+ * `stdlib/FreeBSD/realpath.c:233` (`getattrlist(resolved, &_rp_alist, …,
184
+ * FSOPT_NOFOLLOW)` with `ATTR_CMN_NAME`) and `:348-354` ("attrs already has
185
+ * the real name") — which is why it returns on-disk casing on APFS/HFS+.
186
+ * MEASURED counter-example (#3098): a Linux ext4/tmpfs `chattr +F` casefold
187
+ * directory aliases the two spellings but `realpath(3)` there returns the
188
+ * spelling the caller asked with, so this arm is a no-op on casefolded Linux
189
+ * directories — filed with the transcript as #3154.
104
190
  *
105
191
  * Always converts backslashes to forward slashes for consistent Map keys.
106
192
  */
@@ -108,7 +194,31 @@ export function normalizeFilePath(filePath) {
108
194
  // Convert backslashes to forward slashes first
109
195
  const normalized = filePath.replace(/\\/g, "/");
110
196
  if (process.platform !== "win32" && !isWindowsPath(normalized)) {
111
- return normalized;
197
+ try {
198
+ const canonical = realpathSync.native(normalized);
199
+ // Fast path, not a guard: both arms answer `normalized` when the
200
+ // strings match, but skipping the two `split`s there is a measured
201
+ // 1.9 vs 2.3 microseconds per call on the per-edit seam (#3098).
202
+ if (canonical === normalized)
203
+ return normalized;
204
+ const adopted = adoptCanonicalCasing(normalized, canonical);
205
+ if (adopted === normalized)
206
+ return normalized;
207
+ // The rewrite is string algebra and can land on a DIFFERENT file
208
+ // (#3159 review round 2, F1 — see `adoptCanonicalCasing`). Adopt it
209
+ // only once the filesystem agrees it still names the file the caller
210
+ // held: `canonical` IS `realpath(normalized)`, so this asks exactly
211
+ // "does the rewritten spelling resolve to the same file?". One extra
212
+ // syscall, and only on the rare branch where casing actually moved —
213
+ // never on an already-canonical path. A throw here (the rewritten
214
+ // path does not exist, the #2490 monorepo case) lands in the catch
215
+ // below and keeps the caller's spelling, which is the same answer.
216
+ return realpathSync.native(adopted) === canonical ? adopted : normalized;
217
+ }
218
+ catch {
219
+ // Does not exist (or is unreadable): case-preserving, as above.
220
+ return normalized;
221
+ }
112
222
  }
113
223
  // Windows: try realpathSync.native() for canonical casing
114
224
  // This resolves symlinks and returns the actual filesystem casing
@@ -130,6 +240,16 @@ export function normalizeFilePath(filePath) {
130
240
  }
131
241
  }
132
242
  }
243
+ /** Resolve a filesystem identity once, retaining a usable absolute fallback. */
244
+ export function realpathOrResolve(filePath) {
245
+ const resolved = path.resolve(filePath);
246
+ try {
247
+ return realpathSync(resolved);
248
+ }
249
+ catch {
250
+ return resolved;
251
+ }
252
+ }
133
253
  /**
134
254
  * Normalize a logged `filePath`/`cwd` value, but ONLY when it is already a
135
255
  * fully-qualified path (#2219, the #2141 class's sibling loggers). Several
@@ -5,7 +5,11 @@ import { getMaxLogSizeMB } from "./log-cleanup.js";
5
5
  const DEFAULT_PERF_TOP_N = 5;
6
6
  const MAX_PERF_TOP_N = 50;
7
7
  export const MAX_PERF_PHASE_SAMPLES = 20_000;
8
- const PARSE_YIELD_EVERY = 500;
8
+ // Yield cadence for the full-window parse below. Exported so the occupancy
9
+ // guard can count yields against the lines fed instead of measuring a
10
+ // wall-clock block the scheduler can inflate (#2886): the count is a
11
+ // deterministic function of the input, not of worker contention.
12
+ export const PARSE_YIELD_EVERY = 500;
9
13
  // The window follows the same threshold that rotates the log out from under it.
10
14
  // Exported so a test can size a fixture to the window it means to saturate
11
15
  // rather than to a second copy of the default that can drift out of agreement.
@@ -114,6 +114,6 @@ export function resolvePhpCsFixerConfig(filePath) {
114
114
  const startDir = resolveToolCwd("formatter", "php-cs-fixer", absolute, {
115
115
  cwd: path.parse(absolute).root,
116
116
  allowHomeMarker: true,
117
- });
117
+ }).cwd;
118
118
  return PHP_CS_FIXER_CONFIG_NAMES.map((name) => path.join(startDir, name)).find(existsSync);
119
119
  }
@@ -11,12 +11,14 @@
11
11
  * 6. Test runner (run corresponding test file)
12
12
  * 7. Cascade diagnostics (other files with errors, LSP only)
13
13
  */
14
+ import { createHash } from "node:crypto";
14
15
  import * as nodeFs from "node:fs";
16
+ import * as nodeCrypto from "node:crypto";
15
17
  import * as path from "node:path";
16
18
  import { findNearestContaining, normalizeEphemeralMapKey, } from "./path-utils.js";
17
19
  import { recordFromDispatchDiagnostic, } from "./actionable-warnings.js";
18
20
  import { recordFromCodeQualityDiagnostic, } from "./code-quality-warnings.js";
19
- import { recordDiagnostics } from "./widget-state.js";
21
+ import { admitWidgetDiagnosticsWrite, recordDiagnostics, } from "./widget-state.js";
20
22
  import { getDiagnosticLogger } from "./diagnostic-logger.js";
21
23
  import { getDiagnosticTracker } from "./diagnostic-tracker.js";
22
24
  import { loadDispatchIntegration } from "./dispatch/lazy.js";
@@ -36,8 +38,10 @@ import { RUNTIME_CONFIG } from "./runtime-config.js";
36
38
  import { getAmbientAbortSignal, safeSpawnAsync } from "./safe-spawn.js";
37
39
  import { probeToolAsync } from "./tool-probe.js";
38
40
  import { bounded } from "./deadline-utils.js";
41
+ import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
39
42
  import { enabledAuxiliaryLspServerIds } from "./dispatch/auxiliary-lsp.js";
40
43
  import { recordDegradationOnce } from "./degradation-ledger.js";
44
+ import { establishToolAgreement } from "./tool-agreement.js";
41
45
  import { dropFindingsForMissingPaths } from "./advisory-provenance.js";
42
46
  import { getAutofixPolicyForFile, getPreferredAutofixTools, getRubocopCommand, hasBiomeConfig, hasDetektConfig, hasEslintConfig, hasGolangciConfig, hasKtfmtConfig, hasKtlintConfig, hasOxlintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, markdownlintConfigArgs, } from "./tool-policy.js";
43
47
  const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
@@ -462,6 +466,17 @@ export async function runAutofix(filePath, cwd, getFlag, dbg, deps, getFlagSourc
462
466
  }
463
467
  for (const toolName of preferredAutofixTools) {
464
468
  attemptedTools.push(toolName);
469
+ const agreement = establishToolAgreement(toolName, cwd);
470
+ if (agreement.decision === "decline") {
471
+ const reason = agreement.reason;
472
+ dbg(`autofix: ${toolName} declined (${reason})`);
473
+ recordDegradationOnce({
474
+ kind: "autofix-agreement-unavailable",
475
+ subject: agreement.subject,
476
+ reason,
477
+ });
478
+ continue;
479
+ }
465
480
  if (toolName === "ruff") {
466
481
  const ruffReady = ruffClient.isPythonFile(filePath)
467
482
  ? await ruffClient.ensureAvailable()
@@ -818,7 +833,7 @@ function buildAllClearOutput(_dispatchResult, elapsed, filePath) {
818
833
  parts.push(`${elapsed}ms`);
819
834
  return `checkmark ${parts.join(" · ")}`.replace("checkmark", "\u2713");
820
835
  }
821
- export async function runFormatPhase(filePath, getFormatService, dbg) {
836
+ export async function runFormatPhase(filePath, getFormatService, dbg, signal, budgetMs = HOOK_WALL_BUDGET_MS.tool_result_edit, hook = "tool_result_edit") {
822
837
  let formatChanged = false;
823
838
  let formattersUsed = [];
824
839
  const formatFailures = [];
@@ -827,13 +842,19 @@ export async function runFormatPhase(filePath, getFormatService, dbg) {
827
842
  const formatService = getFormatService();
828
843
  try {
829
844
  formatService.recordRead(filePath);
830
- const result = await formatService.formatFile(filePath);
845
+ const result = await formatService.formatFile(filePath, {
846
+ signal,
847
+ budgetMs,
848
+ hook,
849
+ });
831
850
  // An unavailable tool is NOT a formatter that ran (#2413): keep it out of
832
851
  // `formattersUsed` (which drives change bookkeeping / turn summaries) and
833
852
  // out of `formatFailures` (which requeues). Record it once, distinctly.
834
853
  for (const f of result.formatters) {
835
854
  if (f.outcome !== "unavailable")
836
855
  continue;
856
+ if (f.error?.includes("project tool agreement could not be established"))
857
+ continue;
837
858
  const reason = f.error ?? "formatter executable not found";
838
859
  formatUnavailable.push({ formatter: f.name, reason });
839
860
  recordDegradationOnce({
@@ -931,6 +952,7 @@ function buildEnrichedBlockerOutput(blockers, fileContent = "") {
931
952
  export async function runPipeline(ctx, deps) {
932
953
  const { filePath, cwd, toolName, getFlag, getFlagSource, dbg } = ctx;
933
954
  const { getFormatService } = deps;
955
+ admitWidgetDiagnosticsWrite(filePath, ctx.telemetry?.writeIndex);
934
956
  const phase = createPhaseTracker(toolName, filePath);
935
957
  const pipelineStart = Date.now();
936
958
  clearGraphCache();
@@ -955,7 +977,7 @@ export async function runPipeline(ctx, deps) {
955
977
  const immediateFormat = !!getFlag("immediate-format");
956
978
  const formatDeferred = !autoformatDisabled && !immediateFormat && !!fileContent;
957
979
  if (!autoformatDisabled && immediateFormat && fileContent) {
958
- const formatResult = await runFormatPhase(filePath, getFormatService, dbg);
980
+ const formatResult = await runFormatPhase(filePath, getFormatService, dbg, ctx.signal);
959
981
  formatChanged = formatResult.formatChanged;
960
982
  formattersUsed = formatResult.formattersUsed;
961
983
  formatFailures = formatResult.formatFailures;
@@ -1043,6 +1065,30 @@ export async function runPipeline(ctx, deps) {
1043
1065
  attemptedTools,
1044
1066
  skipReason: autofixSkipReason,
1045
1067
  });
1068
+ // Capture the target's bytes immediately after pi-lens writes and before any
1069
+ // awaitable LSP or dispatch work. `fileModified` also covers side-effect
1070
+ // files, so target membership is the discriminator for pipeline ownership
1071
+ // (#2499).
1072
+ const fileModified = formatChanged || fixedCount > 0;
1073
+ const targetFileModified = piChangedFiles.has(path.resolve(filePath));
1074
+ const postWriteStateHash = fileModified && targetFileModified
1075
+ ? (() => {
1076
+ try {
1077
+ return nodeCrypto
1078
+ .createHash("sha256")
1079
+ .update(nodeFs.readFileSync(filePath))
1080
+ .digest("hex");
1081
+ }
1082
+ catch (error) {
1083
+ recordDegradationOnce({
1084
+ kind: "pipeline-post-write-hash-unavailable",
1085
+ subject: filePath,
1086
+ reason: error instanceof Error ? error.message : String(error),
1087
+ });
1088
+ return undefined;
1089
+ }
1090
+ })()
1091
+ : undefined;
1046
1092
  // --- 4. LSP file sync ---
1047
1093
  // Sync once with final post-format/post-fix content so dispatch and cascade
1048
1094
  // diagnostics do not observe stale pre-format text.
@@ -1060,6 +1106,21 @@ export async function runPipeline(ctx, deps) {
1060
1106
  getFlag: getFlag,
1061
1107
  };
1062
1108
  const { dispatchLintWithResult, computeCascadeForFile } = await loadDispatchIntegration();
1109
+ // Capture the bytes the pipeline presents to analysis before the dispatch
1110
+ // promise can yield to another writer. The blocker evidence below belongs to
1111
+ // this analysis input, not to whatever happens to be on disk when the whole
1112
+ // pipeline returns.
1113
+ const inlineBlockerFileContent = fileContent
1114
+ ? (() => {
1115
+ const content = Buffer.from(fileContent, "utf8");
1116
+ return content.byteLength <= 2 * 1024 * 1024
1117
+ ? {
1118
+ size: content.byteLength,
1119
+ sha256: createHash("sha256").update(content).digest("hex"),
1120
+ }
1121
+ : undefined;
1122
+ })()
1123
+ : undefined;
1063
1124
  const dispatchResult = await dispatchLintWithResult(filePath, cwd, piApi, ctx.modifiedRanges, {
1064
1125
  model: ctx.telemetry?.model ?? "unknown",
1065
1126
  sessionId: ctx.telemetry?.sessionId ?? "unknown",
@@ -1216,6 +1277,11 @@ export async function runPipeline(ctx, deps) {
1216
1277
  dbg,
1217
1278
  turnSeq: ctx.telemetry?.turnIndex,
1218
1279
  writeSeq: ctx.telemetry?.writeIndex,
1280
+ // #3157: `cwd` here is the LANGUAGE root. The cascade's display
1281
+ // filter reads the disposition store and the `.pi-lens.json` rule
1282
+ // policy, both written under the PROJECT root (#1030) — the same
1283
+ // pair the dispatcher is handed above.
1284
+ projectRoot: ctx.projectRoot,
1219
1285
  seqState: ctx.seqState,
1220
1286
  turnEndCascadeSettleStart: ctx.turnEndCascadeSettleStart,
1221
1287
  fileContent,
@@ -1250,7 +1316,6 @@ export async function runPipeline(ctx, deps) {
1250
1316
  output = buildAllClearOutput(dispatchResult, elapsed, filePath);
1251
1317
  }
1252
1318
  phase.end("total", { hasOutput: !!output });
1253
- const fileModified = formatChanged || fixedCount > 0;
1254
1319
  const changedFiles = [...piChangedFiles];
1255
1320
  emitLensAnalysisComplete({
1256
1321
  cwd,
@@ -1276,6 +1341,7 @@ export async function runPipeline(ctx, deps) {
1276
1341
  cascadePromise,
1277
1342
  isError: false,
1278
1343
  fileModified,
1344
+ postWriteStateHash,
1279
1345
  changedFiles,
1280
1346
  inlineBlockerSummary: dispatchResult.hasBlockers
1281
1347
  ? dispatchResult.blockerOutput.trim() || undefined
@@ -1338,5 +1404,8 @@ export async function runPipeline(ctx, deps) {
1338
1404
  source: "autofix",
1339
1405
  }
1340
1406
  : undefined,
1407
+ inlineBlockerFileContent: dispatchResult.hasBlockers
1408
+ ? inlineBlockerFileContent
1409
+ : undefined,
1341
1410
  };
1342
1411
  }
@@ -4,9 +4,17 @@ import { getProjectDataDir } from "../file-utils.js";
4
4
  import { writeFileAtomic } from "../atomic-write.js";
5
5
  import { readJsonCache } from "../json-cache-read.js";
6
6
  import { freshnessFromMtime } from "../freshness.js";
7
+ import { hashDiagnosticContent } from "../lsp/diagnostic-binding.js";
8
+ // v3 (#2154, #3060 review F1): rows carry `fileFingerprints` — the size and
9
+ // sha256 of the bytes the scan actually read. A v2 snapshot has no content
10
+ // axis at all; its rows can only be judged by `mtime <= scannedAt`, the test
11
+ // that let an edit landing mid-scan read as fresh forever. Every row here is
12
+ // a POSITIVE claim, so v2 records are rejected by the version guard and the
13
+ // project is re-scanned rather than served under the axis that failed — the
14
+ // same clean break `WORKSPACE_DIAGNOSTICS_CACHE_VERSION` v3 (#2776) made.
7
15
  // v2: cheap-tier scan now also runs ast-grep-napi (#308); invalidate older
8
16
  // snapshots so a pre-ast-grep cache isn't served as complete via refreshRunners=cached.
9
- export const PROJECT_DIAGNOSTICS_CACHE_VERSION = 2;
17
+ export const PROJECT_DIAGNOSTICS_CACHE_VERSION = 3;
10
18
  const SNAPSHOT_CACHE_FILE = "project-diagnostics.json";
11
19
  const DELTA_CACHE_FILE = "project-diagnostics-delta.json";
12
20
  function cachePath(cwd, fileName) {
@@ -80,15 +88,39 @@ export function reconcileProjectDiagnosticsSnapshot(snapshot) {
80
88
  if (cached !== undefined)
81
89
  return cached;
82
90
  let stale;
83
- let mtimeMs;
91
+ let stat;
84
92
  try {
85
- mtimeMs = fs.statSync(filePath).mtimeMs;
93
+ stat = fs.statSync(filePath);
86
94
  }
87
95
  catch {
88
- mtimeMs = undefined; // deleted / unreadable -> indeterminate -> drop
96
+ stat = undefined; // deleted / unreadable -> indeterminate -> drop
97
+ }
98
+ // #2154: when the scan recorded what this file's bytes WERE, that answers
99
+ // the question directly and the timestamp comparison below cannot — a
100
+ // file edited while the scan was still running carries an mtime at or
101
+ // before `scannedAt` while holding bytes no rule here ever saw. Size is
102
+ // the cheap reject; only a same-size file is read and hashed. An exact
103
+ // content match is stronger evidence than any mtime ordering, so it
104
+ // settles the verdict on its own: these are per-file syntax rules, so
105
+ // identical bytes mean identical findings no matter what the clock says.
106
+ const fingerprint = snapshot.fileFingerprints?.[filePath];
107
+ if (fingerprint && stat) {
108
+ stale = true;
109
+ if (stat.size === fingerprint.sizeBytes) {
110
+ try {
111
+ stale =
112
+ hashDiagnosticContent(fs.readFileSync(filePath, "utf-8")) !==
113
+ fingerprint.contentHash;
114
+ }
115
+ catch {
116
+ stale = true; // unreadable now -> indeterminate -> drop
117
+ }
118
+ }
119
+ staleByFile.set(filePath, stale);
120
+ return stale;
89
121
  }
90
122
  const verdict = freshnessFromMtime({
91
- mtimeMs,
123
+ mtimeMs: stat?.mtimeMs,
92
124
  referenceMs: scannedAtMs,
93
125
  });
94
126
  // Pre-kernel policy: an unreadable/missing file is stale (dropped).