@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
@@ -401,7 +401,7 @@ async function fileView(absolute, resolved, homeDir, workspaceCwd, lspConfig) {
401
401
  resolvedCwd: homeRelativePath(resolveToolCwd("runner", id, absolute, {
402
402
  cwd: workspaceCwd,
403
403
  suppressTelemetry: true,
404
- }), homeDir),
404
+ }).cwd, homeDir),
405
405
  }));
406
406
  return {
407
407
  path: homeRelativePath(absolute, homeDir),
@@ -4,7 +4,10 @@
4
4
  * Centralized file type detection to avoid duplication across clients.
5
5
  * Maps file extensions and paths to semantic file kinds.
6
6
  */
7
- import { basename, extname } from "node:path";
7
+ import { basename, extname, win32 } from "node:path";
8
+ import * as path from "node:path";
9
+ import { isWindowsPath } from "./path-utils.js";
10
+ import { findNearestDirWithMarker } from "./workspace-topology.js";
8
11
  // --- Extension Maps ---
9
12
  export const KIND_EXTENSIONS = {
10
13
  clojure: [".clj", ".cljc", ".cljs", ".edn"],
@@ -40,6 +43,9 @@ export const KIND_EXTENSIONS = {
40
43
  ".cu",
41
44
  // HIP
42
45
  ".hip",
46
+ // CUDA/HIP header convention; clang's driver table omits `.cuh`, but
47
+ // projects use it for C++ headers that must remain in the cxx kind (#2986).
48
+ ".cuh",
43
49
  // Objective-C
44
50
  ".m",
45
51
  ".mm",
@@ -179,6 +185,22 @@ export const SPECIAL_FILENAMES = [
179
185
  })),
180
186
  ];
181
187
  // --- Detection Functions ---
188
+ /**
189
+ * Helm renders YAML-looking files as Go templates before they become YAML.
190
+ * Require the shared chart-root marker before promoting a YAML template, so
191
+ * ordinary repositories can use a `templates/` directory without entering
192
+ * Helm dispatch. `.tpl` remains extension-classified below.
193
+ */
194
+ export function isHelmYamlTemplatePath(filePath) {
195
+ const templatesSegment = isWindowsPath(filePath)
196
+ ? /[\\/]templates[\\/]/i
197
+ : /\/templates\//i;
198
+ if (!templatesSegment.test(filePath) || !/\.ya?ml$/i.test(filePath)) {
199
+ return false;
200
+ }
201
+ const pathApi = isWindowsPath(filePath) ? win32 : path;
202
+ return Boolean(findNearestDirWithMarker(pathApi.dirname(filePath), "chartYamlPath"));
203
+ }
182
204
  /**
183
205
  * Detect the file kind from a file path.
184
206
  * Returns the semantic file kind or undefined if unknown.
@@ -187,6 +209,9 @@ export function detectFileKind(filePath) {
187
209
  if (!filePath || typeof filePath !== "string") {
188
210
  return undefined;
189
211
  }
212
+ if (isHelmYamlTemplatePath(filePath)) {
213
+ return "helm-template";
214
+ }
190
215
  // Check special filenames first
191
216
  const base = basename(filePath);
192
217
  for (const { pattern, kind } of SPECIAL_FILENAMES) {
@@ -34,6 +34,24 @@ function isTypeStub(content) {
34
34
  /^export\s+type\s+\{/.test(l));
35
35
  return typeOnlyLines.length / lines.length >= 0.75;
36
36
  }
37
+ // Shared filename predicate (#2928). The parity test iterates the runner's
38
+ // discovery table; additional conventions are pinned in file-role.test.ts.
39
+ // Keep the case-preserved basename for CamelCase suffixes (contest.java is source).
40
+ function testFileNameMatches(rawBase, base) {
41
+ return (base.includes(".test.") ||
42
+ base.includes(".spec.") ||
43
+ base.includes("_test.") ||
44
+ base.includes("_spec.") ||
45
+ base.startsWith("test_") ||
46
+ base.startsWith("spec_") ||
47
+ /(Test|Tests|TestCase)\.(java|kt|kts|cs|fs|php)$/.test(rawBase));
48
+ }
49
+ /** Filename-only classification for consumers with their own directory policy. */
50
+ export function isTestFileName(filePath) {
51
+ const windowsShaped = isWindowsPath(filePath);
52
+ const rawBase = windowsShaped ? win32.basename(filePath) : basename(filePath);
53
+ return testFileNameMatches(rawBase, rawBase.toLowerCase());
54
+ }
37
55
  /**
38
56
  * Classify the structural role of a file. Pass `content` for higher
39
57
  * accuracy (enables generated-code detection, barrel detection, etc.).
@@ -57,27 +75,9 @@ export function detectFileRole(filePath, content) {
57
75
  .replace(/\\/g, "/")
58
76
  .toLowerCase();
59
77
  // --- Test ---
60
- // Suffix conventions informed by the test-runner table
61
- // (SOURCE_TO_TEST_PATTERNS + RUNNERS kinds in
62
- // clients/test-runner-client.ts); hand-written, not iterated from it —
63
- // see #2928 for the single-classifier fold:
64
- // - `_test.` infix: Go (`*_test.go`), pytest (`*_test.py`), ExUnit
65
- // (`*_test.exs`), Dart (`*_test.dart`). The underscore anchor keeps
66
- // this precise: `contest.py` and `latest.ts` do not match.
67
- // - `_spec.` infix: RSpec (`*_spec.rb`).
68
- // - CamelCase `*Test(s).<ext>` suffix, matched case-SENSITIVELY on the
69
- // raw basename: JUnit/Surefire (`*Test.java`, `*TestCase.java`),
70
- // Kotlin (`*Test.kt`), dotnet (`*Test.cs`/`*Tests.cs`), PHPUnit
71
- // (`*Test.php`). Case matters: Surefire's `*Test.java` does not match
72
- // `contest.java`, so neither do we.
73
- if (base.includes(".test.") ||
74
- base.includes(".spec.") ||
75
- base.includes("_test.") ||
76
- base.includes("_spec.") ||
77
- base.startsWith("test_") ||
78
- base.startsWith("spec_") ||
79
- /(Test|Tests|TestCase)\.(java|kt|kts|cs|fs|php)$/.test(rawBase) ||
80
- dir.includes("/__tests__/") ||
78
+ // #2928: dirname has no trailing slash; match direct __tests__ children too.
79
+ if (testFileNameMatches(rawBase, base) ||
80
+ /(^|[\\/])__tests__([\\/]|$)/.test(dir) ||
81
81
  dir.includes("/test/") ||
82
82
  dir.includes("/tests/") ||
83
83
  dir.includes("/spec/") ||
@@ -2,9 +2,11 @@
2
2
  * Shared file path utilities for pi-lens
3
3
  */
4
4
  import * as fs from "node:fs";
5
+ import { createHash } from "node:crypto";
5
6
  import * as os from "node:os";
6
7
  import * as path from "node:path";
7
8
  import { Minimatch } from "./deps/minimatch.js";
9
+ import { detectFileRole } from "./file-role.js";
8
10
  import { FRESHNESS_CADENCE_MS } from "./freshness-cadence.js";
9
11
  import { isInSpawnTimeoutCooldown, noteSpawnTimeout, } from "./spawn-timeout-cooldown.js";
10
12
  import { collectTrackedFiles, getTrackedFilesSnapshot, } from "./git-tracked-ignore.js";
@@ -22,7 +24,7 @@ import { safeSpawnAsync } from "./safe-spawn.js";
22
24
  * Override: set PILENS_DATA_DIR=/some/path — each project gets its own
23
25
  * subdirectory named after a sanitized form of its absolute path, e.g.
24
26
  * PILENS_DATA_DIR=~/.pi-lens/projects
25
- * → ~/.pi-lens/projects/home-user-myapp/
27
+ * → ~/.pi-lens/projects/home-user-myapp-<8-hex-hash>/
26
28
  *
27
29
  * This keeps project folders clean and avoids creating .pi-lens folders
28
30
  * inside user projects.
@@ -34,14 +36,150 @@ export function getProjectDataDir(cwd) {
34
36
  return legacyProjectDir;
35
37
  }
36
38
  const base = configuredBase || path.join(getGlobalPiLensDir(), "projects");
37
- const normalized = normalizeFilePath(path.resolve(cwd));
38
- const slug = normalized
39
+ const resolvedBase = base.trim();
40
+ const memoKey = `${resolvedBase}\0${path.resolve(cwd)}`;
41
+ const cached = settledDataDirs.get(memoKey);
42
+ if (cached !== undefined)
43
+ return cached;
44
+ const canonical = canonicalProjectRoot(cwd);
45
+ const readable = projectDataDirReadableSlug(canonical.root);
46
+ const hash = projectDataDirRootHash(canonical.root);
47
+ const slug = `${readable || "default"}-${hash}`;
48
+ const dir = path.join(resolvedBase, slug);
49
+ const settled = settleProjectDataDir(resolvedBase, readable || "default", dir, memoKey, canonical.fallback);
50
+ return settled;
51
+ }
52
+ /**
53
+ * The human-readable half of the project data-dir slug: the absolute root
54
+ * with separators folded to dashes and anything else stripped.
55
+ *
56
+ * #2874: this form ALONE is not an identity — `src/pi-lens` and
57
+ * `src/pi/lens` fold to the same string. It is only ever combined with
58
+ * {@link projectDataDirRootHash}. Never use it as a directory name alone.
59
+ */
60
+ function projectDataDirReadableSlug(canonicalRoot) {
61
+ const normalized = normalizeFilePath(canonicalRoot);
62
+ return normalized
39
63
  .replace(/^[a-z]:/i, "") // strip Windows drive letter
40
64
  .replace(/\/+/g, "-") // separators → dashes
41
65
  .replace(/[^A-Za-z0-9-]/g, "") // strip anything else
42
66
  .replace(/^-+/, "") // trim leading dashes
43
67
  .replace(/-+$/, ""); // trim trailing dashes
44
- return path.join(base.trim(), slug || "default");
68
+ }
69
+ /**
70
+ * The canonical root is the resolved absolute path for both slug halves.
71
+ * `realpathSync` is probed only to expose a fallback boundary failure; using
72
+ * its result would let one root spelling change identity when the boundary
73
+ * recovers. The hash is the first 8 hex chars of SHA-256 over that root. A
74
+ * collision needs the same readable slug and the same 32-bit prefix, so the
75
+ * practical collision population is readable-slug twin pairs. Pinned on
76
+ * purpose because changing it renames every directory.
77
+ */
78
+ function canonicalProjectRoot(cwd) {
79
+ const resolved = path.resolve(cwd);
80
+ try {
81
+ // Probe the root, but keep the resolved spelling as the identity in both
82
+ // success and fallback cases. Resolving only on success would let a
83
+ // transient boundary failure choose a second data directory.
84
+ fs.realpathSync(resolved);
85
+ }
86
+ catch {
87
+ // Best-effort: the root may not exist yet.
88
+ return { root: resolved, fallback: true };
89
+ }
90
+ return { root: resolved, fallback: false };
91
+ }
92
+ function projectDataDirRootHash(canonicalRoot) {
93
+ return createHash("sha256").update(canonicalRoot).digest("hex").slice(0, 8);
94
+ }
95
+ const pendingDataDirMigrations = [];
96
+ const settledDataDirs = new Map();
97
+ export function resetProjectDataDirSessionState() {
98
+ pendingDataDirMigrations.splice(0);
99
+ }
100
+ /**
101
+ * One-time upgrade from a pre-#2874 slug directory to its hashed name.
102
+ * When the old directory exists and the new one does not, the state moves
103
+ * with one atomic `renameSync` (same parent, so same filesystem). When both
104
+ * exist the new one wins and nothing moves. Either case queues one
105
+ * {@link ProjectDataDirMigration} for the session-start drain, which emits
106
+ * the `data_dir_migrated` record. Best-effort throughout: on any failure the
107
+ * caller gets the pre-existing directory and behavior is unchanged.
108
+ *
109
+ * No ledger import here on purpose: `probe-home-state.ts` documents that a
110
+ * direct edge between this module and `degradation-ledger.ts` in either
111
+ * direction adds `no-client-cycles` violations, so the record is emitted by
112
+ * the drain site in `runtime-session.ts` instead.
113
+ */
114
+ function settleProjectDataDir(base, oldSlug, dir, memoKey, identityFallback) {
115
+ const oldDir = path.join(base, oldSlug);
116
+ const newExists = fs.existsSync(dir);
117
+ const oldExists = fs.existsSync(oldDir);
118
+ if (!oldExists) {
119
+ if (identityFallback && pendingDataDirMigrations.length < 32) {
120
+ pendingDataDirMigrations.push({
121
+ to: dir,
122
+ outcome: "identity-fallback",
123
+ });
124
+ }
125
+ // Steady state: nothing to migrate from.
126
+ settledDataDirs.set(memoKey, dir);
127
+ return dir;
128
+ }
129
+ if (!newExists) {
130
+ try {
131
+ fs.renameSync(oldDir, dir);
132
+ }
133
+ catch (error) {
134
+ if (error.code === "ENOENT") {
135
+ if (fs.existsSync(dir)) {
136
+ settledDataDirs.set(memoKey, dir);
137
+ return dir;
138
+ }
139
+ if (fs.existsSync(oldDir))
140
+ return oldDir;
141
+ return dir;
142
+ }
143
+ // Best-effort: retain the old directory and publish one bounded
144
+ // failure notice for this process's memo key.
145
+ if (pendingDataDirMigrations.length < 32) {
146
+ pendingDataDirMigrations.push({
147
+ to: dir,
148
+ outcome: "rename-failed",
149
+ });
150
+ }
151
+ settledDataDirs.set(memoKey, oldDir);
152
+ return oldDir;
153
+ }
154
+ }
155
+ // Either the rename just moved the state, or both directories already
156
+ // existed and the new one wins. Queue one migration for the drain.
157
+ settledDataDirs.set(memoKey, dir);
158
+ if (pendingDataDirMigrations.length < 32) {
159
+ pendingDataDirMigrations.push({
160
+ to: dir,
161
+ outcome: newExists ? "coexisting" : "renamed",
162
+ });
163
+ }
164
+ return dir;
165
+ }
166
+ /**
167
+ * Drain the queued {@link ProjectDataDirMigration}s, clearing the queue.
168
+ * Called once per session start by `handleSessionStart`; each queued outcome is
169
+ * therefore recorded at most once per session. Test-only callers must not rely
170
+ * on queue depth across drains.
171
+ */
172
+ export function drainProjectDataDirMigrations() {
173
+ return pendingDataDirMigrations.splice(0);
174
+ }
175
+ /**
176
+ * Test-only reset for the settled-directory memo. The memo is a pure
177
+ * performance cache (one `existsSync` pair saved per latched directory), so
178
+ * clearing it changes no observable behavior — it lets a test stage an
179
+ * old-slug directory AFTER learning the new name, then resolve again.
180
+ */
181
+ export function _resetProjectDataDirMemoForTests() {
182
+ settledDataDirs.clear();
45
183
  }
46
184
  /**
47
185
  * The project-data path to SHOW an agent or a human, for a file that
@@ -167,6 +305,14 @@ export const EXCLUDED_DIRS = [
167
305
  ".next",
168
306
  ".pi-lens",
169
307
  ".pi", // pi agent directory
308
+ // The SAME agent, rebranded: pi derives its config-dir name as
309
+ // `pkg.piConfig?.configDir || ".pi"` (pi-coding-agent 0.85.1,
310
+ // `dist/bundle/chunks/chunk-JVUZSMYM.js`), so a redistribution spells its
311
+ // `<configDir>/agent/sessions` history `.omp/agent/sessions` instead.
312
+ // #3112: a project-local one was walked entry-by-entry and spent the whole
313
+ // startup-scan entry budget, so a five-file project reported
314
+ // `too-many-entries` and never warmed its caches.
315
+ ".omp",
170
316
  ".ruff_cache", // Python linter cache
171
317
  ".worktrees",
172
318
  ".claude",
@@ -871,15 +1017,15 @@ export async function detectFileChangedAfterCommand(filePath, command, args, cwd
871
1017
  * Check if file path is a test/fixture/mock file.
872
1018
  * Used by secrets scanner, rate command, and dispatch runners
873
1019
  * to skip these files (false positives on fake credentials, etc).
1020
+ *
1021
+ * Delegate naming/directory classification to file-role (#2928). Fixtures,
1022
+ * mocks and test helpers remain this skip gate's separate policy.
874
1023
  */
875
1024
  export function isTestFile(filePath) {
1025
+ if (detectFileRole(filePath) === "test")
1026
+ return true;
876
1027
  const normalized = filePath.replace(/\\/g, "/");
877
- return (normalized.includes(".test.") ||
878
- normalized.includes(".spec.") ||
879
- normalized.includes("/test/") ||
880
- normalized.includes("/tests/") ||
881
- normalized.includes("__tests__/") ||
882
- normalized.includes("test-utils") ||
1028
+ return (normalized.includes("test-utils") ||
883
1029
  normalized.startsWith("test-") ||
884
1030
  normalized.includes(".fixture.") ||
885
1031
  normalized.includes(".mock."));
@@ -270,8 +270,17 @@ export const DELIVERY_SURFACES = {
270
270
  // drifting edit already re-touched the file — a fresh pending pair
271
271
  // supersedes this one), with both drop arms counted in the
272
272
  // `late_auxiliary_findings` latency record rather than silenced.
273
+ //
274
+ // #3102: freshness is only half the gate. The survivors also take the shared
275
+ // `clients/dispatch/finding-policy.ts` stack (inline `pi-lens-ignore` →
276
+ // stored dispositions → `.pi-lens.json` rule policy) the per-edit dispatcher,
277
+ // `mode=full` and the `source=lsp` probe lane apply — without it a finding
278
+ // the agent marked `false-positive` re-reported on every drain. Per #3088
279
+ // round-2 F4, the evidence is the CALL TEXT of that filter, which exists
280
+ // nowhere else in this file: a bare `applyFindingPolicy` would also be
281
+ // satisfied by an import line or an unrelated caller.
273
282
  "runtime-turn:late-auxiliary-findings": gated(RUNTIME_TURN_FILE, "Turn-end late-auxiliary LSP findings (collect-later probe of aux " +
274
- "client caches whose grace window expired).", ["gateFindingsByPathFreshness"], ['store: "late-auxiliary-findings"']),
283
+ "client caches whose grace window expired).", ["gateFindingsByPathFreshness", "applyFindingPolicy"], ['store: "late-auxiliary-findings"', "applyFindingPolicy(gate.live, {"]),
275
284
  "runtime-turn:late-runner-findings": gated(RUNTIME_TURN_FILE, "Turn-end CLI runner findings collected after the post-write path.", ["gateFindingsByPathFreshness"], ['store: "late-runner-findings"'], { evidenceMin: 2 }),
276
285
  "runtime-turn:cascade-blocker": labeled(RUNTIME_TURN_FILE, "Turn-end 🧪 cascade neighbor blocker.", "Cascade results settle synchronously this turn where possible " +
277
286
  "(`settleCascadeRuns`), but an unsettled compute can carry over to a " +
@@ -289,6 +298,37 @@ export const DELIVERY_SURFACES = {
289
298
  status: "partial",
290
299
  partialReason: "Same cascade carry-over caveat as runtime-turn:cascade-blocker.",
291
300
  }),
301
+ // #3102: the cold-neighbour cascade run is BUILT here, in the quiet-window
302
+ // reconcile (`onResolvedFound` in index.ts), a turn earlier than the
303
+ // `runtime-turn:cascade-blocker` render that finally carries it — so it is
304
+ // its own delivery lane, and it was the one that rendered raw LSP findings
305
+ // with no policy filter at all. The evidence is the CALL TEXT of the shared
306
+ // filter (#3088 round-2 F4): a bare `applyFindingPolicy` would also be
307
+ // satisfied by the import line.
308
+ "cascade-format:resolved-found-run": gated("clients/cascade-format.ts", "Cold-neighbour ERROR diagnostics formatted into the turn-end cascade " +
309
+ "run by `buildResolvedFoundCascadeRun` (quiet-window reconcile of a " +
310
+ "cascade touch that skipped its in-lane wait).", ["applyFindingPolicy"], ["applyFindingPolicy(retained, {"]),
311
+ // #3157: the IN-LANE cascade — a different delivery lane from the
312
+ // quiet-window run above, and the one #3102's sweep cleared WRONGLY. Its four
313
+ // display sites (passive cold snapshot, fresh touch, touch-error fallback,
314
+ // degraded fallback) assemble `CascadeNeighborResult.diagnostics` and reach
315
+ // the agent through `formatCascadeNeighborDiagnostics` without ever entering
316
+ // the dispatcher's `applyOutputFilters` pipeline: the sweep's file-level
317
+ // verdict ("the per-edit dispatch path, which already filters in
318
+ // dispatcher.ts") was true only of the per-edit RUNNER output in
319
+ // `runners/lsp.ts`, which is a different call site in a different file.
320
+ //
321
+ // Evidence is the CALL TEXT (#3088 round-2 F4), counted: `cascadeDisplay(` is
322
+ // the one seam all four sites route through, so `evidenceMin: 4` is the count
323
+ // of display sites in the file and dropping ANY of them back to a raw
324
+ // `convertLspDiagnostics` reds this suite. Identity-stubbing the callee
325
+ // instead is caught behaviourally — it reds twelve cases in
326
+ // `tests/clients/inlane-cascade-finding-policy.test.ts`, which is a stronger
327
+ // signal than the R2 proximity heuristic. Residual, stated: a FIFTH display
328
+ // site that open-codes the conversion keeps the count at 4 and is caught by
329
+ // review, not by this row.
330
+ "dispatch-integration:in-lane-cascade": gated("clients/dispatch/integration.ts", "In-lane per-edit cascade neighbour ERROR diagnostics, rendered into the " +
331
+ "turn-end cascade block by `computeCascadeForFile`.", ["applyCascadeDisplayPolicy"], ["cascadeDisplay("], { evidenceMin: 4 }),
292
332
  "runtime-turn:call-graph-advisory": labeled(RUNTIME_TURN_FILE, "Turn-end 📊 call-graph impact advisory.", "`runtime.callGraph` is a session-lifetime in-memory structure; this " +
293
333
  "round does not verify or gate ITS OWN freshness policy (how/when it " +
294
334
  "incorporates edits made earlier in the session).", "live", [], {
@@ -296,13 +336,15 @@ export const DELIVERY_SURFACES = {
296
336
  partialReason: "call-graph freshness/invalidation policy is out of this PR's scope — " +
297
337
  "tracked as a follow-up, not silently assumed fresh.",
298
338
  }),
299
- "lens-diagnostics:mode-full": gated(LENS_DIAGNOSTICS_FILE, "`lens_diagnostics mode=full` report.", [
300
- "applyDispositions",
301
- "applyRulePolicy",
302
- "reconcileProjectDiagnosticsSnapshot",
303
- ], [
304
- "applyDispositions(",
305
- "applyRulePolicy(",
339
+ // #3088 round 2 F4: this row used to name `applyDispositions`/`applyRulePolicy`
340
+ // with whole-file evidence literals. Once #3088 folded mode=full's stack onto
341
+ // `applyFindingPolicy`, those two literals survived only in UNRELATED
342
+ // mode=delta helpers in the same file, so deleting the entire policy stack out
343
+ // of `applyInlineSuppressionsToSummaries` left this gate — and the ratchet —
344
+ // green while five behaviour cases redded. The evidence is now the call text
345
+ // of the merge's own filter, which exists nowhere else in the file.
346
+ "lens-diagnostics:mode-full": gated(LENS_DIAGNOSTICS_FILE, "`lens_diagnostics mode=full` report.", ["applyFindingPolicy", "reconcileProjectDiagnosticsSnapshot"], [
347
+ "applyFindingPolicy(summary.diagnostics, {",
306
348
  "reconcileProjectDiagnosticsSnapshot(",
307
349
  ]),
308
350
  "lens-diagnostics:mode-all": gated(LENS_DIAGNOSTICS_FILE, "`lens_diagnostics mode=all` report (widget-state read).", ["reconcileStaleWidgetFiles", "reconcileStaleWidgetDependencyBlockers"], ["reconcileStaleWidgetFiles(", "reconcileStaleWidgetDependencyBlockers("]),
@@ -342,13 +384,18 @@ export const DELIVERY_SURFACES = {
342
384
  "the shared workspace-diagnostics cache: createWorkspaceDiagnosticsCacheContext " +
343
385
  "is the tool's entry, and its lookup() applies the #671/#672 freshness stack " +
344
386
  "(own-file mtime + reverse-dependency mtimes via isEntryFresh) plus the " +
345
- "#1095 content binding.", [
387
+ "#1095 content binding. #3088: every route out of this tool — fresh, " +
388
+ "cache replay, single file, directory — also passes applyProbeFindingPolicy, " +
389
+ "the stored-disposition / .pi-lens.json rule-policy / inline pi-lens-ignore " +
390
+ "stack the per-edit dispatch path and mode=full apply.", [
346
391
  "createWorkspaceDiagnosticsCacheContext",
347
392
  "isEntryFresh",
348
393
  "cacheCtx.lookup",
394
+ "applyProbeFindingPolicy",
349
395
  ], [
350
396
  "createWorkspaceDiagnosticsCacheContext(resolvedCwd)",
351
397
  "cacheCtx.lookup(file, scopeKey)",
398
+ "applyProbeFindingPolicy(",
352
399
  ]),
353
400
  "git-guard:commit-blocked": labeled("clients/git-guard.ts", "Git-guard commit/push 🔴 COMMIT BLOCKED verdict (--lens-guard).", "Synchronous preflight rejection returned inline with the failed git " +
354
401
  "command — no stored state is delivered, so nothing can go stale between " +
@@ -15,6 +15,7 @@ import * as path from "node:path";
15
15
  import { recordFormatter } from "./widget-state.js";
16
16
  import { FileTime } from "./file-time.js";
17
17
  import { loadFormatters } from "./formatters-lazy.js";
18
+ import { bounded } from "./deadline-utils.js";
18
19
  // --- Configuration ---
19
20
  /** Reserved for future batching; formatting currently runs one selected formatter per file. */
20
21
  const DEFAULT_FORMATTER_CONCURRENCY = 1;
@@ -69,7 +70,7 @@ export class FormatService {
69
70
  };
70
71
  }
71
72
  // Run formatters with limited concurrency
72
- const results = await this.runFormattersWithConcurrency(absolutePath, formatters);
73
+ const results = await this.runFormattersWithConcurrency(absolutePath, formatters, DEFAULT_FORMATTER_CONCURRENCY, options.signal, options.budgetMs, options.hook);
73
74
  // Record new file state after formatting
74
75
  this.fileTime.read(absolutePath);
75
76
  for (const [index, result] of results.entries()) {
@@ -84,7 +85,7 @@ export class FormatService {
84
85
  name: formatters[i].name,
85
86
  success: r.success,
86
87
  changed: r.changed,
87
- error: r.error,
88
+ ...(r.error === undefined ? {} : { error: r.error }),
88
89
  outcome: r.outcome,
89
90
  })),
90
91
  anyChanged,
@@ -94,25 +95,46 @@ export class FormatService {
94
95
  /**
95
96
  * Run the selected formatter with timeout protection.
96
97
  */
97
- async runFormattersWithConcurrency(filePath, formatters, _concurrency = DEFAULT_FORMATTER_CONCURRENCY) {
98
+ async runFormattersWithConcurrency(filePath, formatters, _concurrency = DEFAULT_FORMATTER_CONCURRENCY, signal, budgetMs = 30_000, hook = "tool_result_edit") {
98
99
  const results = [];
100
+ const startedAt = Date.now();
99
101
  for (const formatter of formatters) {
100
- // #1097: keep the timer handle so it can be cleared once the race
101
- // settles. An uncleared, REF'D 30s setTimeout that outlives a fast
102
- // `formatFile` win would keep a one-shot `pi --print` process alive for
103
- // up to 30s after completion (same uncleared-race-timeout class as the
104
- // LSP client-wait leak fixed in clients/lsp/index.ts).
105
- let formatTimer;
102
+ const remainingMs = budgetMs - (Date.now() - startedAt);
103
+ if (remainingMs <= 0) {
104
+ results.push({
105
+ success: false,
106
+ changed: false,
107
+ outcome: "failed",
108
+ error: `Formatter aggregate budget exhausted before ${formatter.name}`,
109
+ });
110
+ continue;
111
+ }
112
+ // The shared bound owns both the aggregate timer and abort listener.
113
+ // This keeps one total formatter budget per file instead of re-arming a
114
+ // 30s timer for every formatter.
106
115
  try {
107
- const timeoutMs = 30000;
108
- const timeoutPromise = new Promise((_, reject) => {
109
- formatTimer = setTimeout(() => reject(new Error(`Formatter ${formatter.name} timed out after ${timeoutMs}ms`)), timeoutMs);
116
+ const result = await bounded(loadFormatters().then(({ formatFile }) => formatFile(filePath, formatter)), {
117
+ ms: remainingMs,
118
+ signal,
119
+ hook,
120
+ label: "formatter-aggregate",
110
121
  });
111
- const result = await Promise.race([
112
- loadFormatters().then(({ formatFile }) => formatFile(filePath, formatter)),
113
- timeoutPromise,
114
- ]);
115
- results.push(result);
122
+ if (result)
123
+ results.push(result);
124
+ else if (signal?.aborted) {
125
+ // Caller cancellation is intentional. Do not turn Escape into a
126
+ // formatter failure or requeue record.
127
+ break;
128
+ }
129
+ else {
130
+ results.push({
131
+ success: false,
132
+ changed: false,
133
+ outcome: "failed",
134
+ error: `Formatter ${formatter.name} exceeded aggregate budget`,
135
+ });
136
+ break;
137
+ }
116
138
  }
117
139
  catch (error) {
118
140
  // A timeout or thrown error here is a real failure (#2413): the
@@ -125,10 +147,6 @@ export class FormatService {
125
147
  error: error instanceof Error ? error.message : String(error),
126
148
  });
127
149
  }
128
- finally {
129
- if (formatTimer)
130
- clearTimeout(formatTimer);
131
- }
132
150
  }
133
151
  return results;
134
152
  }