@bonesofspring/ai-rules 0.2.23 → 0.2.24

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 (285) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -1
  3. package/bin/cli.js +32 -9
  4. package/bin/opencode-install.js +531 -0
  5. package/package.json +10 -3
  6. package/presets/_shared/core/agent-team/agent-team-orchestrator.md +2 -2
  7. package/presets/_shared/core/meta/preset-layering.md +3 -3
  8. package/presets/_shared/core/meta/preset-no-cross-stack-leakage.md +1 -1
  9. package/presets/_shared/core/meta/preset-pr-checklist.md +4 -3
  10. package/presets/_shared/core/meta/preset-token-budget.md +3 -0
  11. package/presets/_shared/core/meta/preset-twin-sync.md +16 -0
  12. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-layering.md +3 -3
  13. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  14. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  15. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-token-budget.md +3 -0
  16. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +16 -0
  17. package/presets/claude/go/rules/tooling-and-review/preset-layering.md +3 -3
  18. package/presets/claude/go/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  19. package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  20. package/presets/claude/go/rules/tooling-and-review/preset-token-budget.md +3 -0
  21. package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +16 -0
  22. package/presets/claude/ios-swift/rules/tooling-and-review/preset-layering.md +3 -3
  23. package/presets/claude/ios-swift/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  24. package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  25. package/presets/claude/ios-swift/rules/tooling-and-review/preset-token-budget.md +3 -0
  26. package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +16 -0
  27. package/presets/claude/java/rules/tooling-and-review/preset-layering.md +3 -3
  28. package/presets/claude/java/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  29. package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  30. package/presets/claude/java/rules/tooling-and-review/preset-token-budget.md +3 -0
  31. package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +16 -0
  32. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-layering.md +3 -3
  33. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  34. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  35. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-token-budget.md +3 -0
  36. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +16 -0
  37. package/presets/claude/next/REPO_AGENTS.md +10 -5
  38. package/presets/claude/next/agents/build-verifier.md +3 -1
  39. package/presets/claude/next/rules/tooling-and-review/ai-local-overlay.md +1 -1
  40. package/presets/claude/next/rules/tooling-and-review/preset-layering.md +3 -3
  41. package/presets/claude/next/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  42. package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  43. package/presets/claude/next/rules/tooling-and-review/preset-token-budget.md +3 -0
  44. package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +16 -0
  45. package/presets/claude/nuxt/rules/tooling-and-review/preset-layering.md +3 -3
  46. package/presets/claude/nuxt/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  47. package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  48. package/presets/claude/nuxt/rules/tooling-and-review/preset-token-budget.md +3 -0
  49. package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +16 -0
  50. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-layering.md +3 -3
  51. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  52. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  53. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-token-budget.md +3 -0
  54. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +16 -0
  55. package/presets/claude/php-laravel/rules/tooling-and-review/preset-layering.md +3 -3
  56. package/presets/claude/php-laravel/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  57. package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  58. package/presets/claude/php-laravel/rules/tooling-and-review/preset-token-budget.md +3 -0
  59. package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +16 -0
  60. package/presets/claude/svelte/rules/tooling-and-review/preset-layering.md +3 -3
  61. package/presets/claude/svelte/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  62. package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  63. package/presets/claude/svelte/rules/tooling-and-review/preset-token-budget.md +3 -0
  64. package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +16 -0
  65. package/presets/cursor/android-kotlin/rules/preset-layering.mdc +3 -3
  66. package/presets/cursor/android-kotlin/rules/preset-no-cross-stack-leakage.mdc +1 -1
  67. package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +4 -3
  68. package/presets/cursor/android-kotlin/rules/preset-token-budget.mdc +3 -0
  69. package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +16 -0
  70. package/presets/cursor/go/rules/preset-layering.mdc +3 -3
  71. package/presets/cursor/go/rules/preset-no-cross-stack-leakage.mdc +1 -1
  72. package/presets/cursor/go/rules/preset-pr-checklist.mdc +4 -3
  73. package/presets/cursor/go/rules/preset-token-budget.mdc +3 -0
  74. package/presets/cursor/go/rules/preset-twin-sync.mdc +16 -0
  75. package/presets/cursor/ios-swift/rules/preset-layering.mdc +3 -3
  76. package/presets/cursor/ios-swift/rules/preset-no-cross-stack-leakage.mdc +1 -1
  77. package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +4 -3
  78. package/presets/cursor/ios-swift/rules/preset-token-budget.mdc +3 -0
  79. package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +16 -0
  80. package/presets/cursor/java/rules/preset-layering.mdc +3 -3
  81. package/presets/cursor/java/rules/preset-no-cross-stack-leakage.mdc +1 -1
  82. package/presets/cursor/java/rules/preset-pr-checklist.mdc +4 -3
  83. package/presets/cursor/java/rules/preset-token-budget.mdc +3 -0
  84. package/presets/cursor/java/rules/preset-twin-sync.mdc +16 -0
  85. package/presets/cursor/mcp-ts/rules/preset-layering.mdc +3 -3
  86. package/presets/cursor/mcp-ts/rules/preset-no-cross-stack-leakage.mdc +1 -1
  87. package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +4 -3
  88. package/presets/cursor/mcp-ts/rules/preset-token-budget.mdc +3 -0
  89. package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +16 -0
  90. package/presets/cursor/next/REPO_AGENTS.md +10 -5
  91. package/presets/cursor/next/agents/build-verifier.md +3 -1
  92. package/presets/cursor/next/rules/ai-local-overlay.mdc +1 -1
  93. package/presets/cursor/next/rules/preset-layering.mdc +3 -3
  94. package/presets/cursor/next/rules/preset-no-cross-stack-leakage.mdc +1 -1
  95. package/presets/cursor/next/rules/preset-pr-checklist.mdc +4 -3
  96. package/presets/cursor/next/rules/preset-token-budget.mdc +3 -0
  97. package/presets/cursor/next/rules/preset-twin-sync.mdc +16 -0
  98. package/presets/cursor/nuxt/rules/preset-layering.mdc +3 -3
  99. package/presets/cursor/nuxt/rules/preset-no-cross-stack-leakage.mdc +1 -1
  100. package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +4 -3
  101. package/presets/cursor/nuxt/rules/preset-token-budget.mdc +3 -0
  102. package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +16 -0
  103. package/presets/cursor/php-hexagonal/rules/preset-layering.mdc +3 -3
  104. package/presets/cursor/php-hexagonal/rules/preset-no-cross-stack-leakage.mdc +1 -1
  105. package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +4 -3
  106. package/presets/cursor/php-hexagonal/rules/preset-token-budget.mdc +3 -0
  107. package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +16 -0
  108. package/presets/cursor/php-laravel/rules/preset-layering.mdc +3 -3
  109. package/presets/cursor/php-laravel/rules/preset-no-cross-stack-leakage.mdc +1 -1
  110. package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +4 -3
  111. package/presets/cursor/php-laravel/rules/preset-token-budget.mdc +3 -0
  112. package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +16 -0
  113. package/presets/cursor/svelte/rules/preset-layering.mdc +3 -3
  114. package/presets/cursor/svelte/rules/preset-no-cross-stack-leakage.mdc +1 -1
  115. package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +4 -3
  116. package/presets/cursor/svelte/rules/preset-token-budget.mdc +3 -0
  117. package/presets/cursor/svelte/rules/preset-twin-sync.mdc +16 -0
  118. package/presets/mavis/next/REPO_AGENTS.md +9 -7
  119. package/presets/mavis/next/adapter-map.json +1 -1
  120. package/presets/mavis/next/agents/build-verifier.md +3 -1
  121. package/presets/mavis/next/rules/agent-team-orchestrator.md +2 -2
  122. package/presets/mavis/next/rules/ai-local-overlay.md +1 -1
  123. package/presets/mavis/next/rules/preset-layering.md +3 -3
  124. package/presets/mavis/next/rules/preset-no-cross-stack-leakage.md +1 -1
  125. package/presets/mavis/next/rules/preset-pr-checklist.md +4 -3
  126. package/presets/mavis/next/rules/preset-token-budget.md +3 -0
  127. package/presets/mavis/next/rules/preset-twin-sync.md +16 -0
  128. package/presets/opencode/README.md +61 -0
  129. package/presets/opencode/next/MCP.md +36 -0
  130. package/presets/opencode/next/OPENCODE.md +65 -0
  131. package/presets/opencode/next/README.md +16 -0
  132. package/presets/opencode/next/REPO_AGENTS.md +97 -0
  133. package/presets/opencode/next/adapter-map.json +959 -0
  134. package/presets/opencode/next/agents/README.md +201 -0
  135. package/presets/opencode/next/agents/accessibility-reviewer.md +73 -0
  136. package/presets/opencode/next/agents/api-contract-reviewer.md +69 -0
  137. package/presets/opencode/next/agents/build-verifier.md +124 -0
  138. package/presets/opencode/next/agents/ci-investigator.md +71 -0
  139. package/presets/opencode/next/agents/code-reviewer.md +73 -0
  140. package/presets/opencode/next/agents/codebase-analyzer.md +33 -0
  141. package/presets/opencode/next/agents/debugger.md +88 -0
  142. package/presets/opencode/next/agents/feature-developer.md +109 -0
  143. package/presets/opencode/next/agents/migration-specialist.md +79 -0
  144. package/presets/opencode/next/agents/mockup-reviewer.md +65 -0
  145. package/presets/opencode/next/agents/openspec-author.md +169 -0
  146. package/presets/opencode/next/agents/performance-auditor.md +80 -0
  147. package/presets/opencode/next/agents/playwright-test-generator.md +36 -0
  148. package/presets/opencode/next/agents/playwright-test-healer.md +36 -0
  149. package/presets/opencode/next/agents/playwright-test-planner.md +33 -0
  150. package/presets/opencode/next/agents/qa-tester.md +60 -0
  151. package/presets/opencode/next/agents/security-reviewer.md +66 -0
  152. package/presets/opencode/next/agents/solution-architect.md +80 -0
  153. package/presets/opencode/next/agents/task-analyst.md +178 -0
  154. package/presets/opencode/next/agents/task-router.md +190 -0
  155. package/presets/opencode/next/agents/tech-writer.md +69 -0
  156. package/presets/opencode/next/agents/unit-test-generator.md +39 -0
  157. package/presets/opencode/next/agents/unit-test-healer.md +40 -0
  158. package/presets/opencode/next/agents/unit-test-planner.md +66 -0
  159. package/presets/opencode/next/commands/README.md +49 -0
  160. package/presets/opencode/next/commands/spec-start.md +32 -0
  161. package/presets/opencode/next/commands/task-continue.md +58 -0
  162. package/presets/opencode/next/commands/task.md +58 -0
  163. package/presets/opencode/next/commands/technical-retro.md +88 -0
  164. package/presets/opencode/next/hooks/README.md +22 -0
  165. package/presets/opencode/next/hooks/chain-team-phases.sh +845 -0
  166. package/presets/opencode/next/opencode.json +33 -0
  167. package/presets/opencode/next/rules/README.md +20 -0
  168. package/presets/opencode/next/rules/agent-team-intake.md +11 -0
  169. package/presets/opencode/next/rules/agent-team-orchestrator.md +214 -0
  170. package/presets/opencode/next/rules/ai-local-overlay.md +37 -0
  171. package/presets/opencode/next/rules/anti-sycophancy-discipline.md +23 -0
  172. package/presets/opencode/next/rules/api-services.md +72 -0
  173. package/presets/opencode/next/rules/architecture-boundaries-ui.md +9 -0
  174. package/presets/opencode/next/rules/architecture-boundaries.md +86 -0
  175. package/presets/opencode/next/rules/arrow-functions.md +25 -0
  176. package/presets/opencode/next/rules/code-quality-and-refactoring.md +40 -0
  177. package/presets/opencode/next/rules/code-review-mr.md +40 -0
  178. package/presets/opencode/next/rules/css-property-order-stylelint.md +5 -0
  179. package/presets/opencode/next/rules/design-guidance.md +99 -0
  180. package/presets/opencode/next/rules/feature-delivery-workflow.md +49 -0
  181. package/presets/opencode/next/rules/http-client.md +55 -0
  182. package/presets/opencode/next/rules/layer-barrel-exports.md +68 -0
  183. package/presets/opencode/next/rules/mcp-usage.md +86 -0
  184. package/presets/opencode/next/rules/mockup-review.md +107 -0
  185. package/presets/opencode/next/rules/navigation-router-stack.md +56 -0
  186. package/presets/opencode/next/rules/navigation-router-ui.md +10 -0
  187. package/presets/opencode/next/rules/next-app-core.md +29 -0
  188. package/presets/opencode/next/rules/next-app-router.md +44 -0
  189. package/presets/opencode/next/rules/no-props-spread.md +56 -0
  190. package/presets/opencode/next/rules/no-type-assertion-as-import-export.md +51 -0
  191. package/presets/opencode/next/rules/package-manager.md +11 -0
  192. package/presets/opencode/next/rules/playwright-agents.md +67 -0
  193. package/presets/opencode/next/rules/post-change-lint.md +35 -0
  194. package/presets/opencode/next/rules/preset-layering.md +27 -0
  195. package/presets/opencode/next/rules/preset-no-cross-stack-leakage.md +90 -0
  196. package/presets/opencode/next/rules/preset-pr-checklist.md +36 -0
  197. package/presets/opencode/next/rules/preset-token-budget.md +39 -0
  198. package/presets/opencode/next/rules/preset-twin-sync.md +81 -0
  199. package/presets/opencode/next/rules/product-specs-authoring.md +112 -0
  200. package/presets/opencode/next/rules/product-specs.md +56 -0
  201. package/presets/opencode/next/rules/public-imports.md +50 -0
  202. package/presets/opencode/next/rules/react-a11y-coding.md +31 -0
  203. package/presets/opencode/next/rules/react-next-anti-hallucination.md +60 -0
  204. package/presets/opencode/next/rules/react-ui.md +105 -0
  205. package/presets/opencode/next/rules/reference-features.md +45 -0
  206. package/presets/opencode/next/rules/security-next.md +45 -0
  207. package/presets/opencode/next/rules/store-rtk.md +78 -0
  208. package/presets/opencode/next/rules/technical-retro.md +11 -0
  209. package/presets/opencode/next/rules/tests-e2e-structure.md +72 -0
  210. package/presets/opencode/next/rules/tests-unit.md +72 -0
  211. package/presets/opencode/next/rules/types-jsdoc.md +43 -0
  212. package/presets/opencode/next/rules/ux-design.md +99 -0
  213. package/presets/opencode/next/skills/README.md +21 -0
  214. package/presets/opencode/next/skills/ci-investigation/SKILL.md +44 -0
  215. package/presets/opencode/next/skills/code-review/SKILL.md +44 -0
  216. package/presets/opencode/next/skills/debug-investigation/SKILL.md +59 -0
  217. package/presets/opencode/next/skills/feature-delivery/SKILL.md +39 -0
  218. package/presets/opencode/next/skills/mockup-review/SKILL.md +29 -0
  219. package/presets/opencode/next/skills/playwright-e2e/SKILL.md +33 -0
  220. package/presets/opencode/next/skills/react-performance/SKILL.md +44 -0
  221. package/presets/opencode/next/skills/spec-authoring/SKILL.md +26 -0
  222. package/presets/opencode/next/skills/spec-discovery/SKILL.md +58 -0
  223. package/presets/opencode/next/skills/technical-retro/SKILL.md +54 -0
  224. package/presets/opencode/next/skills/unit-testing/SKILL.md +34 -0
  225. package/presets/opencode/next/skills/ux-design/SKILL.md +36 -0
  226. package/presets/opencode/next/skills/write-adr/SKILL.md +41 -0
  227. package/presets/opencode/next/team/README.md +148 -0
  228. package/presets/opencode/next/team/conventions.md +21 -0
  229. package/presets/opencode/next/team/fixtures/artifact-manifest.example.json +79 -0
  230. package/presets/opencode/next/team/fixtures/bugfix-standard.json +25 -0
  231. package/presets/opencode/next/team/fixtures/feature-full.json +29 -0
  232. package/presets/opencode/next/team/fixtures/feature-light.json +27 -0
  233. package/presets/opencode/next/team/fixtures/feature-with-figma-url.json +17 -0
  234. package/presets/opencode/next/team/fixtures/feature-with-ux-no-figma.json +17 -0
  235. package/presets/opencode/next/team/fixtures/memlog.example.md +9 -0
  236. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/human-gate/metrics.json +69 -0
  237. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/missing-ledger/status.json +5 -0
  238. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/parallel-receipts/metrics.json +79 -0
  239. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/serial-retry/metrics.json +124 -0
  240. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/validation-failed/metrics.json +124 -0
  241. package/presets/opencode/next/team/fixtures/metrics.example.json +54 -0
  242. package/presets/opencode/next/team/fixtures/mockup-review-fail-embedded-status.json +8 -0
  243. package/presets/opencode/next/team/fixtures/mockup-review-standalone.json +14 -0
  244. package/presets/opencode/next/team/fixtures/ux-design-standalone.json +15 -0
  245. package/presets/opencode/next/team/tasks/.gitkeep +0 -0
  246. package/presets/opencode/platform-contract.json +131 -0
  247. package/scripts/capture-cli-golden.mjs +5 -1
  248. package/scripts/check-chain-hook-coverage.sh +1 -0
  249. package/scripts/check-opencode-adapter-map.mjs +294 -0
  250. package/scripts/check-opencode-local-overlay.mjs +56 -0
  251. package/scripts/check-opencode-platform-contract.mjs +388 -0
  252. package/scripts/check-opencode-token-budget.mjs +48 -0
  253. package/scripts/check-preset-leakage.sh +30 -1
  254. package/scripts/check-preset-structure.sh +45 -0
  255. package/scripts/check-task-router-intents.sh +4 -4
  256. package/scripts/fixtures/cli-golden/claude/next.json +8 -8
  257. package/scripts/fixtures/cli-golden/cursor/next.json +8 -8
  258. package/scripts/fixtures/cli-golden/mavis/next.json +9 -9
  259. package/scripts/fixtures/cli-golden/opencode/next.json +125 -0
  260. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/cursor/AGENTS.md +1 -0
  261. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/cursor/rules/extra.mdc +1 -0
  262. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/cursor/rules/sample.mdc +2 -0
  263. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/opencode/OPENCODE.md +1 -0
  264. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/opencode/adapter-map.json +26 -0
  265. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/opencode/rules/sample.md +2 -0
  266. package/scripts/fixtures/opencode-adapter-map/valid/cursor/AGENTS.md +1 -0
  267. package/scripts/fixtures/opencode-adapter-map/valid/cursor/rules/sample.mdc +2 -0
  268. package/scripts/fixtures/opencode-adapter-map/valid/opencode/OPENCODE.md +1 -0
  269. package/scripts/fixtures/opencode-adapter-map/valid/opencode/adapter-map.json +26 -0
  270. package/scripts/fixtures/opencode-adapter-map/valid/opencode/rules/sample.md +2 -0
  271. package/scripts/fixtures/opencode-platform-contract/invalid-home-destination.json +131 -0
  272. package/scripts/fixtures/opencode-platform-contract/invalid-missing-capability.json +126 -0
  273. package/scripts/fixtures/opencode-platform-contract/invalid-released-unknown-stack.json +133 -0
  274. package/scripts/fixtures/opencode-platform-contract/invalid-unknown-native-path.json +131 -0
  275. package/scripts/fixtures/opencode-platform-contract/invalid-version-pin.json +132 -0
  276. package/scripts/fixtures/opencode-platform-contract/valid.json +131 -0
  277. package/scripts/generate-mavis-next.mjs +9 -7
  278. package/scripts/generate-opencode-next.mjs +831 -0
  279. package/scripts/lib/discover-hooks.mjs +23 -1
  280. package/scripts/sync-dogfood-mavis.sh +90 -0
  281. package/scripts/sync-dogfood-opencode.sh +85 -0
  282. package/scripts/test-agent-task-metrics-hooks.mjs +4 -1
  283. package/scripts/test-chain-team-phases-coverage.mjs +6 -1
  284. package/scripts/test-cli-init-clean.mjs +111 -0
  285. package/scripts/test-opencode-context-budget.mjs +53 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### OpenCode platform (experimental Next-only)
6
+
7
+ - New platform **`opencode`**: `npx @bonesofspring/ai-rules init opencode --preset next` installs **`.opencode/**`** plus root `AGENTS.md` and merges MCP into **`opencode.json`**. `clean opencode` is manifest/hash-safe. The CLI never writes `~/.config/opencode/**`.
8
+ - Source: `presets/opencode/next/` derived from `presets/cursor/next` (`adapter-map.json`, `generate-opencode-next.mjs`). `platform-contract.json` `releasedStacks: ["next"]`; nine other stacks deferred.
9
+ - Native OpenCode discovery for agents, skills, and slash commands. Rules under `.opencode/rules/` are lazy-loaded from `AGENTS.md` (no `alwaysApply`/`globs` engine). Pipeline hook is manual `chain-team-phases.sh` (no `hooks.json` plugin in v1).
10
+ - Docs: [`docs/OPENCODE.md`](docs/OPENCODE.md) (consumer), [`docs/OPENCODE-COMPATIBILITY.md`](docs/OPENCODE-COMPATIBILITY.md) (docs-floor evidence). Gates: `check-opencode-*`, `test-opencode-context-budget` wired into `check:preset-structure`.
11
+ - Review follow-up: team-agent write-scope lead-in is role-specific; consumer `OPENCODE.md` no longer uses broken relative doc links; orchestrator uses OpenCode `mode`/`permission` wording; `build-verifier` preset-structure lists OpenCode/Mavis derive gates; Cursor/Claude `REPO_AGENTS.md` keep OpenCode/Mavis pointers so mixed-tool `init` does not drop them.
12
+ - Consumers: re-run `npx @bonesofspring/ai-rules init opencode --preset next` after publish. Cursor/Claude/Mavis installs unchanged.
13
+
5
14
  ### Mavis platform (MiniMax Code, experimental Next-only)
6
15
 
7
16
  - New platform **`mavis`** (`mcode` runtime): `npx @bonesofspring/ai-rules init mavis --preset next` installs **`.mavis/**`** plus root `AGENTS.md`. `clean mavis` is manifest/hash-safe. MCP merges to **`.mavis/mcp.json`** only — the CLI never writes `~/.minimax/**`.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bonesofspring/ai-rules
2
2
 
3
- Presets for Cursor, Claude Code, and **Mavis / MiniMax Code (experimental Next-only)**:
3
+ Presets for Cursor, Claude Code, **Mavis / MiniMax Code (experimental Next-only)**, and **OpenCode (experimental Next-only)**:
4
4
 
5
5
  Node 18+.
6
6
 
@@ -9,8 +9,10 @@ Node 18+.
9
9
  npx @bonesofspring/ai-rules init cursor --preset next
10
10
  npx @bonesofspring/ai-rules init claude --preset next
11
11
  npx @bonesofspring/ai-rules init mavis --preset next
12
+ npx @bonesofspring/ai-rules init opencode --preset next
12
13
  npx @bonesofspring/ai-rules clean cursor
13
14
  npx @bonesofspring/ai-rules clean mavis
15
+ npx @bonesofspring/ai-rules clean opencode
14
16
  ```
15
17
 
16
18
  Global: `npm i -g @bonesofspring/ai-rules`, then `ai-rules ...`.
@@ -58,6 +60,8 @@ Where things go on `init`:
58
60
 
59
61
  **Mavis (MiniMax Code, experimental Next-only):** `ai-rules init mavis --preset next` writes ai-rules-owned **`.mavis/**`** plus root `AGENTS.md`. MCP merges into **`.mavis/mcp.json`** only — the CLI never writes `~/.minimax/**`. `clean mavis` is manifest-owned: unchanged hashes are removed; modified files stay. See [`docs/MAVIS.md`](docs/MAVIS.md), [`presets/mavis/README.md`](presets/mavis/README.md), and [`docs/MAVIS-COMPATIBILITY.md`](docs/MAVIS-COMPATIBILITY.md).
60
62
 
63
+ **OpenCode (experimental Next-only):** `ai-rules init opencode --preset next` writes native **`.opencode/**`** plus root `AGENTS.md` and merges MCP into **`opencode.json`**. `clean opencode` is manifest-owned. The CLI never writes `~/.config/opencode/**`. Last `init` wins on root `AGENTS.md` if several tools are installed. See [`docs/OPENCODE.md`](docs/OPENCODE.md), [`presets/opencode/README.md`](presets/opencode/README.md), and [`docs/OPENCODE-COMPATIBILITY.md`](docs/OPENCODE-COMPATIBILITY.md).
64
+
61
65
  Skills of note: `write-adr` (all stacks); `spec-authoring` (Generate/Bootstrap `docs/specs`); `mockup-review` (**next** / **nuxt** / **svelte** / **ios-swift** / **android-kotlin** — Figma mockup reviewer via agent `mockup-reviewer`); `ux-design` (**next** / **nuxt** / **svelte** / **ios-swift** / **android-kotlin** — behavioral UX via `task-analyst`, no agent); `react-performance` (**next only**); `vue-performance` (**nuxt only**); `svelte-performance` (**svelte only**). Optional command `/spec-start`. Living gotchas: `team/conventions.md`. Opt-in rule samples eval: `yarn check:rule-eval` (workflow `rule-eval.yml`; not part of `check:preset-structure`). Optional consumer specs check: `yarn check:product-specs` (WARN default; `--strict` FAIL).
62
66
 
63
67
  ## Product specs (`docs/specs/`)
package/bin/cli.js CHANGED
@@ -5,11 +5,12 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { cmdInitMavis, cmdCleanMavis } = require('./mavis-install');
8
+ const { cmdInitOpencode, cmdCleanOpencode } = require('./opencode-install');
8
9
 
9
10
  /** Single path segment: letters, digits, dot, underscore, hyphen; no separators or "..". */
10
11
  const PRESET_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
11
12
  const DEFAULT_PRESET = 'next';
12
- const TOOLS = new Set(['cursor', 'claude', 'mavis']);
13
+ const TOOLS = new Set(['cursor', 'claude', 'mavis', 'opencode']);
13
14
 
14
15
  const PRESETS = {
15
16
  cursor: {
@@ -52,9 +53,9 @@ function printHelp() {
52
53
  console.log(`@bonesofspring/ai-rules
53
54
 
54
55
  Usage:
55
- ai-rules init <cursor|claude|mavis> [--preset <name>]
56
- ai-rules init <cursor|claude|mavis> <preset-name>
57
- ai-rules clean <cursor|claude|mavis> [--cwd <dir>]
56
+ ai-rules init <cursor|claude|mavis|opencode> [--preset <name>]
57
+ ai-rules init <cursor|claude|mavis|opencode> <preset-name>
58
+ ai-rules clean <cursor|claude|mavis|opencode> [--cwd <dir>]
58
59
 
59
60
  Options:
60
61
  --preset <name> Preset name (default: ${DEFAULT_PRESET})
@@ -68,7 +69,8 @@ Notes:
68
69
  Cursor init copies rules, commands, agents, hooks, skills, team, hooks.json, BUGBOT.md, AGENTS.md → .cursor/AGENTS.md, MCP.md, mcp.json → .cursor/mcp.json.
69
70
  Claude init copies rules, commands, skills, agents, hooks, team, CLAUDE.md, MCP.md, and mcp.json → .mcp.json (project root).
70
71
  Mavis init copies into .mavis/ (ai-rules-owned) plus REPO_AGENTS.md → <cwd>/AGENTS.md; MCP merges into .mavis/mcp.json only (never ~/.minimax/**).
71
- Both tools also copy REPO_AGENTS.md → <cwd>/AGENTS.md (repo-root cross-tool agents file; overwrite).
72
+ OpenCode init copies into .opencode/ plus REPO_AGENTS.md → <cwd>/AGENTS.md; MCP merges into <cwd>/opencode.json (never ~/.config/opencode/**).
73
+ All init targets also copy REPO_AGENTS.md → <cwd>/AGENTS.md (repo-root cross-tool agents file; overwrite).
72
74
  When <cwd>/docs/specs/ is absent, init also seeds INDEX.md stub + _templates/* from package assets (never overwrites an existing INDEX).
73
75
  clean removes tool dirs/files and <cwd>/AGENTS.md (shared across tools — re-init the other tool to restore).
74
76
  Command forms: "init cursor --preset next", "init cursor next", "--preset next init cursor", "cursor init --preset next".
@@ -96,7 +98,9 @@ Examples:
96
98
  npx @bonesofspring/ai-rules init claude --preset php-hexagonal
97
99
  npx @bonesofspring/ai-rules init claude --preset php-laravel
98
100
  npx @bonesofspring/ai-rules init mavis --preset next
101
+ npx @bonesofspring/ai-rules init opencode --preset next
99
102
  npx @bonesofspring/ai-rules clean mavis
103
+ npx @bonesofspring/ai-rules clean opencode
100
104
  npx @bonesofspring/ai-rules clean cursor
101
105
  `);
102
106
  }
@@ -501,9 +505,21 @@ function cmdInit(tool, preset, cwdOption) {
501
505
  });
502
506
  return;
503
507
  }
508
+ if (tool === 'opencode') {
509
+ cmdInitOpencode({
510
+ preset,
511
+ cwdOption,
512
+ helpers: {
513
+ resolveTargetCwd,
514
+ seedDocsSpecsIfAbsent,
515
+ getPackageRoot,
516
+ },
517
+ });
518
+ return;
519
+ }
504
520
  const cfg = PRESETS[tool];
505
521
  if (!cfg) {
506
- console.error(`Unknown tool: ${tool}. Use cursor, claude, or mavis.`);
522
+ console.error(`Unknown tool: ${tool}. Use cursor, claude, mavis, or opencode.`);
507
523
  process.exit(1);
508
524
  }
509
525
 
@@ -689,9 +705,16 @@ function cmdClean(tool, cwdOption) {
689
705
  });
690
706
  return;
691
707
  }
708
+ if (tool === 'opencode') {
709
+ cmdCleanOpencode({
710
+ cwdOption,
711
+ helpers: { resolveTargetCwd },
712
+ });
713
+ return;
714
+ }
692
715
  const cfg = PRESETS[tool];
693
716
  if (!cfg) {
694
- console.error(`Unknown tool: ${tool}. Use cursor, claude, or mavis.`);
717
+ console.error(`Unknown tool: ${tool}. Use cursor, claude, mavis, or opencode.`);
695
718
  process.exit(1);
696
719
  }
697
720
  const cwd = resolveTargetCwd(cwdOption);
@@ -761,7 +784,7 @@ function main() {
761
784
 
762
785
  if (command === 'init') {
763
786
  if (!tool || !TOOLS.has(tool)) {
764
- console.error('Missing or invalid tool. Use: init cursor|claude|mavis [--preset next]');
787
+ console.error('Missing or invalid tool. Use: init cursor|claude|mavis|opencode [--preset next]');
765
788
  process.exit(1);
766
789
  }
767
790
  cmdInit(tool, preset, parsed.cwd);
@@ -769,7 +792,7 @@ function main() {
769
792
  }
770
793
  if (command === 'clean') {
771
794
  if (!tool || !TOOLS.has(tool)) {
772
- console.error('Missing or invalid tool. Use: clean cursor|claude|mavis');
795
+ console.error('Missing or invalid tool. Use: clean cursor|claude|mavis|opencode');
773
796
  process.exit(1);
774
797
  }
775
798
  cmdClean(tool, parsed.cwd);
@@ -0,0 +1,531 @@
1
+ /**
2
+ * Manifest-owned OpenCode init/clean. Does not write ~/.config/opencode/**.
3
+ */
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const MANIFEST_REL = path.join('.opencode', '.ai-rules-install.json');
9
+ const CONTRACT_REL = path.join('presets', 'opencode', 'platform-contract.json');
10
+ const SKIP_STACK_ROOT = new Set(['adapter-map.json', 'README.md']);
11
+
12
+ function sha256File(filePath) {
13
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
14
+ }
15
+
16
+ function failPhase() {
17
+ return process.env.AI_RULES_OPENCODE_FAIL_PHASE || '';
18
+ }
19
+
20
+ function throwIfPhase(name) {
21
+ if (failPhase() === name) {
22
+ throw new Error(`injected failure at ${name}`);
23
+ }
24
+ }
25
+
26
+ function loadJson(filePath) {
27
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
28
+ }
29
+
30
+ function walkFiles(root) {
31
+ const files = [];
32
+ const visit = (dir) => {
33
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
34
+ const full = path.join(dir, entry.name);
35
+ if (entry.isDirectory()) {
36
+ visit(full);
37
+ } else if (entry.isFile()) {
38
+ files.push(path.relative(root, full).split(path.sep).join('/'));
39
+ }
40
+ }
41
+ };
42
+ if (fs.existsSync(root)) {
43
+ visit(root);
44
+ }
45
+ return files.sort();
46
+ }
47
+
48
+ function writeAtomicCopy(source, dest) {
49
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
50
+ const tmp = `${dest}.ai-rules-tmp`;
51
+ fs.copyFileSync(source, tmp);
52
+ if (dest.endsWith('.sh')) {
53
+ fs.chmodSync(tmp, 0o755);
54
+ }
55
+ fs.renameSync(tmp, dest);
56
+ }
57
+
58
+ function writeAtomicText(dest, text) {
59
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
60
+ const tmp = `${dest}.ai-rules-tmp`;
61
+ fs.writeFileSync(tmp, text);
62
+ fs.renameSync(tmp, dest);
63
+ }
64
+
65
+ function loadContract(packageRoot) {
66
+ const contractPath = path.join(packageRoot, CONTRACT_REL);
67
+ if (!fs.existsSync(contractPath)) {
68
+ throw new Error(`OpenCode platform contract missing: ${contractPath}`);
69
+ }
70
+ return loadJson(contractPath);
71
+ }
72
+
73
+ function packageVersion(packageRoot) {
74
+ return loadJson(path.join(packageRoot, 'package.json')).version;
75
+ }
76
+
77
+ function isPlainObject(value) {
78
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
79
+ }
80
+
81
+ function ownKeys(object) {
82
+ return Object.keys(object).filter((key) => Object.hasOwn(object, key));
83
+ }
84
+
85
+ function buildPlan({ contract, sourceBase, cwd }) {
86
+ const files = [];
87
+ const seen = new Set();
88
+ const addFile = (source, destRel) => {
89
+ if (seen.has(destRel)) {
90
+ return;
91
+ }
92
+ seen.add(destRel);
93
+ files.push({
94
+ source,
95
+ dest: path.join(cwd, destRel),
96
+ rel: destRel,
97
+ });
98
+ };
99
+
100
+ for (const entry of contract.entryFiles || []) {
101
+ const source = path.join(sourceBase, entry.source);
102
+ if (fs.existsSync(source)) {
103
+ addFile(source, entry.destination);
104
+ }
105
+ }
106
+
107
+ for (const mapping of contract.directoryMappings || []) {
108
+ const sourceDir = path.join(sourceBase, mapping.source);
109
+ if (!fs.existsSync(sourceDir)) {
110
+ continue;
111
+ }
112
+ for (const rel of walkFiles(sourceDir)) {
113
+ addFile(path.join(sourceDir, rel), `${mapping.destination}/${rel}`);
114
+ }
115
+ }
116
+
117
+ const mcpMd = path.join(sourceBase, 'MCP.md');
118
+ if (fs.existsSync(mcpMd)) {
119
+ addFile(mcpMd, path.join(contract.managedRoot, 'MCP.md'));
120
+ }
121
+
122
+ const mcpSource = path.join(sourceBase, contract.mcp.source);
123
+ const mcp = fs.existsSync(mcpSource)
124
+ ? {
125
+ source: mcpSource,
126
+ dest: path.join(cwd, contract.mcp.fallbackDestination),
127
+ rel: contract.mcp.fallbackDestination,
128
+ }
129
+ : null;
130
+
131
+ for (const skip of SKIP_STACK_ROOT) {
132
+ const skipped = path.join(sourceBase, skip);
133
+ if (fs.existsSync(skipped)) {
134
+ const accidental = files.find((item) => item.source === skipped);
135
+ if (accidental) {
136
+ throw new Error(`refusing to install stack-root ${skip}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ return { files, mcp };
142
+ }
143
+
144
+ function capabilityModes(contract) {
145
+ const modes = {};
146
+ for (const [key, value] of Object.entries(contract.nativeCapabilities || {})) {
147
+ modes[key] = value.status;
148
+ }
149
+ return modes;
150
+ }
151
+
152
+ function shippedMcpIds(mcpSource) {
153
+ if (!mcpSource || !fs.existsSync(mcpSource)) {
154
+ return [];
155
+ }
156
+ const parsed = loadJson(mcpSource);
157
+ if (!parsed || typeof parsed.mcp !== 'object' || parsed.mcp === null) {
158
+ return [];
159
+ }
160
+ return Object.keys(parsed.mcp);
161
+ }
162
+
163
+ function previousManifest(cwd) {
164
+ const manifestPath = path.join(cwd, MANIFEST_REL);
165
+ if (!fs.existsSync(manifestPath)) {
166
+ return null;
167
+ }
168
+ try {
169
+ return loadJson(manifestPath);
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ function mergeServerEntry(existingEntry, presetEntry) {
176
+ if (!isPlainObject(existingEntry) || !isPlainObject(presetEntry)) {
177
+ return presetEntry;
178
+ }
179
+ const merged = { ...existingEntry, ...presetEntry };
180
+ for (const nested of ['env', 'headers', 'environment']) {
181
+ const existingNested = existingEntry[nested];
182
+ const presetNested = presetEntry[nested];
183
+ if (isPlainObject(existingNested) && isPlainObject(presetNested)) {
184
+ merged[nested] = { ...existingNested, ...presetNested };
185
+ } else if (isPlainObject(existingNested) && presetNested === undefined) {
186
+ merged[nested] = existingNested;
187
+ }
188
+ }
189
+ return merged;
190
+ }
191
+
192
+ function mergeOpencodeConfig(presetObj, existingObj) {
193
+ const preset = isPlainObject(presetObj) ? presetObj : {};
194
+ const existing = isPlainObject(existingObj) ? existingObj : {};
195
+ const existingServersInvalid = Object.hasOwn(existing, 'mcp') && !isPlainObject(existing.mcp);
196
+ const presetMcp = isPlainObject(preset.mcp) ? preset.mcp : {};
197
+ const existingMcp =
198
+ !existingServersInvalid && isPlainObject(existing.mcp) ? existing.mcp : {};
199
+ const extrasKept = ownKeys(existingMcp).filter((key) => !Object.hasOwn(presetMcp, key));
200
+ const mergedMcp = { ...existingMcp };
201
+ for (const key of ownKeys(presetMcp)) {
202
+ mergedMcp[key] = mergeServerEntry(existingMcp[key], presetMcp[key]);
203
+ }
204
+ const merged = { ...existing };
205
+ if (!merged.$schema && preset.$schema) {
206
+ merged.$schema = preset.$schema;
207
+ }
208
+ merged.mcp = mergedMcp;
209
+ return { merged, extrasKept, existingServersInvalid };
210
+ }
211
+
212
+ function backupFile(destAbs) {
213
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
214
+ const bak = `${destAbs}.bak.${stamp}`;
215
+ fs.copyFileSync(destAbs, bak);
216
+ return bak;
217
+ }
218
+
219
+ function installOpencodeJson(mcpFrom, mcpDestAbs, destLabel) {
220
+ const presetObj = loadJson(mcpFrom);
221
+ if (!fs.existsSync(mcpDestAbs)) {
222
+ fs.mkdirSync(path.dirname(mcpDestAbs), { recursive: true });
223
+ fs.copyFileSync(mcpFrom, mcpDestAbs);
224
+ console.log(`Copied ${path.basename(mcpFrom)} → ${destLabel}`);
225
+ return;
226
+ }
227
+ let existingObj;
228
+ try {
229
+ existingObj = loadJson(mcpDestAbs);
230
+ } catch {
231
+ const bak = backupFile(mcpDestAbs);
232
+ fs.copyFileSync(mcpFrom, mcpDestAbs);
233
+ console.warn(`Existing ${destLabel} was not valid JSON; backed up to ${bak} and replaced.`);
234
+ return;
235
+ }
236
+ if (!isPlainObject(existingObj)) {
237
+ const bak = backupFile(mcpDestAbs);
238
+ fs.copyFileSync(mcpFrom, mcpDestAbs);
239
+ console.warn(`Existing ${destLabel} was not a JSON object; backed up to ${bak} and replaced.`);
240
+ return;
241
+ }
242
+ const { merged, extrasKept, existingServersInvalid } = mergeOpencodeConfig(
243
+ presetObj,
244
+ existingObj,
245
+ );
246
+ if (existingServersInvalid) {
247
+ const bak = backupFile(mcpDestAbs);
248
+ console.warn(
249
+ `Existing ${destLabel} mcp was not a JSON object; backed up to ${bak} and merging as empty extras.`,
250
+ );
251
+ }
252
+ writeAtomicText(mcpDestAbs, `${JSON.stringify(merged, null, 2)}\n`);
253
+ if (extrasKept.length > 0) {
254
+ console.log(
255
+ `Merged ${path.basename(mcpFrom)} → ${destLabel} (kept extra servers: ${extrasKept.join(', ')})`,
256
+ );
257
+ } else {
258
+ console.log(`Merged ${path.basename(mcpFrom)} → ${destLabel}`);
259
+ }
260
+ }
261
+
262
+ function restoreRollback(overwritten, created, rollbackRoot, managedRoot, rootExisted) {
263
+ for (const item of overwritten) {
264
+ try {
265
+ if (fs.existsSync(item.backup)) {
266
+ fs.mkdirSync(path.dirname(item.dest), { recursive: true });
267
+ fs.copyFileSync(item.backup, item.dest);
268
+ }
269
+ } catch {
270
+ // Best-effort restore; caller already reports the failure.
271
+ }
272
+ }
273
+ const createdPaths = [...created].sort((a, b) => b.length - a.length);
274
+ for (const dest of createdPaths) {
275
+ try {
276
+ if (!fs.existsSync(dest)) {
277
+ continue;
278
+ }
279
+ const stat = fs.lstatSync(dest);
280
+ if (stat.isDirectory()) {
281
+ fs.rmSync(dest, { recursive: true, force: true });
282
+ } else {
283
+ fs.unlinkSync(dest);
284
+ }
285
+ } catch {
286
+ // Best-effort.
287
+ }
288
+ }
289
+ if (fs.existsSync(rollbackRoot)) {
290
+ fs.rmSync(rollbackRoot, { recursive: true, force: true });
291
+ }
292
+ if (!rootExisted && fs.existsSync(managedRoot)) {
293
+ fs.rmSync(managedRoot, { recursive: true, force: true });
294
+ } else if (rootExisted && fs.existsSync(managedRoot)) {
295
+ removeEmptyDirs(managedRoot);
296
+ }
297
+ }
298
+
299
+ function cmdInitOpencode({ preset, cwdOption, helpers }) {
300
+ const { resolveTargetCwd, seedDocsSpecsIfAbsent, getPackageRoot } = helpers;
301
+ const packageRoot = getPackageRoot();
302
+ const cwd = resolveTargetCwd(cwdOption);
303
+ const contract = loadContract(packageRoot);
304
+ const stack = preset || 'next';
305
+ const releasedStacks = Array.isArray(contract.releasedStacks) ? contract.releasedStacks : [];
306
+ if (releasedStacks.length > 0 && !releasedStacks.includes(stack)) {
307
+ console.error(`Preset "${stack}" for opencode is not released yet.`);
308
+ console.error(`Released stacks: ${releasedStacks.join(', ')}`);
309
+ process.exit(1);
310
+ }
311
+ const sourceBase = path.join(packageRoot, contract.sourceRoot, stack);
312
+ if (!fs.existsSync(sourceBase)) {
313
+ console.error(`Preset "${stack}" for opencode not found.`);
314
+ console.error(`Looked in: ${sourceBase}`);
315
+ process.exit(1);
316
+ }
317
+
318
+ const managedRoot = path.join(cwd, contract.managedRoot);
319
+ const rootExisted = fs.existsSync(managedRoot);
320
+ const docsSpecsDir = path.join(cwd, 'docs', 'specs');
321
+ const docsSpecsExisted = fs.existsSync(docsSpecsDir);
322
+ const prior = previousManifest(cwd);
323
+ const runId = new Date().toISOString().replace(/[:.]/g, '-');
324
+ const rollbackRoot = path.join(managedRoot, '.rollback', runId);
325
+ const created = new Set();
326
+ const overwritten = [];
327
+
328
+ try {
329
+ throwIfPhase('validate');
330
+ const plan = buildPlan({ contract, sourceBase, cwd });
331
+ for (const item of plan.files) {
332
+ if (!fs.existsSync(item.source)) {
333
+ throw new Error(`Source missing: ${item.source}`);
334
+ }
335
+ }
336
+
337
+ throwIfPhase('snapshot');
338
+ for (const item of plan.files) {
339
+ if (!fs.existsSync(item.dest)) {
340
+ continue;
341
+ }
342
+ const backup = path.join(rollbackRoot, item.rel);
343
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
344
+ fs.copyFileSync(item.dest, backup);
345
+ overwritten.push({ dest: item.dest, backup });
346
+ const priorHash = prior && prior.managedFiles ? prior.managedFiles[item.rel] : null;
347
+ const currentHash = sha256File(item.dest);
348
+ if (priorHash && priorHash !== currentHash) {
349
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
350
+ const reported = `${item.dest}.bak.${stamp}`;
351
+ fs.copyFileSync(item.dest, reported);
352
+ console.warn(`Modified managed file ${item.rel} backed up to ${reported} before overwrite.`);
353
+ }
354
+ }
355
+ if (plan.mcp && fs.existsSync(plan.mcp.dest)) {
356
+ const backup = path.join(rollbackRoot, plan.mcp.rel);
357
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
358
+ fs.copyFileSync(plan.mcp.dest, backup);
359
+ overwritten.push({ dest: plan.mcp.dest, backup });
360
+ }
361
+
362
+ throwIfPhase('write');
363
+ for (const item of plan.files) {
364
+ if (!fs.existsSync(item.dest)) {
365
+ created.add(item.dest);
366
+ }
367
+ writeAtomicCopy(item.source, item.dest);
368
+ console.log(`Copied ${path.basename(item.source)} → ${item.rel}`);
369
+ }
370
+
371
+ throwIfPhase('mcp');
372
+ if (plan.mcp) {
373
+ if (!fs.existsSync(plan.mcp.dest)) {
374
+ created.add(plan.mcp.dest);
375
+ }
376
+ installOpencodeJson(plan.mcp.source, plan.mcp.dest, plan.mcp.rel);
377
+ }
378
+
379
+ throwIfPhase('docs');
380
+ const seeded = seedDocsSpecsIfAbsent(packageRoot, cwd);
381
+ if (seeded && !docsSpecsExisted) {
382
+ created.add(docsSpecsDir);
383
+ }
384
+
385
+ throwIfPhase('manifest');
386
+ const managedFiles = {};
387
+ for (const item of plan.files) {
388
+ managedFiles[item.rel] = sha256File(item.dest);
389
+ }
390
+ if (plan.mcp && fs.existsSync(plan.mcp.dest)) {
391
+ managedFiles[plan.mcp.rel] = sha256File(plan.mcp.dest);
392
+ }
393
+ const manifest = {
394
+ schemaVersion: 1,
395
+ packageVersion: packageVersion(packageRoot),
396
+ platform: 'opencode',
397
+ preset: stack,
398
+ installedAt: new Date().toISOString(),
399
+ managedFiles,
400
+ managedDirectories: (contract.directoryMappings || []).map((entry) => entry.destination),
401
+ shippedMcpServerIds: plan.mcp ? shippedMcpIds(plan.mcp.source) : [],
402
+ docsSeed: seeded
403
+ ? { seeded: true }
404
+ : { seeded: false, reason: docsSpecsExisted ? 'directory-exists' : 'assets-missing' },
405
+ capabilityModes: capabilityModes(contract),
406
+ };
407
+ const manifestAbs = path.join(cwd, MANIFEST_REL);
408
+ if (!fs.existsSync(manifestAbs)) {
409
+ created.add(manifestAbs);
410
+ }
411
+ writeAtomicText(manifestAbs, `${JSON.stringify(manifest, null, 2)}\n`);
412
+
413
+ if (fs.existsSync(rollbackRoot)) {
414
+ fs.rmSync(rollbackRoot, { recursive: true, force: true });
415
+ }
416
+ const rollbackParent = path.join(managedRoot, '.rollback');
417
+ if (fs.existsSync(rollbackParent) && fs.readdirSync(rollbackParent).length === 0) {
418
+ fs.rmdirSync(rollbackParent);
419
+ }
420
+ } catch (error) {
421
+ restoreRollback(overwritten, created, rollbackRoot, managedRoot, rootExisted);
422
+ const message = error instanceof Error ? error.message : String(error);
423
+ console.error(`init failed: ${message}`);
424
+ process.exit(1);
425
+ }
426
+
427
+ console.log(`Done. .opencode installed at ${managedRoot} (preset: ${stack})`);
428
+ }
429
+
430
+ function stripShippedMcp(mcpAbs, shippedIds) {
431
+ if (!fs.existsSync(mcpAbs) || shippedIds.length === 0) {
432
+ return;
433
+ }
434
+ let parsed;
435
+ try {
436
+ parsed = loadJson(mcpAbs);
437
+ } catch {
438
+ console.warn(`Preserved ${mcpAbs} (not valid JSON); shipped MCP keys not stripped.`);
439
+ return;
440
+ }
441
+ if (!isPlainObject(parsed) || !isPlainObject(parsed.mcp)) {
442
+ return;
443
+ }
444
+ const nextServers = { ...parsed.mcp };
445
+ for (const id of shippedIds) {
446
+ delete nextServers[id];
447
+ }
448
+ writeAtomicText(mcpAbs, `${JSON.stringify({ ...parsed, mcp: nextServers }, null, 2)}\n`);
449
+ }
450
+
451
+ function removeEmptyDirs(dir) {
452
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
453
+ return;
454
+ }
455
+ for (const name of fs.readdirSync(dir)) {
456
+ removeEmptyDirs(path.join(dir, name));
457
+ }
458
+ if (fs.readdirSync(dir).length === 0) {
459
+ fs.rmdirSync(dir);
460
+ }
461
+ }
462
+
463
+ function cmdCleanOpencode({ cwdOption, helpers }) {
464
+ const { resolveTargetCwd } = helpers;
465
+ const cwd = resolveTargetCwd(cwdOption);
466
+ const manifestPath = path.join(cwd, MANIFEST_REL);
467
+ if (!fs.existsSync(manifestPath)) {
468
+ console.error(
469
+ 'clean opencode: missing .opencode/.ai-rules-install.json; nothing removed (fail-safe).',
470
+ );
471
+ process.exit(1);
472
+ }
473
+ let manifest;
474
+ try {
475
+ manifest = loadJson(manifestPath);
476
+ } catch {
477
+ console.error(
478
+ 'clean opencode: invalid .opencode/.ai-rules-install.json; nothing removed (fail-safe).',
479
+ );
480
+ process.exit(1);
481
+ }
482
+ if (!manifest || manifest.platform !== 'opencode' || !isPlainObject(manifest.managedFiles)) {
483
+ console.error(
484
+ 'clean opencode: install manifest is not an OpenCode ownership record; nothing removed.',
485
+ );
486
+ process.exit(1);
487
+ }
488
+
489
+ const preserved = [];
490
+ for (const [rel, installedHash] of Object.entries(manifest.managedFiles)) {
491
+ if (rel === MANIFEST_REL) {
492
+ continue;
493
+ }
494
+ const abs = path.join(cwd, rel);
495
+ if (!fs.existsSync(abs)) {
496
+ continue;
497
+ }
498
+ const current = sha256File(abs);
499
+ if (current === installedHash) {
500
+ fs.unlinkSync(abs);
501
+ } else {
502
+ preserved.push(rel);
503
+ console.warn(`Preserved modified managed file ${rel}`);
504
+ }
505
+ }
506
+
507
+ if (preserved.includes('opencode.json') && Array.isArray(manifest.shippedMcpServerIds)) {
508
+ stripShippedMcp(path.join(cwd, 'opencode.json'), manifest.shippedMcpServerIds);
509
+ }
510
+
511
+ for (const dirRel of [...(manifest.managedDirectories || [])].sort((a, b) => b.length - a.length)) {
512
+ removeEmptyDirs(path.join(cwd, dirRel));
513
+ }
514
+
515
+ fs.unlinkSync(manifestPath);
516
+ removeEmptyDirs(path.join(cwd, '.opencode'));
517
+ if (
518
+ fs.existsSync(path.join(cwd, '.opencode')) &&
519
+ fs.readdirSync(path.join(cwd, '.opencode')).length === 0
520
+ ) {
521
+ fs.rmdirSync(path.join(cwd, '.opencode'));
522
+ }
523
+
524
+ console.log(
525
+ preserved.length > 0
526
+ ? `clean opencode: removed unchanged managed files; preserved ${preserved.join(', ')}`
527
+ : 'clean opencode: removed unchanged managed files',
528
+ );
529
+ }
530
+
531
+ module.exports = { cmdInitOpencode, cmdCleanOpencode, MANIFEST_REL };