ultimate-pi 0.18.0 → 0.19.0

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 (314) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
  2. package/.agents/skills/harness-decisions/SKILL.md +2 -3
  3. package/.agents/skills/harness-governor/SKILL.md +6 -5
  4. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  5. package/.agents/skills/harness-review/SKILL.md +7 -7
  6. package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
  7. package/.agents/skills/harness-steer/SKILL.md +1 -1
  8. package/.agents/skills/sentrux/SKILL.md +9 -9
  9. package/.pi/PACKAGING.md +4 -4
  10. package/.pi/SYSTEM.md +54 -120
  11. package/.pi/agents/harness/incident-recorder.md +0 -1
  12. package/.pi/agents/harness/planning/decompose.md +1 -3
  13. package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
  14. package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
  15. package/.pi/agents/harness/planning/hypothesis.md +0 -2
  16. package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
  17. package/.pi/agents/harness/planning/plan-adversary.md +0 -2
  18. package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
  19. package/.pi/agents/harness/planning/planning-context.md +0 -2
  20. package/.pi/agents/harness/planning/review-integrator.md +0 -2
  21. package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
  22. package/.pi/agents/harness/planning/stack-researcher.md +0 -2
  23. package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -2
  24. package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -2
  25. package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -2
  26. package/.pi/agents/harness/{executor.md → running/executor.md} +0 -2
  27. package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
  28. package/.pi/agents/harness/sentrux-steward.md +0 -2
  29. package/.pi/agents/harness/trace-librarian.md +0 -1
  30. package/.pi/extensions/00-harness-project-control.ts +133 -0
  31. package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
  32. package/.pi/extensions/agt-kill-switch.ts +57 -0
  33. package/.pi/extensions/agt-prompt-guard.ts +32 -0
  34. package/.pi/extensions/budget-guard.ts +2 -0
  35. package/.pi/extensions/custom-footer.ts +46 -145
  36. package/.pi/extensions/custom-header.ts +1 -1
  37. package/.pi/extensions/custom-system-prompt.ts +1 -1
  38. package/.pi/extensions/debate-orchestrator.ts +7 -5
  39. package/.pi/extensions/harness-ask-user.ts +8 -8
  40. package/.pi/extensions/harness-debate-tools.ts +27 -43
  41. package/.pi/extensions/harness-lens.ts +94 -0
  42. package/.pi/extensions/harness-live-widget.ts +33 -2
  43. package/.pi/extensions/harness-plan-approval.ts +12 -12
  44. package/.pi/extensions/harness-run-context.ts +1214 -852
  45. package/.pi/extensions/harness-subagent-governance.ts +8 -0
  46. package/.pi/extensions/harness-subagent-submit.ts +36 -164
  47. package/.pi/extensions/harness-subagents.ts +4 -4
  48. package/.pi/extensions/harness-telemetry.ts +3 -1
  49. package/.pi/extensions/harness-web-tools.ts +3 -3
  50. package/.pi/extensions/observation-bus.ts +2 -0
  51. package/.pi/extensions/policy-gate.ts +27 -5
  52. package/.pi/extensions/review-integrity.ts +91 -10
  53. package/.pi/extensions/sentrux-rules-sync.ts +3 -1
  54. package/.pi/extensions/subagent-governance.ts +92 -0
  55. package/.pi/extensions/test-diff-integrity.ts +1 -0
  56. package/.pi/extensions/trace-recorder.ts +3 -1
  57. package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
  58. package/.pi/harness/README.md +6 -2
  59. package/.pi/harness/agents.manifest.json +38 -49
  60. package/.pi/harness/agents.policy.yaml +275 -0
  61. package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
  62. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
  63. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
  64. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
  65. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
  66. package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
  67. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  68. package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
  69. package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
  70. package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
  71. package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
  72. package/.pi/harness/docs/adrs/README.md +6 -0
  73. package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
  74. package/.pi/harness/docs/practice-map.md +2 -2
  75. package/.pi/harness/evolution/README.md +1 -2
  76. package/.pi/harness/examples/agents.policy.project.yaml +19 -0
  77. package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
  78. package/.pi/harness/policies/bash-denylists.yaml +5 -0
  79. package/.pi/harness/policies/defaults.yaml +51 -0
  80. package/.pi/harness/policies/orchestrator.yaml +18 -0
  81. package/.pi/harness/policies/phases.yaml +10 -0
  82. package/.pi/harness/policies/roles.yaml +5 -0
  83. package/.pi/harness/policies/web-guard.yaml +5 -0
  84. package/.pi/harness/policies/workflow-sequences.yaml +9 -0
  85. package/.pi/harness/sentrux/architecture.manifest.json +26 -4
  86. package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
  87. package/.pi/harness/specs/observation.schema.json +2 -1
  88. package/.pi/lib/agents-policy.d.mts +70 -0
  89. package/.pi/lib/agents-policy.mjs +325 -0
  90. package/.pi/lib/agents-policy.ts +19 -0
  91. package/.pi/lib/agt/audit-run-sink.ts +52 -0
  92. package/.pi/lib/agt/build-evaluation-context.ts +285 -0
  93. package/.pi/lib/agt/config.ts +28 -0
  94. package/.pi/lib/agt/delegation.ts +69 -0
  95. package/.pi/lib/agt/evaluate-policy.ts +56 -0
  96. package/.pi/lib/agt/identity-registry.ts +41 -0
  97. package/.pi/lib/agt/index.ts +55 -0
  98. package/.pi/lib/agt/kill-switch-state.ts +11 -0
  99. package/.pi/lib/agt/legacy-evaluate.ts +101 -0
  100. package/.pi/lib/agt/policy-engine.ts +154 -0
  101. package/.pi/lib/agt/rings.ts +21 -0
  102. package/.pi/lib/agt/sre-hooks.ts +45 -0
  103. package/.pi/lib/agt/trust-run-store.ts +26 -0
  104. package/.pi/lib/agt/workflow-history.ts +29 -0
  105. package/.pi/lib/agt-governance-active.ts +14 -0
  106. package/.pi/lib/agt-tool-guard.ts +78 -0
  107. package/.pi/lib/ask-user/dialog.ts +314 -0
  108. package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
  109. package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
  110. package/.pi/{extensions/lib → lib}/extension-load-guard.ts +21 -0
  111. package/.pi/lib/harness-agt-tool-guard.ts +5 -0
  112. package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +6 -16
  113. package/.pi/lib/harness-debate-core-deps.ts +14 -0
  114. package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
  115. package/.pi/lib/harness-lens/.gitattributes +1 -0
  116. package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
  117. package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
  118. package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
  119. package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
  120. package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
  121. package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
  122. package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
  123. package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
  124. package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
  125. package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
  126. package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
  127. package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
  128. package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
  129. package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
  130. package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
  131. package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
  132. package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
  133. package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
  134. package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
  135. package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
  136. package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
  137. package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
  138. package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
  139. package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
  140. package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
  141. package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
  142. package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
  143. package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
  144. package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
  145. package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
  146. package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
  147. package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
  148. package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
  149. package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
  150. package/.pi/lib/harness-lens/clients/types.ts +59 -0
  151. package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
  152. package/.pi/lib/harness-lens/index.ts +532 -0
  153. package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
  154. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
  155. package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
  156. package/.pi/lib/harness-project-config.ts +91 -0
  157. package/.pi/lib/harness-run-context-responses.ts +9 -0
  158. package/.pi/lib/harness-run-context.ts +1 -3
  159. package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +4 -3
  160. package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +5 -28
  161. package/.pi/lib/harness-subagent-auth.ts +51 -0
  162. package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +13 -10
  163. package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
  164. package/.pi/lib/harness-subagent-submit-register.ts +163 -0
  165. package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -55
  166. package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
  167. package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
  168. package/.pi/lib/harness-ui-state.ts +27 -12
  169. package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
  170. package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
  171. package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
  172. package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
  173. package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
  174. package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
  175. package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
  176. package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +3 -52
  177. package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
  178. package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
  179. package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
  180. package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
  181. package/.pi/prompts/harness-auto.md +2 -2
  182. package/.pi/prompts/harness-plan.md +4 -6
  183. package/.pi/prompts/harness-review.md +9 -9
  184. package/.pi/prompts/harness-run.md +7 -7
  185. package/.pi/prompts/harness-setup.md +42 -68
  186. package/.pi/prompts/harness-steer.md +2 -2
  187. package/.pi/scripts/README.md +3 -5
  188. package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
  189. package/.pi/scripts/graphify-kb-updater.mjs +48 -8
  190. package/.pi/scripts/harness-agents-manifest.mjs +61 -4
  191. package/.pi/scripts/harness-agt-doctor.ts +36 -0
  192. package/.pi/scripts/harness-cli-verify.sh +9 -2
  193. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  194. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  195. package/.pi/scripts/harness-verify.mjs +113 -39
  196. package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
  197. package/.pi/scripts/validate-plan-dag.mjs +65 -74
  198. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
  199. package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
  200. package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
  201. package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
  202. package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
  203. package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
  204. package/.pi/skills/architecture/layered/SKILL.md +68 -0
  205. package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
  206. package/.pi/skills/architecture/microservices/SKILL.md +64 -0
  207. package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
  208. package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
  209. package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
  210. package/.pi/skills/architecture/service-based/SKILL.md +64 -0
  211. package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
  212. package/.pi/skills/architecture/space-based/SKILL.md +60 -0
  213. package/.pi/skills/ast-grep/SKILL.md +40 -321
  214. package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
  215. package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
  216. package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
  217. package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
  218. package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
  219. package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
  220. package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
  221. package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
  222. package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
  223. package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
  224. package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
  225. package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
  226. package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
  227. package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
  228. package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
  229. package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
  230. package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
  231. package/.pi/skills/lsp-navigation/SKILL.md +89 -0
  232. package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
  233. package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
  234. package/.pi/skills/quality/security-review/SKILL.md +34 -0
  235. package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
  236. package/.pi/skills/quality/testability-design/SKILL.md +33 -0
  237. package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
  238. package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
  239. package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
  240. package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
  241. package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
  242. package/.sentrux/rules.toml +20 -4
  243. package/AGENTS.md +5 -0
  244. package/CHANGELOG.md +26 -0
  245. package/README.md +85 -58
  246. package/THIRD_PARTY_NOTICES.md +12 -21
  247. package/package.json +15 -7
  248. package/vendor/pi-subagents/src/agents.ts +45 -1
  249. package/vendor/pi-subagents/src/subagents.ts +866 -811
  250. package/vendor/pi-vcc/src/core/brief.ts +68 -99
  251. package/vendor/pi-vcc/src/core/settings.ts +2 -2
  252. package/.agents/skills/caveman/SKILL.md +0 -67
  253. package/.pi/agents/harness/meta-optimizer.md +0 -36
  254. package/.pi/agents/harness/planning/scout-graphify.md +0 -39
  255. package/.pi/agents/harness/planning/scout-semantic.md +0 -41
  256. package/.pi/agents/harness/planning/scout-structure.md +0 -37
  257. package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
  258. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -209
  259. package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
  260. package/.pi/extensions/pi-model-router-harness.ts +0 -42
  261. package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
  262. package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
  263. package/.pi/model-router.example.json +0 -36
  264. package/.pi/prompts/harness-critic.md +0 -10
  265. package/.pi/prompts/harness-eval.md +0 -10
  266. package/.pi/prompts/harness-router-tune.md +0 -52
  267. package/.pi/scripts/harness-generate-model-router.mjs +0 -327
  268. package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
  269. package/.pi/scripts/harness-sync-model-router.mjs +0 -97
  270. package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
  271. package/vendor/pi-model-router/.prettierignore +0 -4
  272. package/vendor/pi-model-router/.prettierrc +0 -5
  273. package/vendor/pi-model-router/AGENTS.md +0 -39
  274. package/vendor/pi-model-router/LICENSE +0 -21
  275. package/vendor/pi-model-router/README.md +0 -99
  276. package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
  277. package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
  278. package/vendor/pi-model-router/extensions/commands.ts +0 -720
  279. package/vendor/pi-model-router/extensions/config.ts +0 -348
  280. package/vendor/pi-model-router/extensions/constants.ts +0 -1
  281. package/vendor/pi-model-router/extensions/index.ts +0 -478
  282. package/vendor/pi-model-router/extensions/provider.ts +0 -580
  283. package/vendor/pi-model-router/extensions/routing.ts +0 -564
  284. package/vendor/pi-model-router/extensions/state.ts +0 -52
  285. package/vendor/pi-model-router/extensions/types.ts +0 -95
  286. package/vendor/pi-model-router/extensions/ui.ts +0 -144
  287. package/vendor/pi-model-router/model-router.example.json +0 -48
  288. package/vendor/pi-model-router/package.json +0 -48
  289. package/vendor/pi-model-router/tsconfig.json +0 -16
  290. /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
  291. /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
  292. /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
  293. /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
  294. /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
  295. /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
  296. /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
  297. /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
  298. /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
  299. /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
  300. /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
  301. /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
  302. /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
  303. /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
  304. /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
  305. /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
  306. /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
  307. /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
  308. /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
  309. /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
  310. /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
  311. /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
  312. /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
  313. /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
  314. /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
@@ -0,0 +1,1000 @@
1
+ /**
2
+ * Formatter Definitions for pi-lens
3
+ *
4
+ * Auto-detects formatters based on:
5
+ * - Config files (biome.json, .prettierrc, etc.)
6
+ * - Dependencies (package.json, requirements.txt, etc.)
7
+ * - Binary availability (which/where)
8
+ *
9
+ * Inspired by OpenCode's formatter.ts pattern
10
+ */
11
+
12
+ import * as fs from "node:fs/promises";
13
+ import * as path from "node:path";
14
+ import { logLatency } from "./latency-logger.js";
15
+ import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
16
+ import {
17
+ getAutoInstallToolIdForFormatter,
18
+ getFormatterPolicyForFile,
19
+ getSmartDefaultFormatterName,
20
+ hasBiomeConfig,
21
+ hasBlackConfig,
22
+ hasClangFormatConfig,
23
+ hasCljfmtConfig,
24
+ hasCmakeFormatConfig,
25
+ hasGoogleJavaFormatConfig,
26
+ hasNearestPackageJsonDependency,
27
+ hasNearestPackageJsonField,
28
+ hasOcamlformatConfig,
29
+ hasOxfmtConfig,
30
+ hasPhpCsFixerConfig,
31
+ hasPrettierConfig,
32
+ hasRubocopConfig,
33
+ hasRuffConfig,
34
+ hasSqlfluffConfig,
35
+ hasStandardrbConfig,
36
+ hasStyluaConfig,
37
+ hasVitePlusConfig,
38
+ } from "./tool-policy.js";
39
+
40
+ const _lazyInstallAttempts = new Set<string>();
41
+
42
+ async function tryLazyInstallFormatterTool(
43
+ _tool: "rubocop" | "rustfmt",
44
+ _cwd: string,
45
+ ): Promise<boolean> {
46
+ return false;
47
+ }
48
+
49
+ // --- Types ---
50
+
51
+ export interface FormatterInfo {
52
+ name: string;
53
+ command: string[]; // Command with $FILE placeholder — used as fallback
54
+ extensions: string[];
55
+ /** Detect if this formatter should be used for a project */
56
+ detect(cwd: string): Promise<boolean>;
57
+ /**
58
+ * Optionally resolve the full command at runtime (venv, vendor/bin, bundle exec).
59
+ * Return null to fall back to the static `command` field.
60
+ * filePath is already resolved to an absolute path.
61
+ */
62
+ resolveCommand?(filePath: string, cwd: string): Promise<string[] | null>;
63
+ }
64
+
65
+ export interface FormatterResult {
66
+ success: boolean;
67
+ changed: boolean;
68
+ error?: string;
69
+ }
70
+
71
+ // --- Utility Functions ---
72
+
73
+ async function fileExists(filePath: string): Promise<boolean> {
74
+ try {
75
+ await fs.access(filePath);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ async function findUp(
83
+ targets: string[],
84
+ startDir: string,
85
+ stopDir: string = path.parse(startDir).root,
86
+ ): Promise<string[]> {
87
+ const found: string[] = [];
88
+ let currentDir = startDir;
89
+
90
+ while (currentDir !== stopDir) {
91
+ for (const target of targets) {
92
+ const checkPath = path.join(currentDir, target);
93
+ if (await fileExists(checkPath)) {
94
+ found.push(checkPath);
95
+ }
96
+ }
97
+ const parent = path.dirname(currentDir);
98
+ if (parent === currentDir) break;
99
+ currentDir = parent;
100
+ }
101
+
102
+ return found;
103
+ }
104
+
105
+ async function which(command: string): Promise<string | null> {
106
+ const result = safeSpawn(
107
+ process.platform === "win32" ? "where" : "which",
108
+ [command],
109
+ { timeout: 5000 },
110
+ );
111
+ if (result.error || result.status !== 0) return null;
112
+ return result.stdout?.trim().split(/\r?\n/)[0] ?? null;
113
+ }
114
+
115
+ async function resolveGoFmtBinary(): Promise<string | null> {
116
+ const inPath = await which("gofmt");
117
+ if (inPath) return inPath;
118
+
119
+ const goCheck = safeSpawn("go", ["env", "GOROOT"], {
120
+ timeout: 5000,
121
+ });
122
+ if (goCheck.error || goCheck.status !== 0) return null;
123
+
124
+ const goroot = (goCheck.stdout ?? "").trim();
125
+ if (!goroot) return null;
126
+
127
+ const binary = path.join(
128
+ goroot,
129
+ "bin",
130
+ process.platform === "win32" ? "gofmt.exe" : "gofmt",
131
+ );
132
+ return (await fileExists(binary)) ? binary : null;
133
+ }
134
+
135
+ // --- Venv / Local Binary Helpers ---
136
+
137
+ /**
138
+ * Walk up from cwd looking for a binary in .venv or venv.
139
+ * Returns the absolute path if found, null otherwise.
140
+ */
141
+ async function findInVenv(binary: string, cwd: string): Promise<string | null> {
142
+ const isWin = process.platform === "win32";
143
+ const candidates = isWin
144
+ ? [
145
+ `.venv/Scripts/${binary}.exe`,
146
+ `venv/Scripts/${binary}.exe`,
147
+ `.venv/Scripts/${binary}`,
148
+ `venv/Scripts/${binary}`,
149
+ ]
150
+ : [`.venv/bin/${binary}`, `venv/bin/${binary}`];
151
+
152
+ let dir = cwd;
153
+ const root = path.parse(dir).root;
154
+ while (dir !== root) {
155
+ for (const candidate of candidates) {
156
+ const full = path.join(dir, candidate);
157
+ if (await fileExists(full)) return full;
158
+ }
159
+ const parent = path.dirname(dir);
160
+ if (parent === dir) break;
161
+ dir = parent;
162
+ }
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * Check vendor/bin for PHP Composer-managed tools.
168
+ * Walks up from cwd to find vendor/bin/<binary>.
169
+ */
170
+ async function findInVendorBin(
171
+ binary: string,
172
+ cwd: string,
173
+ ): Promise<string | null> {
174
+ const isWin = process.platform === "win32";
175
+ const names = isWin ? [`${binary}.bat`, binary] : [binary];
176
+ let dir = cwd;
177
+ const root = path.parse(dir).root;
178
+ while (dir !== root) {
179
+ for (const name of names) {
180
+ const full = path.join(dir, "vendor", "bin", name);
181
+ if (await fileExists(full)) return full;
182
+ }
183
+ const parent = path.dirname(dir);
184
+ if (parent === dir) break;
185
+ dir = parent;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ /**
191
+ * Check node_modules/.bin for locally installed Node tools.
192
+ * Walks up from cwd to find node_modules/.bin/<binary>.
193
+ */
194
+ async function findInNodeModules(
195
+ binary: string,
196
+ cwd: string,
197
+ ): Promise<string | null> {
198
+ const isWin = process.platform === "win32";
199
+ let dir = cwd;
200
+ const root = path.parse(dir).root;
201
+ while (dir !== root) {
202
+ const candidates = isWin
203
+ ? [
204
+ path.join(dir, "node_modules", ".bin", `${binary}.cmd`),
205
+ path.join(dir, "node_modules", ".bin", binary),
206
+ ]
207
+ : [path.join(dir, "node_modules", ".bin", binary)];
208
+ for (const full of candidates) {
209
+ if (await fileExists(full)) return full;
210
+ }
211
+ const parent = path.dirname(dir);
212
+ if (parent === dir) break;
213
+ dir = parent;
214
+ }
215
+ return null;
216
+ }
217
+
218
+ /**
219
+ * Returns true if `bundle exec <gem>` should be used:
220
+ * bundle binary is available AND Gemfile.lock exists in the tree.
221
+ */
222
+ async function canUseBundleExec(cwd: string): Promise<boolean> {
223
+ if ((await which("bundle")) === null) return false;
224
+ const lockfiles = await findUp(["Gemfile.lock"], cwd);
225
+ return lockfiles.length > 0;
226
+ }
227
+
228
+ async function resolveManagedSmartDefaultCommand(
229
+ formatterName: string,
230
+ filePath: string,
231
+ args: string[],
232
+ ): Promise<string[] | null> {
233
+ const toolId = getAutoInstallToolIdForFormatter(formatterName);
234
+ if (!toolId) return null;
235
+ const { ensureTool } = await import("./installer/index.js");
236
+ const installed = await ensureTool(toolId);
237
+ if (!installed) return null;
238
+ return [installed, ...args, filePath];
239
+ }
240
+
241
+ function hasExplicitFormatterConfig(
242
+ formatterName: string,
243
+ cwd: string,
244
+ ): boolean {
245
+ switch (formatterName) {
246
+ case "biome":
247
+ return hasBiomeConfig(cwd);
248
+ case "prettier":
249
+ return (
250
+ hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier")
251
+ );
252
+ case "oxfmt":
253
+ return (
254
+ hasOxfmtConfig(cwd) ||
255
+ hasVitePlusConfig(cwd) ||
256
+ hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt")
257
+ );
258
+ case "ruff":
259
+ return hasRuffConfig(cwd);
260
+ case "black":
261
+ return hasBlackConfig(cwd);
262
+ case "sqlfluff":
263
+ return hasSqlfluffConfig(cwd);
264
+ case "rubocop":
265
+ return hasRubocopConfig(cwd);
266
+ case "standardrb":
267
+ return hasStandardrbConfig(cwd);
268
+ case "clang-format":
269
+ return hasClangFormatConfig(cwd);
270
+ case "php-cs-fixer":
271
+ return hasPhpCsFixerConfig(cwd);
272
+ case "stylua":
273
+ return hasStyluaConfig(cwd);
274
+ case "ocamlformat":
275
+ return hasOcamlformatConfig(cwd);
276
+ case "google-java-format":
277
+ return hasGoogleJavaFormatConfig(cwd);
278
+ case "cljfmt":
279
+ return hasCljfmtConfig(cwd);
280
+ case "cmake-format":
281
+ return hasCmakeFormatConfig(cwd);
282
+ default:
283
+ return false;
284
+ }
285
+ }
286
+
287
+ // --- Formatter Definitions ---
288
+
289
+ async function hasEditorConfig(cwd: string): Promise<boolean> {
290
+ try {
291
+ await fs.access(path.join(cwd, ".editorconfig"));
292
+ return true;
293
+ } catch {
294
+ return false;
295
+ }
296
+ }
297
+
298
+ export const biomeFormatter: FormatterInfo = {
299
+ name: "biome",
300
+ command: ["npx", "@biomejs/biome", "format", "--write", "$FILE"],
301
+ async resolveCommand(filePath, cwd) {
302
+ const editorConfigFlag = (await hasEditorConfig(cwd))
303
+ ? ["--use-editorconfig=true"]
304
+ : [];
305
+ const local = await findInNodeModules("biome", cwd);
306
+ if (local)
307
+ return [local, "format", "--write", ...editorConfigFlag, filePath];
308
+ const toolId = getAutoInstallToolIdForFormatter("biome");
309
+ if (!toolId) return null;
310
+ const { ensureTool } = await import("./installer/index.js");
311
+ const installed = await ensureTool(toolId);
312
+ if (installed)
313
+ return [installed, "format", "--write", ...editorConfigFlag, filePath];
314
+ return null;
315
+ },
316
+ extensions: [
317
+ ".js",
318
+ ".jsx",
319
+ ".mjs",
320
+ ".cjs",
321
+ ".ts",
322
+ ".tsx",
323
+ ".mts",
324
+ ".cts",
325
+ ".json",
326
+ ".jsonc",
327
+ ".css",
328
+ ".scss",
329
+ ".sass",
330
+ ".vue",
331
+ ".svelte",
332
+ ".html",
333
+ ".htm",
334
+ ],
335
+ async detect(cwd: string) {
336
+ return (
337
+ hasBiomeConfig(cwd) ||
338
+ hasNearestPackageJsonDependency(cwd, "@biomejs/biome")
339
+ );
340
+ },
341
+ };
342
+
343
+ export const prettierFormatter: FormatterInfo = {
344
+ name: "prettier",
345
+ command: ["npx", "prettier", "--write", "$FILE"],
346
+ async resolveCommand(filePath, cwd) {
347
+ const local = await findInNodeModules("prettier", cwd);
348
+ if (local) return [local, "--write", filePath];
349
+ return resolveManagedSmartDefaultCommand("prettier", filePath, ["--write"]);
350
+ },
351
+ extensions: [
352
+ ".js",
353
+ ".jsx",
354
+ ".mjs",
355
+ ".cjs",
356
+ ".ts",
357
+ ".tsx",
358
+ ".mts",
359
+ ".cts",
360
+ ".json",
361
+ ".jsonc",
362
+ ".css",
363
+ ".scss",
364
+ ".sass",
365
+ ".less",
366
+ ".vue",
367
+ ".svelte",
368
+ ".html",
369
+ ".htm",
370
+ ".md",
371
+ ".mdx",
372
+ ".yaml",
373
+ ".yml",
374
+ ".graphql",
375
+ ".gql",
376
+ ],
377
+ async detect(cwd: string) {
378
+ return (
379
+ hasPrettierConfig(cwd) ||
380
+ hasNearestPackageJsonDependency(cwd, "prettier") ||
381
+ hasNearestPackageJsonField(cwd, "prettier")
382
+ );
383
+ },
384
+ };
385
+
386
+ export const oxfmtFormatter: FormatterInfo = {
387
+ name: "oxfmt",
388
+ command: ["oxfmt", "$FILE"],
389
+ async resolveCommand(filePath, cwd) {
390
+ if (hasVitePlusConfig(cwd)) {
391
+ const localVp = await findInNodeModules("vp", cwd);
392
+ if (localVp) return [localVp, "fmt", filePath, "--write"];
393
+ const globalVp = await which("vp");
394
+ if (globalVp) return [globalVp, "fmt", filePath, "--write"];
395
+ }
396
+ const local = await findInNodeModules("oxfmt", cwd);
397
+ if (local) return [local, filePath];
398
+ const found = await which("oxfmt");
399
+ if (found) return [found, filePath];
400
+ return null;
401
+ },
402
+ extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
403
+ async detect(cwd: string) {
404
+ return (
405
+ hasOxfmtConfig(cwd) ||
406
+ hasVitePlusConfig(cwd) ||
407
+ hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt")
408
+ );
409
+ },
410
+ };
411
+
412
+ export const ruffFormatter: FormatterInfo = {
413
+ name: "ruff",
414
+ command: ["ruff", "format", "$FILE"],
415
+ extensions: [".py", ".pyi"],
416
+ async resolveCommand(filePath, cwd) {
417
+ const venv = await findInVenv("ruff", cwd);
418
+ if (venv) return [venv, "format", filePath];
419
+ const toolId = getAutoInstallToolIdForFormatter("ruff");
420
+ if (!toolId) return null;
421
+ const { ensureTool } = await import("./installer/index.js");
422
+ const installed = await ensureTool(toolId);
423
+ if (installed) return [installed, "format", filePath];
424
+ return null;
425
+ },
426
+ async detect(cwd: string) {
427
+ if (hasRuffConfig(cwd)) return true;
428
+ // No-config fallback: if Ruff is already available, allow formatter usage.
429
+ // This keeps Python default behavior consistent with startup defaults.
430
+ const { getToolPath } = await import("./installer/index.js");
431
+ const installed = await getToolPath("ruff");
432
+ return Boolean(installed);
433
+ },
434
+ };
435
+
436
+ export const blackFormatter: FormatterInfo = {
437
+ name: "black",
438
+ command: ["black", "$FILE"],
439
+ extensions: [".py", ".pyi"],
440
+ async resolveCommand(filePath, cwd) {
441
+ const venv = await findInVenv("black", cwd);
442
+ if (venv) return [venv, filePath];
443
+ return null;
444
+ },
445
+ async detect(cwd: string) {
446
+ return hasBlackConfig(cwd);
447
+ },
448
+ };
449
+
450
+ export const sqlfluffFormatter: FormatterInfo = {
451
+ name: "sqlfluff",
452
+ command: ["sqlfluff", "fix", "--force", "$FILE"],
453
+ extensions: [".sql"],
454
+ async resolveCommand(filePath, cwd) {
455
+ const venv = await findInVenv("sqlfluff", cwd);
456
+ if (venv) return [venv, "fix", "--force", filePath];
457
+ return null;
458
+ },
459
+ async detect(cwd: string) {
460
+ return hasSqlfluffConfig(cwd);
461
+ },
462
+ };
463
+
464
+ export const gofmtFormatter: FormatterInfo = {
465
+ name: "gofmt",
466
+ command: ["gofmt", "-w", "$FILE"],
467
+ extensions: [".go"],
468
+ async resolveCommand(filePath, _cwd) {
469
+ const gofmtBinary = await resolveGoFmtBinary();
470
+ if (!gofmtBinary) return null;
471
+ return [gofmtBinary, "-w", filePath];
472
+ },
473
+ async detect(_cwd: string) {
474
+ return (await resolveGoFmtBinary()) !== null;
475
+ },
476
+ };
477
+
478
+ export const rustfmtFormatter: FormatterInfo = {
479
+ name: "rustfmt",
480
+ command: ["rustfmt", "$FILE"],
481
+ extensions: [".rs"],
482
+ async detect(cwd: string) {
483
+ if ((await which("rustfmt")) !== null) return true;
484
+ // If we're in a Rust project, attempt one lazy install of rustfmt component.
485
+ const rustProject = (await findUp(["Cargo.toml"], cwd)).length > 0;
486
+ if (!rustProject) return false;
487
+ if ((await which("rustup")) === null) return false;
488
+ // rustfmt: PATH-only; no lazy gem/rustup install from harness-lens
489
+ return (await which("rustfmt")) !== null;
490
+ },
491
+ };
492
+
493
+ export const zigFormatter: FormatterInfo = {
494
+ name: "zig",
495
+ command: ["zig", "fmt", "$FILE"],
496
+ extensions: [".zig", ".zon"],
497
+ async detect(_cwd: string) {
498
+ return (await which("zig")) !== null;
499
+ },
500
+ };
501
+
502
+ export const dartFormatter: FormatterInfo = {
503
+ name: "dart",
504
+ command: ["dart", "format", "$FILE"],
505
+ extensions: [".dart"],
506
+ async detect(_cwd: string) {
507
+ return (await which("dart")) !== null;
508
+ },
509
+ };
510
+
511
+ export const shfmtFormatter: FormatterInfo = {
512
+ name: "shfmt",
513
+ command: ["shfmt", "-w", "$FILE"],
514
+ extensions: [".sh", ".bash"],
515
+ async resolveCommand(filePath, _cwd) {
516
+ const inPath = await which("shfmt");
517
+ if (inPath) return [inPath, "-w", filePath];
518
+ return resolveManagedSmartDefaultCommand("shfmt", filePath, ["-w"]);
519
+ },
520
+ async detect(_cwd: string) {
521
+ if ((await which("shfmt")) !== null) return true;
522
+ const { getToolPath } = await import("./installer/index.js");
523
+ return Boolean(await getToolPath("shfmt"));
524
+ },
525
+ };
526
+
527
+ export const nixfmtFormatter: FormatterInfo = {
528
+ name: "nixfmt",
529
+ command: ["nixfmt", "$FILE"],
530
+ extensions: [".nix"],
531
+ async detect(_cwd: string) {
532
+ return (await which("nixfmt")) !== null;
533
+ },
534
+ };
535
+
536
+ export const mixFormatter: FormatterInfo = {
537
+ name: "mix",
538
+ command: ["mix", "format", "$FILE"],
539
+ extensions: [".ex", ".exs", ".eex", ".heex", ".leex"],
540
+ async detect(_cwd: string) {
541
+ return (await which("mix")) !== null;
542
+ },
543
+ };
544
+
545
+ export const ocamlformatFormatter: FormatterInfo = {
546
+ name: "ocamlformat",
547
+ command: ["ocamlformat", "-i", "$FILE"],
548
+ extensions: [".ml", ".mli"],
549
+ async detect(cwd: string) {
550
+ const hasBinary = (await which("ocamlformat")) !== null;
551
+ if (!hasBinary) return false;
552
+ const configs = [".ocamlformat"];
553
+ const found = await findUp(configs, cwd);
554
+ return found.length > 0;
555
+ },
556
+ };
557
+
558
+ export const clangFormatFormatter: FormatterInfo = {
559
+ name: "clang-format",
560
+ command: ["clang-format", "-i", "$FILE"],
561
+ extensions: [".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".ino"],
562
+ async detect(cwd: string) {
563
+ const hasBinary = (await which("clang-format")) !== null;
564
+ if (!hasBinary) return false;
565
+ const configs = [".clang-format", "_clang-format"];
566
+ const found = await findUp(configs, cwd);
567
+ return found.length > 0;
568
+ },
569
+ };
570
+
571
+ export const ktlintFormatter: FormatterInfo = {
572
+ name: "ktlint",
573
+ command: ["ktlint", "-F", "$FILE"],
574
+ extensions: [".kt", ".kts"],
575
+ async resolveCommand(filePath, _cwd) {
576
+ const inPath = await which("ktlint");
577
+ if (inPath) return [inPath, "-F", filePath];
578
+ return resolveManagedSmartDefaultCommand("ktlint", filePath, ["-F"]);
579
+ },
580
+ async detect(_cwd: string) {
581
+ if ((await which("ktlint")) !== null) return true;
582
+ const { getToolPath } = await import("./installer/index.js");
583
+ return Boolean(await getToolPath("ktlint"));
584
+ },
585
+ };
586
+
587
+ export const rubocopFormatter: FormatterInfo = {
588
+ name: "rubocop",
589
+ command: ["rubocop", "-a", "--no-color", "$FILE"],
590
+ extensions: [".rb", ".rake", ".gemspec", ".ru"],
591
+ async resolveCommand(filePath, cwd) {
592
+ if (await canUseBundleExec(cwd))
593
+ return ["bundle", "exec", "rubocop", "-a", "--no-color", filePath];
594
+ return null;
595
+ },
596
+ async detect(cwd: string) {
597
+ if (!hasRubocopConfig(cwd)) return false;
598
+ if ((await which("rubocop")) !== null) return true;
599
+ // rubocop: PATH-only; no lazy gem install from harness-lens
600
+ return (await which("rubocop")) !== null;
601
+ },
602
+ };
603
+
604
+ export const standardrbFormatter: FormatterInfo = {
605
+ name: "standardrb",
606
+ command: ["standardrb", "--fix", "$FILE"],
607
+ extensions: [".rb", ".rake"],
608
+ async resolveCommand(filePath, cwd) {
609
+ if (await canUseBundleExec(cwd))
610
+ return ["bundle", "exec", "standardrb", "--fix", filePath];
611
+ return null;
612
+ },
613
+ async detect(cwd: string) {
614
+ if (!hasStandardrbConfig(cwd)) return false;
615
+ return (await which("standardrb")) !== null;
616
+ },
617
+ };
618
+
619
+ export const gleamFormatter: FormatterInfo = {
620
+ name: "gleam",
621
+ command: ["gleam", "format", "$FILE"],
622
+ extensions: [".gleam"],
623
+ async detect(cwd: string) {
624
+ // Present if gleam.toml exists (any Gleam project)
625
+ const found = await findUp(["gleam.toml"], cwd);
626
+ if (found.length > 0) return (await which("gleam")) !== null;
627
+ return false;
628
+ },
629
+ };
630
+
631
+ export const terraformFormatter: FormatterInfo = {
632
+ name: "terraform",
633
+ command: ["terraform", "fmt", "$FILE"],
634
+ extensions: [".tf", ".tfvars"],
635
+ async detect(_cwd: string) {
636
+ return (await which("terraform")) !== null;
637
+ },
638
+ };
639
+
640
+ export const phpCsFixerFormatter: FormatterInfo = {
641
+ name: "php-cs-fixer",
642
+ command: ["php-cs-fixer", "fix", "$FILE"],
643
+ extensions: [".php"],
644
+ async resolveCommand(filePath, cwd) {
645
+ const vendor = await findInVendorBin("php-cs-fixer", cwd);
646
+ if (vendor) return [vendor, "fix", filePath];
647
+ return null;
648
+ },
649
+ async detect(cwd: string) {
650
+ const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
651
+ const globalBin = await which("php-cs-fixer");
652
+ if (!vendorBin && !globalBin) return false;
653
+ // Only run if project has explicit config
654
+ const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
655
+ const found = await findUp(configs, cwd);
656
+ return found.length > 0;
657
+ },
658
+ };
659
+
660
+ export const csharpierFormatter: FormatterInfo = {
661
+ name: "csharpier",
662
+ command: ["dotnet", "csharpier", "$FILE"],
663
+ extensions: [".cs"],
664
+ async detect(_cwd: string) {
665
+ // Check dotnet is available AND csharpier tool is installed
666
+ if ((await which("dotnet")) === null) return false;
667
+ const result = safeSpawn("dotnet", ["csharpier", "--version"], {
668
+ timeout: 5000,
669
+ });
670
+ return !result.error && result.status === 0;
671
+ },
672
+ };
673
+
674
+ export const fantomasFormatter: FormatterInfo = {
675
+ name: "fantomas",
676
+ command: ["fantomas", "$FILE"],
677
+ extensions: [".fs", ".fsi", ".fsx"],
678
+ async detect(_cwd: string) {
679
+ return (await which("fantomas")) !== null;
680
+ },
681
+ };
682
+
683
+ export const swiftformatFormatter: FormatterInfo = {
684
+ name: "swiftformat",
685
+ command: ["swiftformat", "$FILE"],
686
+ extensions: [".swift"],
687
+ async detect(_cwd: string) {
688
+ return (await which("swiftformat")) !== null;
689
+ },
690
+ };
691
+
692
+ export const styluaFormatter: FormatterInfo = {
693
+ name: "stylua",
694
+ command: ["stylua", "$FILE"],
695
+ extensions: [".lua"],
696
+ async detect(cwd: string) {
697
+ if ((await which("stylua")) === null) return false;
698
+ // Prefer explicit config but also run if binary is present in a Lua project
699
+ const configs = ["stylua.toml", ".stylua.toml"];
700
+ const found = await findUp(configs, cwd);
701
+ return found.length > 0;
702
+ },
703
+ };
704
+
705
+ export const ormoluFormatter: FormatterInfo = {
706
+ name: "ormolu",
707
+ command: ["ormolu", "--mode", "inplace", "$FILE"],
708
+ extensions: [".hs", ".lhs"],
709
+ async detect(_cwd: string) {
710
+ return (await which("ormolu")) !== null;
711
+ },
712
+ };
713
+
714
+ export const taploFormatter: FormatterInfo = {
715
+ name: "taplo",
716
+ command: ["taplo", "fmt", "$FILE"],
717
+ extensions: [".toml"],
718
+ async resolveCommand(filePath, _cwd) {
719
+ const inPath = await which("taplo");
720
+ if (inPath) return [inPath, "fmt", filePath];
721
+ return resolveManagedSmartDefaultCommand("taplo", filePath, ["fmt"]);
722
+ },
723
+ async detect(_cwd: string) {
724
+ if ((await which("taplo")) !== null) return true;
725
+ const { getToolPath } = await import("./installer/index.js");
726
+ return Boolean(await getToolPath("taplo"));
727
+ },
728
+ };
729
+
730
+ export const googleJavaFormatFormatter: FormatterInfo = {
731
+ name: "google-java-format",
732
+ command: ["google-java-format", "--replace", "$FILE"],
733
+ extensions: [".java"],
734
+ async detect(cwd: string) {
735
+ if ((await which("google-java-format")) === null) return false;
736
+ return hasGoogleJavaFormatConfig(cwd);
737
+ },
738
+ };
739
+
740
+ export const cljfmtFormatter: FormatterInfo = {
741
+ name: "cljfmt",
742
+ command: ["cljfmt", "fix", "$FILE"],
743
+ extensions: [".clj", ".cljc", ".cljs"],
744
+ async detect(cwd: string) {
745
+ if ((await which("cljfmt")) === null) return false;
746
+ return hasCljfmtConfig(cwd);
747
+ },
748
+ };
749
+
750
+ export const cmakeFormatFormatter: FormatterInfo = {
751
+ name: "cmake-format",
752
+ command: ["cmake-format", "-i", "$FILE"],
753
+ extensions: [".cmake"],
754
+ async detect(cwd: string) {
755
+ if ((await which("cmake-format")) === null) return false;
756
+ return hasCmakeFormatConfig(cwd);
757
+ },
758
+ };
759
+
760
+ export const psscriptanalyzerFormatFormatter: FormatterInfo = {
761
+ name: "psscriptanalyzer-format",
762
+ command: [
763
+ "pwsh",
764
+ "-Command",
765
+ "Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'",
766
+ ],
767
+ extensions: [".ps1", ".psm1", ".psd1"],
768
+ async resolveCommand(filePath, _cwd) {
769
+ const pwsh = (await which("pwsh")) ?? (await which("powershell"));
770
+ if (!pwsh) return null;
771
+ return [
772
+ pwsh,
773
+ "-NoProfile",
774
+ "-Command",
775
+ `$content = Get-Content -Raw '${filePath}'; $formatted = Invoke-Formatter -ScriptDefinition $content; Set-Content -Path '${filePath}' -Value $formatted`,
776
+ ];
777
+ },
778
+ async detect(_cwd: string) {
779
+ const pwsh = (await which("pwsh")) ?? (await which("powershell"));
780
+ if (!pwsh) return false;
781
+ // Check PSScriptAnalyzer module is available
782
+ const result = safeSpawn(
783
+ pwsh,
784
+ [
785
+ "-NoProfile",
786
+ "-Command",
787
+ "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
788
+ ],
789
+ { timeout: 5_000 },
790
+ );
791
+ return (result.stdout ?? "").includes("PSScriptAnalyzer");
792
+ },
793
+ };
794
+
795
+ // --- Registry ---
796
+
797
+ const ALL_FORMATTERS: FormatterInfo[] = [
798
+ biomeFormatter,
799
+ prettierFormatter,
800
+ oxfmtFormatter,
801
+ ruffFormatter,
802
+ blackFormatter,
803
+ sqlfluffFormatter,
804
+ gofmtFormatter,
805
+ rustfmtFormatter,
806
+ zigFormatter,
807
+ dartFormatter,
808
+ shfmtFormatter,
809
+ nixfmtFormatter,
810
+ mixFormatter,
811
+ ocamlformatFormatter,
812
+ clangFormatFormatter,
813
+ ktlintFormatter,
814
+ terraformFormatter,
815
+ phpCsFixerFormatter,
816
+ csharpierFormatter,
817
+ fantomasFormatter,
818
+ swiftformatFormatter,
819
+ styluaFormatter,
820
+ ormoluFormatter,
821
+ rubocopFormatter,
822
+ standardrbFormatter,
823
+ gleamFormatter,
824
+ taploFormatter,
825
+ googleJavaFormatFormatter,
826
+ cljfmtFormatter,
827
+ cmakeFormatFormatter,
828
+ psscriptanalyzerFormatFormatter,
829
+ ];
830
+
831
+ // Cache for detection results - stores array of enabled formatter names per cwd+ext
832
+ const detectionCache = new Map<string, Map<string, string[]>>();
833
+
834
+ // --- Public API ---
835
+
836
+ export async function getFormattersForFile(
837
+ filePath: string,
838
+ cwd: string,
839
+ ): Promise<FormatterInfo[]> {
840
+ const ext = path.extname(filePath).toLowerCase();
841
+ const cacheKey = `${cwd}:${ext}`;
842
+
843
+ // Check cache
844
+ let cached = detectionCache.get(cwd);
845
+ if (!cached) {
846
+ cached = new Map();
847
+ detectionCache.set(cwd, cached);
848
+ }
849
+
850
+ if (cached.has(cacheKey)) {
851
+ const enabledNames = cached.get(cacheKey);
852
+ if (!enabledNames || enabledNames.length === 0) return [];
853
+ // Return cached formatters by name (preserves priority order)
854
+ return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
855
+ }
856
+
857
+ // Detect formatters for this extension
858
+ const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext));
859
+ const formatterPolicy = getFormatterPolicyForFile(filePath);
860
+ const smartDefaultFormatterName = getSmartDefaultFormatterName(filePath);
861
+
862
+ const candidateFormatters = formatterPolicy?.formatterNames?.length
863
+ ? matching.filter((f) => formatterPolicy.formatterNames.includes(f.name))
864
+ : matching;
865
+
866
+ let selected: FormatterInfo | undefined;
867
+ if (formatterPolicy) {
868
+ const explicitlyConfigured = candidateFormatters.filter((formatter) =>
869
+ hasExplicitFormatterConfig(formatter.name, cwd),
870
+ );
871
+ if (explicitlyConfigured.length > 0) {
872
+ // A formatter with explicit project config was found — use it.
873
+ // Prefer the policy's defaultFormatter only if it has explicit config,
874
+ // otherwise pick the first explicitly-configured formatter.
875
+ selected = formatterPolicy.defaultFormatter
876
+ ? (explicitlyConfigured.find(
877
+ (f) => f.name === formatterPolicy.defaultFormatter,
878
+ ) ?? explicitlyConfigured[0])
879
+ : explicitlyConfigured[0];
880
+ } else if (smartDefaultFormatterName) {
881
+ // Reached only when explicitlyConfigured is empty, so no candidate
882
+ // has explicit config. Safe to activate the smart-default.
883
+ const smartDefaultFormatter = candidateFormatters.find(
884
+ (f) => f.name === smartDefaultFormatterName,
885
+ );
886
+ if (smartDefaultFormatter) {
887
+ const autoInstallToolId = getAutoInstallToolIdForFormatter(
888
+ smartDefaultFormatter.name,
889
+ );
890
+ if (autoInstallToolId || (await smartDefaultFormatter.detect(cwd))) {
891
+ selected = smartDefaultFormatter;
892
+ }
893
+ }
894
+ }
895
+ } else {
896
+ for (const formatter of candidateFormatters) {
897
+ try {
898
+ if (!(await hasExplicitFormatterConfig(formatter.name, cwd))) continue;
899
+ if (await formatter.detect(cwd)) {
900
+ selected = formatter;
901
+ break;
902
+ }
903
+ } catch (err) {
904
+ console.error(`[format] Detection failed for ${formatter.name}:`, err);
905
+ }
906
+ }
907
+ }
908
+
909
+ const enabled = selected ? [selected] : [];
910
+
911
+ let selectionReason: string;
912
+ if (!selected) {
913
+ selectionReason = "none";
914
+ } else if (!formatterPolicy) {
915
+ selectionReason = "detect";
916
+ } else {
917
+ selectionReason = candidateFormatters.some((f) =>
918
+ hasExplicitFormatterConfig(f.name, cwd),
919
+ )
920
+ ? "explicit-config"
921
+ : "smart-default";
922
+ }
923
+ logLatency({
924
+ type: "phase",
925
+ phase: "formatter_selected",
926
+ filePath: filePath,
927
+ durationMs: 0,
928
+ metadata: {
929
+ formatter: selected?.name ?? null,
930
+ reason: selectionReason,
931
+ cwd,
932
+ },
933
+ });
934
+
935
+ // Store the list of enabled formatter names in cache
936
+ const enabledNames = enabled.map((f) => f.name);
937
+ cached.set(cacheKey, enabledNames);
938
+ return enabled;
939
+ }
940
+
941
+ export function clearFormatterCache(): void {
942
+ detectionCache.clear();
943
+ }
944
+
945
+ export function clearFormatterRuntimeState(): void {
946
+ detectionCache.clear();
947
+ _lazyInstallAttempts.clear();
948
+ }
949
+
950
+ export async function formatFile(
951
+ filePath: string,
952
+ formatter: FormatterInfo,
953
+ ): Promise<FormatterResult> {
954
+ try {
955
+ const absolutePath = path.resolve(filePath);
956
+ const cwd = path.dirname(absolutePath);
957
+ const contentBefore = await fs.readFile(absolutePath, "utf-8");
958
+
959
+ // Resolve command: prefer local (venv/vendor/node_modules) over global
960
+ const resolved = formatter.resolveCommand
961
+ ? await formatter.resolveCommand(absolutePath, cwd)
962
+ : null;
963
+ const cmd =
964
+ resolved ??
965
+ formatter.command.map((c) => c.replace("$FILE", absolutePath));
966
+
967
+ // Run formatter without blocking the event loop.
968
+ const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
969
+ timeout: 15000,
970
+ cwd,
971
+ });
972
+
973
+ if (result.error) {
974
+ return {
975
+ success: false,
976
+ changed: false,
977
+ error: result.error.message,
978
+ };
979
+ }
980
+
981
+ // Check if content changed
982
+ const contentAfter = await fs.readFile(absolutePath, "utf-8");
983
+ const changed = contentBefore !== contentAfter;
984
+
985
+ return {
986
+ success: true,
987
+ changed,
988
+ };
989
+ } catch (err) {
990
+ return {
991
+ success: false,
992
+ changed: false,
993
+ error: err instanceof Error ? err.message : String(err),
994
+ };
995
+ }
996
+ }
997
+
998
+ export function listAllFormatters(): string[] {
999
+ return ALL_FORMATTERS.map((f) => f.name);
1000
+ }