@herbertgao/pi-extensions 2026.8.3 → 2026.8.5

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 (408) hide show
  1. package/README.md +45 -4
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  31. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  32. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  33. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  34. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  35. package/node_modules/pi-footer/LICENSE +21 -0
  36. package/node_modules/pi-footer/README.md +354 -0
  37. package/node_modules/pi-footer/package.json +68 -0
  38. package/node_modules/pi-footer/src/cache.ts +106 -0
  39. package/node_modules/pi-footer/src/colors.ts +237 -0
  40. package/node_modules/pi-footer/src/config.ts +202 -0
  41. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  42. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  43. package/node_modules/pi-footer/src/git.ts +135 -0
  44. package/node_modules/pi-footer/src/index.ts +268 -0
  45. package/node_modules/pi-footer/src/metrics.ts +141 -0
  46. package/node_modules/pi-footer/src/presets.ts +383 -0
  47. package/node_modules/pi-footer/src/render.ts +114 -0
  48. package/node_modules/pi-footer/src/separators.ts +52 -0
  49. package/node_modules/pi-footer/src/types.ts +122 -0
  50. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  51. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  52. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  53. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  54. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  55. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  56. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  57. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  58. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  59. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  60. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  61. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  62. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  63. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  64. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  65. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  66. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  67. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  68. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  69. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  70. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  71. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  72. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  73. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  74. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  75. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  76. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  77. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  78. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  79. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  80. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  81. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  82. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  83. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  84. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  85. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  86. package/node_modules/pi-footer/src/ui.ts +66 -0
  87. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  88. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  89. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  90. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  91. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  92. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  93. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  94. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  95. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  96. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  97. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  99. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  100. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  102. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  103. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  104. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  105. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  106. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  107. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  108. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  109. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  110. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  111. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  112. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  113. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  114. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  115. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  116. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  117. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  118. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  119. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  120. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  121. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  122. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  123. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  124. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  125. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  126. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  127. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  128. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  129. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  130. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  131. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  132. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  133. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  134. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  135. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  136. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  137. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  138. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  139. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  140. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  141. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  142. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  143. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  144. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  145. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  146. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  147. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  148. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  149. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  150. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  151. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  152. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  153. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  154. package/node_modules/pi-lens/README.md +13 -0
  155. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  156. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  157. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  158. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  160. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  161. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  162. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  163. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  164. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  165. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  166. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  167. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  168. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  169. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  170. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  171. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  172. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  173. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  174. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  175. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  177. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  178. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  179. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  180. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  181. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  182. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  183. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  184. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  185. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  186. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  187. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  188. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  189. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  190. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  191. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  192. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  193. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  194. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  195. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  196. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  197. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  198. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  199. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  201. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  202. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  203. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  204. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  205. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  206. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  207. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  208. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  209. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  210. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  212. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  213. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  214. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  215. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  216. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  217. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  218. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  219. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  220. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  221. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  222. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  223. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  224. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  225. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  226. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  227. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  228. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  229. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  230. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  231. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  232. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  233. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  234. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  235. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  236. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  237. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  238. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  239. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  240. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  241. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  242. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  243. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  244. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  245. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  246. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  247. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  248. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  249. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  250. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  251. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  252. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  253. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  254. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  255. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  256. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  257. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  258. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  259. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  260. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  261. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  262. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  263. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  264. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  265. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  266. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  267. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  268. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  269. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  270. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  271. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  272. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  273. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  274. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  275. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  277. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  278. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  279. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  280. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  281. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  282. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  283. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  284. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  285. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  286. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  287. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  288. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  289. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  290. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  291. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  292. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  293. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  294. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  295. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  296. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  297. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  298. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  299. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  300. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  301. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  302. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  303. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  304. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  305. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  306. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  307. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  308. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  309. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  310. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  311. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  312. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  313. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  314. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  315. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  316. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  317. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  318. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  319. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  320. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  321. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  322. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  323. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  324. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  325. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  326. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  327. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  328. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  329. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  330. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  331. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  332. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  333. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  334. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  335. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  336. package/node_modules/pi-lens/dist/index.js +49361 -36888
  337. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  338. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  339. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  340. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  341. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  342. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  343. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  344. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  345. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  346. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  347. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  348. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  349. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  350. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  351. package/node_modules/pi-lens/docs/fable.md +27 -0
  352. package/node_modules/pi-lens/docs/features.md +31 -2
  353. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  354. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  355. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  356. package/node_modules/pi-lens/docs/mcp.md +37 -4
  357. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  358. package/node_modules/pi-lens/docs/settings.md +12 -1
  359. package/node_modules/pi-lens/docs/tools.md +4 -1
  360. package/node_modules/pi-lens/docs/usage.md +18 -0
  361. package/node_modules/pi-lens/package.json +10 -10
  362. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  363. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  364. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  365. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  366. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  367. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  368. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  369. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  370. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  371. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  372. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  373. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  374. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  375. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  376. package/node_modules/pi-mcp-adapter/README.md +34 -3
  377. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  378. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  379. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  380. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  381. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  382. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  383. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  384. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  385. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  386. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  387. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  388. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  389. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  390. package/node_modules/pi-mcp-adapter/package.json +3 -3
  391. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  392. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  393. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  394. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  395. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  396. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  397. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  398. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  399. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  400. package/package.json +17 -13
  401. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  402. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  403. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  404. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  405. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  406. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  407. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  408. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -8,14 +8,22 @@
8
8
  *
9
9
  * Inspired by OpenCode's formatter.ts pattern
10
10
  */
11
+ import { logExtension } from "./extension-log.js";
11
12
  import * as fs from "node:fs/promises";
12
13
  import * as path from "node:path";
14
+ import { BoundedLruCache } from "./bounded-cache.js";
15
+ import { normalizeMapKey } from "./path-utils.js";
16
+ import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
17
+ import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
13
18
  import { logLatency } from "./latency-logger.js";
14
19
  import { findGlobalBinary } from "./package-manager.js";
15
20
  import { safeSpawnAsync } from "./safe-spawn.js";
16
- import { getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOxfmtConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasVitePlusConfig, } from "./tool-policy.js";
21
+ import { assertInstallAllowed } from "./project-trust.js";
22
+ import { getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
17
23
  const _lazyInstallAttempts = new Set();
18
24
  export async function tryLazyInstallFormatterTool(tool, cwd) {
25
+ if (!assertInstallAllowed(`formatter lazy install: ${tool}`))
26
+ return false;
19
27
  const attemptKey = `${tool}:${cwd}`;
20
28
  if (_lazyInstallAttempts.has(attemptKey))
21
29
  return false;
@@ -28,7 +36,11 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
28
36
  });
29
37
  const ok = !res.error && res.status === 0;
30
38
  if (!ok) {
31
- console.error(`[format] lazy-install rubocop failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`);
39
+ logExtension({
40
+ subsystem: "format",
41
+ message: `lazy-install rubocop failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
42
+ metadata: { tool: "rubocop", cwd },
43
+ });
32
44
  }
33
45
  return ok;
34
46
  }
@@ -39,10 +51,23 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
39
51
  });
40
52
  const ok = !res.error && res.status === 0;
41
53
  if (!ok) {
42
- console.error(`[format] lazy-install rustfmt failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`);
54
+ logExtension({
55
+ subsystem: "format",
56
+ message: `lazy-install rustfmt failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
57
+ metadata: { tool: "rustfmt", cwd },
58
+ });
43
59
  }
44
60
  return ok;
45
61
  }
62
+ // --- Types ---
63
+ /**
64
+ * Sentinel a `resolveCommand` returns to mean "do not format this file at
65
+ * all" — distinct from `null`, which means "fall back to the static
66
+ * `command`". #1144's style-preserving resolvers return this when the repo
67
+ * has no config AND the file's indentation is undetectable: running the
68
+ * stock command there is exactly the stock-style imposition the fix bans.
69
+ */
70
+ export const SKIP_FORMATTING = "skip-formatting";
46
71
  // --- Utility Functions ---
47
72
  async function fileExists(filePath) {
48
73
  try {
@@ -189,13 +214,23 @@ async function resolveManagedSmartDefaultCommand(formatterName, filePath, args)
189
214
  return null;
190
215
  return [installed, ...args, filePath];
191
216
  }
192
- function hasExplicitFormatterConfig(formatterName, cwd) {
217
+ function hasExplicitFormatterConfig(formatterName, cwd, ext) {
193
218
  switch (formatterName) {
194
219
  case "biome":
195
220
  return hasBiomeConfig(cwd);
196
221
  case "prettier":
197
222
  return (hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier"));
198
223
  case "oxfmt":
224
+ // .svelte is conditional beyond "an oxfmt config exists": oxfmt
225
+ // requires the `svelte` package installed AND the config's `svelte`
226
+ // flag enabled (verified empirically — see hasOxfmtSvelteConfig).
227
+ // The generic checks below are NOT sufficient for .svelte — an
228
+ // oxfmt.toml with no svelte flag, or an oxfmt dependency alone,
229
+ // both fail at runtime for .svelte specifically (other extensions
230
+ // are unaffected by this stricter gate).
231
+ if (ext === ".svelte") {
232
+ return hasOxfmtSvelteConfig(cwd);
233
+ }
199
234
  return (hasOxfmtConfig(cwd) ||
200
235
  hasVitePlusConfig(cwd) ||
201
236
  // The published package is `oxfmt`; `@oxc-project/oxfmt` does not
@@ -224,6 +259,8 @@ function hasExplicitFormatterConfig(formatterName, cwd) {
224
259
  return hasGoogleJavaFormatConfig(cwd);
225
260
  case "ktfmt":
226
261
  return hasKtfmtConfig(cwd);
262
+ case "ktlint":
263
+ return hasKtlintConfig(cwd);
227
264
  case "cljfmt":
228
265
  return hasCljfmtConfig(cwd);
229
266
  case "cmake-format":
@@ -242,28 +279,94 @@ async function hasEditorConfig(cwd) {
242
279
  return false;
243
280
  }
244
281
  }
282
+ async function indentationArgs(filePath, tool, cwd) {
283
+ if (await hasEditorConfig(cwd) || hasExplicitFormatterConfig(tool, cwd, path.extname(filePath))) {
284
+ return [];
285
+ }
286
+ let content;
287
+ try {
288
+ content = await fs.readFile(filePath, "utf8");
289
+ }
290
+ catch {
291
+ // Resolution tests and callers may probe a path before it exists.
292
+ return [];
293
+ }
294
+ if (!hasDetectableIndentation(content))
295
+ return null;
296
+ const indentation = detectIndentation(content);
297
+ if (tool === "shfmt")
298
+ return indentation.style === "tab" ? ["-i", "0"] : ["-i", String(indentation.width)];
299
+ if (tool === "prettier") {
300
+ return [indentation.style === "tab" ? "--use-tabs" : "--no-use-tabs", "--tab-width", String(indentation.width)];
301
+ }
302
+ if (tool === "ruff") {
303
+ // `ruff format` has NO --indent-style/--indent-width flags (it errors with
304
+ // "unexpected argument" and exits 2, which formatFile reported as a silent
305
+ // clean no-op back when exit-code strictness was opt-in). Style is pinned
306
+ // through inline TOML overrides instead (#1144 follow-up).
307
+ return [
308
+ "--config",
309
+ `indent-width=${indentation.width}`,
310
+ "--config",
311
+ `format.indent-style='${indentation.style}'`,
312
+ ];
313
+ }
314
+ return ["--indent-style", indentation.style, "--indent-width", String(indentation.width)];
315
+ }
316
+ /**
317
+ * Every biome invocation must carry this. Biome exits 1 with "No files were
318
+ * processed in the specified paths" when the path is ignored by the repo's own
319
+ * biome.json or carries an extension biome does not handle — a benign outcome
320
+ * that, under #1337's strict default, would otherwise report a formatting
321
+ * failure on every edit under an ignored directory. Verified against biome
322
+ * 2.4.12: ignored path → exit 1, +flag → exit 0; unsupported extension → exit
323
+ * 1, +flag → exit 0; and a real syntax error still exits 1 WITH the flag, so
324
+ * strictness is preserved for actual failures.
325
+ *
326
+ * `clients/dispatch/runners/biome-check.ts:108` already passes this on the lint
327
+ * path; the formatter path did not, which is what made biome the one
328
+ * misclassified formatter in the #1337 audit.
329
+ */
330
+ const BIOME_UNMATCHED_FLAG = "--no-errors-on-unmatched";
245
331
  export const biomeFormatter = {
246
332
  name: "biome",
247
- command: ["npx", "@biomejs/biome", "format", "--write", "$FILE"],
333
+ command: [
334
+ "npx",
335
+ "@biomejs/biome",
336
+ "format",
337
+ "--write",
338
+ BIOME_UNMATCHED_FLAG,
339
+ "$FILE",
340
+ ],
248
341
  async resolveCommand(filePath, cwd) {
249
342
  const editorConfigFlag = (await hasEditorConfig(cwd))
250
343
  ? ["--use-editorconfig=true"]
251
344
  : [];
345
+ const styleArgs = await indentationArgs(filePath, "biome", cwd);
346
+ if (styleArgs === null)
347
+ return SKIP_FORMATTING;
348
+ const args = [
349
+ "format",
350
+ "--write",
351
+ BIOME_UNMATCHED_FLAG,
352
+ ...editorConfigFlag,
353
+ ...styleArgs,
354
+ ];
252
355
  const local = await findInNodeModules("biome", cwd);
253
356
  if (local)
254
- return [local, "format", "--write", ...editorConfigFlag, filePath];
357
+ return [local, ...args, filePath];
255
358
  // Any package manager's global bin dir (npm/pnpm/yarn/bun) before we
256
359
  // auto-install — catches a `pnpm add -g @biomejs/biome` PATH misses (#375).
257
360
  const global = await findGlobalBinary("biome");
258
361
  if (global)
259
- return [global, "format", "--write", ...editorConfigFlag, filePath];
362
+ return [global, ...args, filePath];
260
363
  const toolId = getAutoInstallToolIdForFormatter("biome");
261
364
  if (!toolId)
262
365
  return null;
263
366
  const { ensureTool } = await import("./installer/index.js");
264
367
  const installed = await ensureTool(toolId);
265
368
  if (installed)
266
- return [installed, "format", "--write", ...editorConfigFlag, filePath];
369
+ return [installed, ...args, filePath];
267
370
  return null;
268
371
  },
269
372
  extensions: [
@@ -294,14 +397,18 @@ export const prettierFormatter = {
294
397
  name: "prettier",
295
398
  command: ["npx", "prettier", "--write", "$FILE"],
296
399
  async resolveCommand(filePath, cwd) {
400
+ const styleArgs = await indentationArgs(filePath, "prettier", cwd);
401
+ if (styleArgs === null)
402
+ return SKIP_FORMATTING;
403
+ const args = ["--write", ...styleArgs];
297
404
  const local = await findInNodeModules("prettier", cwd);
298
405
  if (local)
299
- return [local, "--write", filePath];
406
+ return [local, ...args, filePath];
300
407
  // Global bin of any manager (npm/pnpm/yarn/bun) before auto-install (#375).
301
408
  const global = await findGlobalBinary("prettier");
302
409
  if (global)
303
- return [global, "--write", filePath];
304
- return resolveManagedSmartDefaultCommand("prettier", filePath, ["--write"]);
410
+ return [global, ...args, filePath];
411
+ return resolveManagedSmartDefaultCommand("prettier", filePath, args);
305
412
  },
306
413
  extensions: [
307
414
  ".js",
@@ -338,6 +445,11 @@ export const prettierFormatter = {
338
445
  export const oxfmtFormatter = {
339
446
  name: "oxfmt",
340
447
  command: ["oxfmt", "$FILE"],
448
+ // #1337 audit: oxfmt (and the `vp fmt --write` path) publish no exit-code
449
+ // table. `--write` is the default and `--check` is the separate verification
450
+ // mode, so there is no documented nonzero-on-reformat. Absent a documented
451
+ // benign-nonzero mode, it stays strict — the safe direction, since the failure
452
+ // mode of guessing wrong the other way is a silent no-op (#1336).
341
453
  async resolveCommand(filePath, cwd) {
342
454
  if (hasVitePlusConfig(cwd)) {
343
455
  const localVp = await findInNodeModules("vp", cwd);
@@ -355,18 +467,10 @@ export const oxfmtFormatter = {
355
467
  return [found, filePath];
356
468
  return null;
357
469
  },
358
- extensions: [
359
- ".js", ".jsx", ".mjs", ".cjs",
360
- ".ts", ".tsx", ".mts", ".cts",
361
- ".vue",
362
- ".css", ".scss", ".less",
363
- ".html", ".htm",
364
- ".json", ".jsonc",
365
- ".yaml", ".yml",
366
- ".md", ".mdx",
367
- ".graphql", ".gql",
368
- ".toml",
369
- ],
470
+ // Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
471
+ // Do not hand-maintain a second copy of this list (#1134 — previously two
472
+ // parallel hand-maintained lists, the #883 single-source-of-truth class).
473
+ extensions: [...OXFMT_SUPPORTED_EXTENSIONS],
370
474
  async detect(cwd) {
371
475
  return (hasOxfmtConfig(cwd) ||
372
476
  hasVitePlusConfig(cwd) ||
@@ -379,17 +483,25 @@ export const ruffFormatter = {
379
483
  name: "ruff",
380
484
  command: ["ruff", "format", "$FILE"],
381
485
  extensions: [".py", ".pyi"],
486
+ // Strict (the #1337 default): `ruff format` exits 0 on a successful in-place
487
+ // rewrite and 2 on argument rejection / syntax error (verified, ruff 0.x:
488
+ // well-formed → 0, reformatted → 0, unparseable → 2). The exit-2 no-op is
489
+ // exactly what hid the bad --indent-style flags for a full release cycle.
382
490
  async resolveCommand(filePath, cwd) {
491
+ const styleArgs = await indentationArgs(filePath, "ruff", cwd);
492
+ if (styleArgs === null)
493
+ return SKIP_FORMATTING;
494
+ const args = ["format", ...styleArgs];
383
495
  const venv = await findInVenv("ruff", cwd);
384
496
  if (venv)
385
- return [venv, "format", filePath];
497
+ return [venv, ...args, filePath];
386
498
  const toolId = getAutoInstallToolIdForFormatter("ruff");
387
499
  if (!toolId)
388
500
  return null;
389
501
  const { ensureTool } = await import("./installer/index.js");
390
502
  const installed = await ensureTool(toolId);
391
503
  if (installed)
392
- return [installed, "format", filePath];
504
+ return [installed, ...args, filePath];
393
505
  return null;
394
506
  },
395
507
  async detect(cwd) {
@@ -420,6 +532,11 @@ export const sqlfluffFormatter = {
420
532
  name: "sqlfluff",
421
533
  command: ["sqlfluff", "fix", "--force", "$FILE"],
422
534
  extensions: [".sql"],
535
+ lenientExitCode: "lint-autofix: `sqlfluff fix` writes the corrected file and then exits 1 " +
536
+ "when unfixable violations remain (its documented exit codes are 0 = all " +
537
+ "clean, 1 = violations remain, 2 = command/config failure), so a nonzero " +
538
+ "exit routinely accompanies a successful rewrite.",
539
+ lenientStatuses: [1],
423
540
  async resolveCommand(filePath, cwd) {
424
541
  const venv = await findInVenv("sqlfluff", cwd);
425
542
  if (venv)
@@ -481,11 +598,14 @@ export const shfmtFormatter = {
481
598
  name: "shfmt",
482
599
  command: ["shfmt", "-w", "$FILE"],
483
600
  extensions: [".sh", ".bash"],
484
- async resolveCommand(filePath, _cwd) {
601
+ async resolveCommand(filePath, cwd) {
602
+ const styleArgs = await indentationArgs(filePath, "shfmt", cwd);
603
+ if (styleArgs === null)
604
+ return SKIP_FORMATTING;
485
605
  const inPath = await which("shfmt");
486
606
  if (inPath)
487
- return [inPath, "-w", filePath];
488
- return resolveManagedSmartDefaultCommand("shfmt", filePath, ["-w"]);
607
+ return [inPath, "-w", ...styleArgs, filePath];
608
+ return resolveManagedSmartDefaultCommand("shfmt", filePath, ["-w", ...styleArgs]);
489
609
  },
490
610
  async detect(_cwd) {
491
611
  if ((await which("shfmt")) !== null)
@@ -497,6 +617,9 @@ export const shfmtFormatter = {
497
617
  export const nixfmtFormatter = {
498
618
  name: "nixfmt",
499
619
  command: ["nixfmt", "$FILE"],
620
+ // #1337 audit: nixfmt's README documents in-place formatting but has no
621
+ // exit-code section and no change-detection mode, so nothing documents a
622
+ // benign nonzero. Strict by default (see oxfmt above for the rationale).
500
623
  extensions: [".nix"],
501
624
  async detect(_cwd) {
502
625
  return (await which("nixfmt")) !== null;
@@ -540,6 +663,11 @@ export const ktlintFormatter = {
540
663
  name: "ktlint",
541
664
  command: ["ktlint", "-F", "$FILE"],
542
665
  extensions: [".kt", ".kts"],
666
+ lenientExitCode: "lint-autofix: `ktlint -F` autocorrects what it can and then exits 1 if " +
667
+ "any lint error remains unfixed — the documented CLI contract (ktlint " +
668
+ "exits nonzero whenever violations are reported, and -F does not suppress " +
669
+ "the ones it cannot correct).",
670
+ lenientStatuses: [1],
543
671
  async resolveCommand(filePath, _cwd) {
544
672
  const inPath = await which("ktlint");
545
673
  if (inPath)
@@ -576,6 +704,12 @@ export const rubocopFormatter = {
576
704
  name: "rubocop",
577
705
  command: ["rubocop", "-a", "--no-color", "$FILE"],
578
706
  extensions: [".rb", ".rake", ".gemspec", ".ru"],
707
+ lenientExitCode: "lint-autofix: `rubocop -a` exits 1 whenever ANY offense remains after it " +
708
+ "has already rewritten the file. Verified locally (rubocop on Ruby 3.4): a " +
709
+ "file with an unfixable Lint/UselessAssignment exits 1, and even a " +
710
+ "tidy file exits 1 on an unfixable Style/Documentation offense — nonzero " +
711
+ "is the normal outcome, not a failure.",
712
+ lenientStatuses: [1],
579
713
  async resolveCommand(filePath, cwd) {
580
714
  if (await canUseBundleExec(cwd))
581
715
  return ["bundle", "exec", "rubocop", "-a", "--no-color", filePath];
@@ -594,6 +728,9 @@ export const standardrbFormatter = {
594
728
  name: "standardrb",
595
729
  command: ["standardrb", "--fix", "$FILE"],
596
730
  extensions: [".rb", ".rake"],
731
+ lenientExitCode: "lint-autofix: standardrb is a RuboCop wrapper and inherits its exit " +
732
+ "contract — `--fix` exits 1 when offenses remain after the rewrite.",
733
+ lenientStatuses: [1],
597
734
  async resolveCommand(filePath, cwd) {
598
735
  if (await canUseBundleExec(cwd))
599
736
  return ["bundle", "exec", "standardrb", "--fix", filePath];
@@ -625,6 +762,18 @@ export const terraformFormatter = {
625
762
  return (await which("terraform")) !== null;
626
763
  },
627
764
  };
765
+ export const terragruntHclFormatter = {
766
+ name: "terragrunt-hcl",
767
+ command: ["terragrunt", "hcl", "fmt", "--file", "$FILE"],
768
+ extensions: [],
769
+ filenames: TERRAGRUNT_FILENAMES,
770
+ // Strict (the #1337 default): verified exit 0 on a successful in-place format
771
+ // against terragrunt v1.1.2. A binary predating the `hcl` command group exits
772
+ // nonzero and touches nothing, which unguarded reads as "already formatted".
773
+ async detect(_cwd) {
774
+ return (await which("terragrunt")) !== null;
775
+ },
776
+ };
628
777
  export const phpCsFixerFormatter = {
629
778
  name: "php-cs-fixer",
630
779
  command: ["php-cs-fixer", "fix", "$FILE"],
@@ -766,24 +915,44 @@ export const cmakeFormatFormatter = {
766
915
  return hasCmakeFormatConfig(cwd);
767
916
  },
768
917
  };
918
+ /**
919
+ * The PowerShell one-liner behind `psscriptanalyzer-format`.
920
+ *
921
+ * #1337 audit finding: without `$ErrorActionPreference = 'Stop'`, a failing
922
+ * `Invoke-Formatter` is a NON-TERMINATING error — pwsh still exits 0, so this
923
+ * formatter could never report a failure through the strict seam and the #1336
924
+ * silent-no-op class survived intact for `.ps1/.psm1/.psd1`. Verified: an
925
+ * `Invoke-Formatter` argument-validation failure exits 0 under the old script.
926
+ *
927
+ * Two more defects fixed here rather than left as landmines:
928
+ * - the old script ran `Set-Content -Value $formatted` even when `$formatted`
929
+ * was `$null`, which TRUNCATES the file it was asked to format;
930
+ * - single-quoted interpolation broke on any path containing an apostrophe.
931
+ * An empty/whitespace-only file exits 0 without touching anything, so "nothing
932
+ * to format" stays a clean no-op rather than a reported failure.
933
+ */
934
+ function psScriptAnalyzerCommand(filePath) {
935
+ // PowerShell single-quoted strings escape an apostrophe by doubling it.
936
+ const quoted = filePath.replace(/'/g, "''");
937
+ return [
938
+ "$ErrorActionPreference = 'Stop'",
939
+ `$p = '${quoted}'`,
940
+ "$content = Get-Content -Raw -LiteralPath $p",
941
+ "if ([string]::IsNullOrWhiteSpace($content)) { exit 0 }",
942
+ "$formatted = Invoke-Formatter -ScriptDefinition $content",
943
+ "if ($null -eq $formatted) { throw 'Invoke-Formatter returned no output' }",
944
+ "Set-Content -LiteralPath $p -Value $formatted",
945
+ ].join("; ");
946
+ }
769
947
  export const psscriptanalyzerFormatFormatter = {
770
948
  name: "psscriptanalyzer-format",
771
- command: [
772
- "pwsh",
773
- "-Command",
774
- "Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'",
775
- ],
949
+ command: ["pwsh", "-NoProfile", "-Command", psScriptAnalyzerCommand("$FILE")],
776
950
  extensions: [".ps1", ".psm1", ".psd1"],
777
951
  async resolveCommand(filePath, _cwd) {
778
952
  const pwsh = (await which("pwsh")) ?? (await which("powershell"));
779
953
  if (!pwsh)
780
954
  return null;
781
- return [
782
- pwsh,
783
- "-NoProfile",
784
- "-Command",
785
- `$content = Get-Content -Raw '${filePath}'; $formatted = Invoke-Formatter -ScriptDefinition $content; Set-Content -Path '${filePath}' -Value $formatted`,
786
- ];
955
+ return [pwsh, "-NoProfile", "-Command", psScriptAnalyzerCommand(filePath)];
787
956
  },
788
957
  async detect(_cwd) {
789
958
  const pwsh = (await which("pwsh")) ?? (await which("powershell"));
@@ -799,7 +968,11 @@ export const psscriptanalyzerFormatFormatter = {
799
968
  },
800
969
  };
801
970
  // --- Registry ---
802
- const ALL_FORMATTERS = [
971
+ // Exported for the formatter/policy drift guard (tests/clients/
972
+ // formatter-policy-consistency.test.ts, #1135): the test cross-checks these
973
+ // definitions against tool-policy.ts's FORMATTER_POLICY_BY_EXTENSION so the two
974
+ // hand-maintained inverse mappings can never silently diverge.
975
+ export const ALL_FORMATTERS = [
803
976
  biomeFormatter,
804
977
  prettierFormatter,
805
978
  oxfmtFormatter,
@@ -818,6 +991,7 @@ const ALL_FORMATTERS = [
818
991
  ktlintFormatter,
819
992
  ktfmtFormatter,
820
993
  terraformFormatter,
994
+ terragruntHclFormatter,
821
995
  phpCsFixerFormatter,
822
996
  csharpierFormatter,
823
997
  fantomasFormatter,
@@ -833,27 +1007,70 @@ const ALL_FORMATTERS = [
833
1007
  cmakeFormatFormatter,
834
1008
  psscriptanalyzerFormatFormatter,
835
1009
  ];
1010
+ // Basenames claimed by a filename-keyed formatter, e.g. terragrunt.hcl.
1011
+ const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
836
1012
  // Cache for detection results - stores array of enabled formatter names per cwd+ext
837
- const detectionCache = new Map();
1013
+ const detectionCache = new BoundedLruCache(32);
1014
+ // These are the formatter configuration files consulted by the policy helpers
1015
+ // above. Their metadata is part of detection cache identity: changing a file
1016
+ // must re-run detection even when PATH and installed tools are unchanged.
1017
+ const FORMATTER_CONFIG_FILES = [
1018
+ "package.json", "biome.json", "biome.jsonc", ".prettierrc", ".prettierrc.json",
1019
+ ".prettierrc.yml", ".prettierrc.yaml", ".prettierrc.js", ".prettierrc.cjs",
1020
+ ".prettierrc.mjs", "prettier.config.js", "prettier.config.cjs", "prettier.config.mjs",
1021
+ "prettier.config.ts", "pyproject.toml", "ruff.toml", ".ruff.toml", "black.toml",
1022
+ ".black", "tox.ini", "requirements.txt", "Pipfile", ".sqlfluff", ".rubocop.yml", ".rubocop.yaml", "Gemfile", ".clang-format",
1023
+ "_clang-format", ".php-cs-fixer.php", ".php-cs-fixer.dist.php", "stylua.toml",
1024
+ ".stylua.toml", ".ocamlformat", ".editorconfig", ".ktfmt", ".ktfmt.kts",
1025
+ ".cljfmt.edn", "cmake-format.py", ".cmake-format.yaml", ".cmake-format.json",
1026
+ "oxfmt.toml", ".oxfmtrc.json", "vite.config.ts", "vite.config.js", "vite.config.mjs",
1027
+ ];
1028
+ async function formatterConfigSignature(cwd) {
1029
+ const paths = await findUp(FORMATTER_CONFIG_FILES, cwd);
1030
+ const parts = await Promise.all(paths.sort((a, b) => a.localeCompare(b)).map(async (filePath) => {
1031
+ try {
1032
+ const stat = await fs.stat(filePath);
1033
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
1034
+ }
1035
+ catch {
1036
+ return `${filePath}:missing`;
1037
+ }
1038
+ }));
1039
+ return parts.join("|");
1040
+ }
838
1041
  // --- Public API ---
839
1042
  export async function getFormattersForFile(filePath, cwd) {
840
1043
  const ext = path.extname(filePath).toLowerCase();
841
- const cacheKey = `${cwd}:${ext}`;
1044
+ const base = path.basename(filePath).toLowerCase();
1045
+ // Filename-keyed formatters (e.g. terragrunt.hcl) can share an extension
1046
+ // with unrelated files in the same dir (.terraform.lock.hcl next to
1047
+ // terragrunt.hcl). Fold the basename into the cache key only when a
1048
+ // filename-based formatter actually applies, so a plain .hcl file cached
1049
+ // first doesn't poison the cache for terragrunt.hcl/root.hcl, or vice versa.
1050
+ const normalizedCwd = normalizeMapKey(cwd);
1051
+ const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
1052
+ ? `${normalizedCwd}:${ext}:${base}`
1053
+ : `${normalizedCwd}:${ext}`;
1054
+ const configSignature = await formatterConfigSignature(cwd);
842
1055
  // Check cache
843
- let cached = detectionCache.get(cwd);
1056
+ let cached = detectionCache.get(normalizedCwd);
1057
+ if (cached?.signature !== configSignature) {
1058
+ cached = undefined;
1059
+ detectionCache.delete(normalizedCwd);
1060
+ }
844
1061
  if (!cached) {
845
- cached = new Map();
846
- detectionCache.set(cwd, cached);
1062
+ cached = { signature: configSignature, entries: new Map() };
1063
+ detectionCache.set(normalizedCwd, cached);
847
1064
  }
848
- if (cached.has(cacheKey)) {
849
- const enabledNames = cached.get(cacheKey);
1065
+ if (cached.entries.has(cacheKey)) {
1066
+ const enabledNames = cached.entries.get(cacheKey);
850
1067
  if (!enabledNames || enabledNames.length === 0)
851
1068
  return [];
852
1069
  // Return cached formatters by name (preserves priority order)
853
1070
  return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
854
1071
  }
855
- // Detect formatters for this extension
856
- const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext));
1072
+ // Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
1073
+ const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
857
1074
  const formatterPolicy = getFormatterPolicyForFile(filePath);
858
1075
  const smartDefaultFormatterName = getSmartDefaultFormatterName(filePath);
859
1076
  const candidateFormatters = formatterPolicy?.formatterNames?.length
@@ -861,7 +1078,7 @@ export async function getFormattersForFile(filePath, cwd) {
861
1078
  : matching;
862
1079
  let selected;
863
1080
  if (formatterPolicy) {
864
- const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd));
1081
+ const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd, ext));
865
1082
  if (explicitlyConfigured.length > 0) {
866
1083
  // A formatter with explicit project config was found — use it.
867
1084
  // Prefer the policy's defaultFormatter only if it has explicit config,
@@ -892,7 +1109,11 @@ export async function getFormattersForFile(filePath, cwd) {
892
1109
  }
893
1110
  catch (err) {
894
1111
  // pi-lens-ignore: missing-error-propagation — optional formatter detection, skip on failure
895
- console.error(`[format] Detection failed for ${formatter.name}:`, err);
1112
+ logExtension({
1113
+ subsystem: "format",
1114
+ message: `Detection failed for ${formatter.name}: ${err instanceof Error ? err.message : String(err)}`,
1115
+ metadata: { formatter: formatter.name, cwd },
1116
+ });
896
1117
  }
897
1118
  }
898
1119
  }
@@ -905,7 +1126,7 @@ export async function getFormattersForFile(filePath, cwd) {
905
1126
  selectionReason = "detect";
906
1127
  }
907
1128
  else {
908
- selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd))
1129
+ selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd, ext))
909
1130
  ? "explicit-config"
910
1131
  : "smart-default";
911
1132
  }
@@ -922,7 +1143,7 @@ export async function getFormattersForFile(filePath, cwd) {
922
1143
  });
923
1144
  // Store the list of enabled formatter names in cache
924
1145
  const enabledNames = enabled.map((f) => f.name);
925
- cached.set(cacheKey, enabledNames);
1146
+ cached.entries.set(cacheKey, enabledNames);
926
1147
  return enabled;
927
1148
  }
928
1149
  export function clearFormatterCache() {
@@ -932,27 +1153,103 @@ export function clearFormatterRuntimeState() {
932
1153
  detectionCache.clear();
933
1154
  _lazyInstallAttempts.clear();
934
1155
  }
1156
+ // ESC is built via fromCharCode so no raw control byte sits in the source.
1157
+ const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
1158
+ const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
1159
+ const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
1160
+ /**
1161
+ * First line of `text` that actually carries a diagnostic.
1162
+ *
1163
+ * #1337 made nonzero exits user-visible, which put whatever this returns in
1164
+ * front of the agent (`clients/pipeline.ts`) and in the end-of-turn summary
1165
+ * (`clients/runtime-agent-end.ts`) — so "first line of stderr" is no longer
1166
+ * good enough. Biome opens stderr with a decorated section rule
1167
+ * (`format ━━━━━━━━━━━`), which as an error message is pure noise.
1168
+ *
1169
+ * Skips blank lines, pure box-drawing rules, and short banner headings; strips
1170
+ * ANSI colour so the message stays readable in a plain-text surface.
1171
+ */
1172
+ export function firstDiagnosticLine(text) {
1173
+ for (const raw of (text ?? "").split("\n")) {
1174
+ const line = raw.replace(ANSI_ESCAPE, "").trimEnd();
1175
+ const stripped = line.replace(BOX_DRAWING_GLOBAL, "").trim();
1176
+ if (!stripped)
1177
+ continue;
1178
+ // "format ━━━━━━━━" is a section banner, not a diagnostic. Require a rule
1179
+ // AND a short remainder so a real one-line error containing a box
1180
+ // character is not discarded.
1181
+ if (HAS_BOX_DRAWING.test(line) && stripped.length <= 24)
1182
+ continue;
1183
+ return stripped.slice(0, 300);
1184
+ }
1185
+ return undefined;
1186
+ }
1187
+ /**
1188
+ * Resolve a formatter command without allowing the static command fallback to
1189
+ * bypass a resolver's style-preservation refusal (#1345). `null` means the
1190
+ * primary command is unavailable; the explicit sentinel means formatting is
1191
+ * forbidden for this file and must be returned before the static command is
1192
+ * materialized.
1193
+ */
1194
+ async function resolveFormatterCommand(formatter, absolutePath, cwd) {
1195
+ const resolved = formatter.resolveCommand
1196
+ ? await formatter.resolveCommand(absolutePath, cwd)
1197
+ : null;
1198
+ if (resolved === SKIP_FORMATTING)
1199
+ return SKIP_FORMATTING;
1200
+ if (resolved !== null)
1201
+ return resolved;
1202
+ const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
1203
+ // Trust gate on the install-capable static fallback (#1334 S5): npx can
1204
+ // DOWNLOAD packages, so an untrusted project treats the fallback as
1205
+ // unavailable -- a skip, not a formatter failure; may converge next turn.
1206
+ if (fallback[0] === "npx" &&
1207
+ !assertInstallAllowed(`formatter npx fallback: ${formatter.name}`)) {
1208
+ // No second ledger entry here (#1366 review): assertInstallAllowed just
1209
+ // recorded the trust-refusal with this formatter's context — recording
1210
+ // formatter-skip too would count one user-visible degradation twice.
1211
+ return SKIP_FORMATTING;
1212
+ }
1213
+ return fallback;
1214
+ }
935
1215
  export async function formatFile(filePath, formatter) {
936
1216
  try {
937
1217
  const absolutePath = path.resolve(filePath);
938
1218
  const cwd = path.dirname(absolutePath);
939
1219
  const contentBefore = await fs.readFile(absolutePath, "utf-8");
940
- // Resolve command: prefer local (venv/vendor/node_modules) over global
941
- const resolved = formatter.resolveCommand
942
- ? await formatter.resolveCommand(absolutePath, cwd)
943
- : null;
944
- const cmd = resolved ??
945
- formatter.command.map((c) => c.replace("$FILE", absolutePath));
1220
+ // Resolve command: prefer local (venv/vendor/node_modules) over global.
1221
+ // The shared seam must honor SKIP_FORMATTING before selecting the static
1222
+ // command, including its npx fallback (#1345).
1223
+ const cmd = await resolveFormatterCommand(formatter, absolutePath, cwd);
1224
+ if (cmd === SKIP_FORMATTING) {
1225
+ // Style-preserving refusal (#1144): no repo config and no detectable
1226
+ // indentation to pin — formatting would impose the tool's stock style.
1227
+ return { success: true, changed: false };
1228
+ }
946
1229
  // Run formatter without blocking the event loop.
947
1230
  const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
948
1231
  timeout: 15000,
949
1232
  cwd,
950
1233
  });
951
- if (result.error) {
1234
+ // Strict by default (#1337): only a formatter with a documented
1235
+ // benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
1236
+ // read as a successful run. Everything else that exits nonzero never
1237
+ // rewrote the file, and reporting {success: true, changed: false} there is
1238
+ // indistinguishable from "already formatted" — the #1336 silent no-op.
1239
+ const lenientOk = formatter.lenientExitCode !== undefined &&
1240
+ result.status !== null &&
1241
+ (formatter.lenientStatuses ?? []).includes(result.status);
1242
+ if (result.error || (result.status !== 0 && !lenientOk)) {
952
1243
  return {
953
1244
  success: false,
954
1245
  changed: false,
955
- error: result.error.message,
1246
+ error: result.error?.message ||
1247
+ firstDiagnosticLine(result.stderr) ||
1248
+ // biome, ktlint and `mix format` report on STDOUT; without this
1249
+ // their diagnostic is discarded and the user is told only that
1250
+ // the tool "exited with status 1".
1251
+ firstDiagnosticLine(result.stdout) ||
1252
+ `${formatter.name} exited with status ${result.status}`,
956
1253
  };
957
1254
  }
958
1255
  // Check if content changed