@bonesofspring/ai-rules 0.2.23 → 0.2.25

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 (287) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +20 -5
  3. package/bin/cli.js +121 -15
  4. package/bin/mavis-install.js +5 -1
  5. package/bin/opencode-install.js +535 -0
  6. package/package.json +10 -3
  7. package/presets/_shared/core/agent-team/agent-team-orchestrator.md +2 -2
  8. package/presets/_shared/core/meta/preset-layering.md +3 -3
  9. package/presets/_shared/core/meta/preset-no-cross-stack-leakage.md +1 -1
  10. package/presets/_shared/core/meta/preset-pr-checklist.md +4 -3
  11. package/presets/_shared/core/meta/preset-token-budget.md +3 -0
  12. package/presets/_shared/core/meta/preset-twin-sync.md +16 -0
  13. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-layering.md +3 -3
  14. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  15. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  16. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-token-budget.md +3 -0
  17. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +16 -0
  18. package/presets/claude/go/rules/tooling-and-review/preset-layering.md +3 -3
  19. package/presets/claude/go/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  20. package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  21. package/presets/claude/go/rules/tooling-and-review/preset-token-budget.md +3 -0
  22. package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +16 -0
  23. package/presets/claude/ios-swift/rules/tooling-and-review/preset-layering.md +3 -3
  24. package/presets/claude/ios-swift/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  25. package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  26. package/presets/claude/ios-swift/rules/tooling-and-review/preset-token-budget.md +3 -0
  27. package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +16 -0
  28. package/presets/claude/java/rules/tooling-and-review/preset-layering.md +3 -3
  29. package/presets/claude/java/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  30. package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  31. package/presets/claude/java/rules/tooling-and-review/preset-token-budget.md +3 -0
  32. package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +16 -0
  33. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-layering.md +3 -3
  34. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  35. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  36. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-token-budget.md +3 -0
  37. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +16 -0
  38. package/presets/claude/next/REPO_AGENTS.md +34 -8
  39. package/presets/claude/next/agents/build-verifier.md +3 -1
  40. package/presets/claude/next/rules/tooling-and-review/ai-local-overlay.md +1 -1
  41. package/presets/claude/next/rules/tooling-and-review/preset-layering.md +3 -3
  42. package/presets/claude/next/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  43. package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  44. package/presets/claude/next/rules/tooling-and-review/preset-token-budget.md +3 -0
  45. package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +16 -0
  46. package/presets/claude/nuxt/rules/tooling-and-review/preset-layering.md +3 -3
  47. package/presets/claude/nuxt/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  48. package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  49. package/presets/claude/nuxt/rules/tooling-and-review/preset-token-budget.md +3 -0
  50. package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +16 -0
  51. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-layering.md +3 -3
  52. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  53. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  54. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-token-budget.md +3 -0
  55. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +16 -0
  56. package/presets/claude/php-laravel/rules/tooling-and-review/preset-layering.md +3 -3
  57. package/presets/claude/php-laravel/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  58. package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  59. package/presets/claude/php-laravel/rules/tooling-and-review/preset-token-budget.md +3 -0
  60. package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +16 -0
  61. package/presets/claude/svelte/rules/tooling-and-review/preset-layering.md +3 -3
  62. package/presets/claude/svelte/rules/tooling-and-review/preset-no-cross-stack-leakage.md +1 -1
  63. package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +4 -3
  64. package/presets/claude/svelte/rules/tooling-and-review/preset-token-budget.md +3 -0
  65. package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +16 -0
  66. package/presets/cursor/android-kotlin/rules/preset-layering.mdc +3 -3
  67. package/presets/cursor/android-kotlin/rules/preset-no-cross-stack-leakage.mdc +1 -1
  68. package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +4 -3
  69. package/presets/cursor/android-kotlin/rules/preset-token-budget.mdc +3 -0
  70. package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +16 -0
  71. package/presets/cursor/go/rules/preset-layering.mdc +3 -3
  72. package/presets/cursor/go/rules/preset-no-cross-stack-leakage.mdc +1 -1
  73. package/presets/cursor/go/rules/preset-pr-checklist.mdc +4 -3
  74. package/presets/cursor/go/rules/preset-token-budget.mdc +3 -0
  75. package/presets/cursor/go/rules/preset-twin-sync.mdc +16 -0
  76. package/presets/cursor/ios-swift/rules/preset-layering.mdc +3 -3
  77. package/presets/cursor/ios-swift/rules/preset-no-cross-stack-leakage.mdc +1 -1
  78. package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +4 -3
  79. package/presets/cursor/ios-swift/rules/preset-token-budget.mdc +3 -0
  80. package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +16 -0
  81. package/presets/cursor/java/rules/preset-layering.mdc +3 -3
  82. package/presets/cursor/java/rules/preset-no-cross-stack-leakage.mdc +1 -1
  83. package/presets/cursor/java/rules/preset-pr-checklist.mdc +4 -3
  84. package/presets/cursor/java/rules/preset-token-budget.mdc +3 -0
  85. package/presets/cursor/java/rules/preset-twin-sync.mdc +16 -0
  86. package/presets/cursor/mcp-ts/rules/preset-layering.mdc +3 -3
  87. package/presets/cursor/mcp-ts/rules/preset-no-cross-stack-leakage.mdc +1 -1
  88. package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +4 -3
  89. package/presets/cursor/mcp-ts/rules/preset-token-budget.mdc +3 -0
  90. package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +16 -0
  91. package/presets/cursor/next/REPO_AGENTS.md +26 -8
  92. package/presets/cursor/next/agents/build-verifier.md +3 -1
  93. package/presets/cursor/next/rules/ai-local-overlay.mdc +1 -1
  94. package/presets/cursor/next/rules/preset-layering.mdc +3 -3
  95. package/presets/cursor/next/rules/preset-no-cross-stack-leakage.mdc +1 -1
  96. package/presets/cursor/next/rules/preset-pr-checklist.mdc +4 -3
  97. package/presets/cursor/next/rules/preset-token-budget.mdc +3 -0
  98. package/presets/cursor/next/rules/preset-twin-sync.mdc +16 -0
  99. package/presets/cursor/nuxt/rules/preset-layering.mdc +3 -3
  100. package/presets/cursor/nuxt/rules/preset-no-cross-stack-leakage.mdc +1 -1
  101. package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +4 -3
  102. package/presets/cursor/nuxt/rules/preset-token-budget.mdc +3 -0
  103. package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +16 -0
  104. package/presets/cursor/php-hexagonal/rules/preset-layering.mdc +3 -3
  105. package/presets/cursor/php-hexagonal/rules/preset-no-cross-stack-leakage.mdc +1 -1
  106. package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +4 -3
  107. package/presets/cursor/php-hexagonal/rules/preset-token-budget.mdc +3 -0
  108. package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +16 -0
  109. package/presets/cursor/php-laravel/rules/preset-layering.mdc +3 -3
  110. package/presets/cursor/php-laravel/rules/preset-no-cross-stack-leakage.mdc +1 -1
  111. package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +4 -3
  112. package/presets/cursor/php-laravel/rules/preset-token-budget.mdc +3 -0
  113. package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +16 -0
  114. package/presets/cursor/svelte/rules/preset-layering.mdc +3 -3
  115. package/presets/cursor/svelte/rules/preset-no-cross-stack-leakage.mdc +1 -1
  116. package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +4 -3
  117. package/presets/cursor/svelte/rules/preset-token-budget.mdc +3 -0
  118. package/presets/cursor/svelte/rules/preset-twin-sync.mdc +16 -0
  119. package/presets/mavis/next/MAVIS.md +1 -0
  120. package/presets/mavis/next/REPO_AGENTS.md +28 -48
  121. package/presets/mavis/next/adapter-map.json +3 -3
  122. package/presets/mavis/next/agents/build-verifier.md +3 -1
  123. package/presets/mavis/next/rules/agent-team-orchestrator.md +2 -2
  124. package/presets/mavis/next/rules/ai-local-overlay.md +1 -1
  125. package/presets/mavis/next/rules/preset-layering.md +3 -3
  126. package/presets/mavis/next/rules/preset-no-cross-stack-leakage.md +1 -1
  127. package/presets/mavis/next/rules/preset-pr-checklist.md +4 -3
  128. package/presets/mavis/next/rules/preset-token-budget.md +3 -0
  129. package/presets/mavis/next/rules/preset-twin-sync.md +16 -0
  130. package/presets/opencode/README.md +61 -0
  131. package/presets/opencode/next/MCP.md +36 -0
  132. package/presets/opencode/next/OPENCODE.md +65 -0
  133. package/presets/opencode/next/README.md +16 -0
  134. package/presets/opencode/next/REPO_AGENTS.md +74 -0
  135. package/presets/opencode/next/adapter-map.json +959 -0
  136. package/presets/opencode/next/agents/README.md +201 -0
  137. package/presets/opencode/next/agents/accessibility-reviewer.md +73 -0
  138. package/presets/opencode/next/agents/api-contract-reviewer.md +69 -0
  139. package/presets/opencode/next/agents/build-verifier.md +124 -0
  140. package/presets/opencode/next/agents/ci-investigator.md +71 -0
  141. package/presets/opencode/next/agents/code-reviewer.md +73 -0
  142. package/presets/opencode/next/agents/codebase-analyzer.md +33 -0
  143. package/presets/opencode/next/agents/debugger.md +88 -0
  144. package/presets/opencode/next/agents/feature-developer.md +109 -0
  145. package/presets/opencode/next/agents/migration-specialist.md +79 -0
  146. package/presets/opencode/next/agents/mockup-reviewer.md +65 -0
  147. package/presets/opencode/next/agents/openspec-author.md +169 -0
  148. package/presets/opencode/next/agents/performance-auditor.md +80 -0
  149. package/presets/opencode/next/agents/playwright-test-generator.md +36 -0
  150. package/presets/opencode/next/agents/playwright-test-healer.md +36 -0
  151. package/presets/opencode/next/agents/playwright-test-planner.md +33 -0
  152. package/presets/opencode/next/agents/qa-tester.md +60 -0
  153. package/presets/opencode/next/agents/security-reviewer.md +66 -0
  154. package/presets/opencode/next/agents/solution-architect.md +80 -0
  155. package/presets/opencode/next/agents/task-analyst.md +178 -0
  156. package/presets/opencode/next/agents/task-router.md +190 -0
  157. package/presets/opencode/next/agents/tech-writer.md +69 -0
  158. package/presets/opencode/next/agents/unit-test-generator.md +39 -0
  159. package/presets/opencode/next/agents/unit-test-healer.md +40 -0
  160. package/presets/opencode/next/agents/unit-test-planner.md +66 -0
  161. package/presets/opencode/next/commands/README.md +49 -0
  162. package/presets/opencode/next/commands/spec-start.md +32 -0
  163. package/presets/opencode/next/commands/task-continue.md +58 -0
  164. package/presets/opencode/next/commands/task.md +58 -0
  165. package/presets/opencode/next/commands/technical-retro.md +88 -0
  166. package/presets/opencode/next/hooks/README.md +22 -0
  167. package/presets/opencode/next/hooks/chain-team-phases.sh +845 -0
  168. package/presets/opencode/next/opencode.json +33 -0
  169. package/presets/opencode/next/rules/README.md +20 -0
  170. package/presets/opencode/next/rules/agent-team-intake.md +11 -0
  171. package/presets/opencode/next/rules/agent-team-orchestrator.md +214 -0
  172. package/presets/opencode/next/rules/ai-local-overlay.md +37 -0
  173. package/presets/opencode/next/rules/anti-sycophancy-discipline.md +23 -0
  174. package/presets/opencode/next/rules/api-services.md +72 -0
  175. package/presets/opencode/next/rules/architecture-boundaries-ui.md +9 -0
  176. package/presets/opencode/next/rules/architecture-boundaries.md +86 -0
  177. package/presets/opencode/next/rules/arrow-functions.md +25 -0
  178. package/presets/opencode/next/rules/code-quality-and-refactoring.md +40 -0
  179. package/presets/opencode/next/rules/code-review-mr.md +40 -0
  180. package/presets/opencode/next/rules/css-property-order-stylelint.md +5 -0
  181. package/presets/opencode/next/rules/design-guidance.md +99 -0
  182. package/presets/opencode/next/rules/feature-delivery-workflow.md +49 -0
  183. package/presets/opencode/next/rules/http-client.md +55 -0
  184. package/presets/opencode/next/rules/layer-barrel-exports.md +68 -0
  185. package/presets/opencode/next/rules/mcp-usage.md +86 -0
  186. package/presets/opencode/next/rules/mockup-review.md +107 -0
  187. package/presets/opencode/next/rules/navigation-router-stack.md +56 -0
  188. package/presets/opencode/next/rules/navigation-router-ui.md +10 -0
  189. package/presets/opencode/next/rules/next-app-core.md +29 -0
  190. package/presets/opencode/next/rules/next-app-router.md +44 -0
  191. package/presets/opencode/next/rules/no-props-spread.md +56 -0
  192. package/presets/opencode/next/rules/no-type-assertion-as-import-export.md +51 -0
  193. package/presets/opencode/next/rules/package-manager.md +11 -0
  194. package/presets/opencode/next/rules/playwright-agents.md +67 -0
  195. package/presets/opencode/next/rules/post-change-lint.md +35 -0
  196. package/presets/opencode/next/rules/preset-layering.md +27 -0
  197. package/presets/opencode/next/rules/preset-no-cross-stack-leakage.md +90 -0
  198. package/presets/opencode/next/rules/preset-pr-checklist.md +36 -0
  199. package/presets/opencode/next/rules/preset-token-budget.md +39 -0
  200. package/presets/opencode/next/rules/preset-twin-sync.md +81 -0
  201. package/presets/opencode/next/rules/product-specs-authoring.md +112 -0
  202. package/presets/opencode/next/rules/product-specs.md +56 -0
  203. package/presets/opencode/next/rules/public-imports.md +50 -0
  204. package/presets/opencode/next/rules/react-a11y-coding.md +31 -0
  205. package/presets/opencode/next/rules/react-next-anti-hallucination.md +60 -0
  206. package/presets/opencode/next/rules/react-ui.md +105 -0
  207. package/presets/opencode/next/rules/reference-features.md +45 -0
  208. package/presets/opencode/next/rules/security-next.md +45 -0
  209. package/presets/opencode/next/rules/store-rtk.md +78 -0
  210. package/presets/opencode/next/rules/technical-retro.md +11 -0
  211. package/presets/opencode/next/rules/tests-e2e-structure.md +72 -0
  212. package/presets/opencode/next/rules/tests-unit.md +72 -0
  213. package/presets/opencode/next/rules/types-jsdoc.md +43 -0
  214. package/presets/opencode/next/rules/ux-design.md +99 -0
  215. package/presets/opencode/next/skills/README.md +21 -0
  216. package/presets/opencode/next/skills/ci-investigation/SKILL.md +44 -0
  217. package/presets/opencode/next/skills/code-review/SKILL.md +44 -0
  218. package/presets/opencode/next/skills/debug-investigation/SKILL.md +59 -0
  219. package/presets/opencode/next/skills/feature-delivery/SKILL.md +39 -0
  220. package/presets/opencode/next/skills/mockup-review/SKILL.md +29 -0
  221. package/presets/opencode/next/skills/playwright-e2e/SKILL.md +33 -0
  222. package/presets/opencode/next/skills/react-performance/SKILL.md +44 -0
  223. package/presets/opencode/next/skills/spec-authoring/SKILL.md +26 -0
  224. package/presets/opencode/next/skills/spec-discovery/SKILL.md +58 -0
  225. package/presets/opencode/next/skills/technical-retro/SKILL.md +54 -0
  226. package/presets/opencode/next/skills/unit-testing/SKILL.md +34 -0
  227. package/presets/opencode/next/skills/ux-design/SKILL.md +36 -0
  228. package/presets/opencode/next/skills/write-adr/SKILL.md +41 -0
  229. package/presets/opencode/next/team/README.md +148 -0
  230. package/presets/opencode/next/team/conventions.md +21 -0
  231. package/presets/opencode/next/team/fixtures/artifact-manifest.example.json +79 -0
  232. package/presets/opencode/next/team/fixtures/bugfix-standard.json +25 -0
  233. package/presets/opencode/next/team/fixtures/feature-full.json +29 -0
  234. package/presets/opencode/next/team/fixtures/feature-light.json +27 -0
  235. package/presets/opencode/next/team/fixtures/feature-with-figma-url.json +17 -0
  236. package/presets/opencode/next/team/fixtures/feature-with-ux-no-figma.json +17 -0
  237. package/presets/opencode/next/team/fixtures/memlog.example.md +9 -0
  238. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/human-gate/metrics.json +69 -0
  239. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/missing-ledger/status.json +5 -0
  240. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/parallel-receipts/metrics.json +79 -0
  241. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/serial-retry/metrics.json +124 -0
  242. package/presets/opencode/next/team/fixtures/metrics-scenarios/tasks/validation-failed/metrics.json +124 -0
  243. package/presets/opencode/next/team/fixtures/metrics.example.json +54 -0
  244. package/presets/opencode/next/team/fixtures/mockup-review-fail-embedded-status.json +8 -0
  245. package/presets/opencode/next/team/fixtures/mockup-review-standalone.json +14 -0
  246. package/presets/opencode/next/team/fixtures/ux-design-standalone.json +15 -0
  247. package/presets/opencode/next/team/tasks/.gitkeep +0 -0
  248. package/presets/opencode/platform-contract.json +131 -0
  249. package/scripts/capture-cli-golden.mjs +5 -1
  250. package/scripts/check-chain-hook-coverage.sh +1 -0
  251. package/scripts/check-opencode-adapter-map.mjs +294 -0
  252. package/scripts/check-opencode-local-overlay.mjs +56 -0
  253. package/scripts/check-opencode-platform-contract.mjs +388 -0
  254. package/scripts/check-opencode-token-budget.mjs +48 -0
  255. package/scripts/check-preset-leakage.sh +30 -1
  256. package/scripts/check-preset-structure.sh +45 -0
  257. package/scripts/check-task-router-intents.sh +4 -4
  258. package/scripts/fixtures/cli-golden/claude/next.json +8 -8
  259. package/scripts/fixtures/cli-golden/cursor/next.json +8 -8
  260. package/scripts/fixtures/cli-golden/mavis/next.json +10 -10
  261. package/scripts/fixtures/cli-golden/opencode/next.json +125 -0
  262. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/cursor/AGENTS.md +1 -0
  263. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/cursor/rules/extra.mdc +1 -0
  264. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/cursor/rules/sample.mdc +2 -0
  265. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/opencode/OPENCODE.md +1 -0
  266. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/opencode/adapter-map.json +26 -0
  267. package/scripts/fixtures/opencode-adapter-map/invalid-unmapped/opencode/rules/sample.md +2 -0
  268. package/scripts/fixtures/opencode-adapter-map/valid/cursor/AGENTS.md +1 -0
  269. package/scripts/fixtures/opencode-adapter-map/valid/cursor/rules/sample.mdc +2 -0
  270. package/scripts/fixtures/opencode-adapter-map/valid/opencode/OPENCODE.md +1 -0
  271. package/scripts/fixtures/opencode-adapter-map/valid/opencode/adapter-map.json +26 -0
  272. package/scripts/fixtures/opencode-adapter-map/valid/opencode/rules/sample.md +2 -0
  273. package/scripts/fixtures/opencode-platform-contract/invalid-home-destination.json +131 -0
  274. package/scripts/fixtures/opencode-platform-contract/invalid-missing-capability.json +126 -0
  275. package/scripts/fixtures/opencode-platform-contract/invalid-released-unknown-stack.json +133 -0
  276. package/scripts/fixtures/opencode-platform-contract/invalid-unknown-native-path.json +131 -0
  277. package/scripts/fixtures/opencode-platform-contract/invalid-version-pin.json +132 -0
  278. package/scripts/fixtures/opencode-platform-contract/valid.json +131 -0
  279. package/scripts/generate-mavis-next.mjs +4 -101
  280. package/scripts/generate-opencode-next.mjs +730 -0
  281. package/scripts/lib/discover-hooks.mjs +23 -1
  282. package/scripts/sync-dogfood-mavis.sh +90 -0
  283. package/scripts/sync-dogfood-opencode.sh +85 -0
  284. package/scripts/test-agent-task-metrics-hooks.mjs +4 -1
  285. package/scripts/test-chain-team-phases-coverage.mjs +6 -1
  286. package/scripts/test-cli-init-clean.mjs +180 -2
  287. package/scripts/test-opencode-context-budget.mjs +53 -0
@@ -10,6 +10,15 @@ export function loadReleasedMavisStacks(presetsRoot) {
10
10
  return Array.isArray(contract.releasedStacks) ? contract.releasedStacks : [];
11
11
  }
12
12
 
13
+ export function loadReleasedOpencodeStacks(presetsRoot) {
14
+ const contractPath = path.join(presetsRoot, 'opencode', 'platform-contract.json');
15
+ if (!fs.existsSync(contractPath)) {
16
+ return [];
17
+ }
18
+ const contract = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
19
+ return Array.isArray(contract.releasedStacks) ? contract.releasedStacks : [];
20
+ }
21
+
13
22
  export function discoverChainHooks(presetsRoot) {
14
23
  const hooks = [];
15
24
  for (const platform of ['cursor', 'claude']) {
@@ -30,6 +39,12 @@ export function discoverChainHooks(presetsRoot) {
30
39
  hooks.push(hook);
31
40
  }
32
41
  }
42
+ for (const stack of loadReleasedOpencodeStacks(presetsRoot)) {
43
+ const hook = path.join(presetsRoot, 'opencode', stack, 'hooks', 'chain-team-phases.sh');
44
+ if (fs.existsSync(hook)) {
45
+ hooks.push(hook);
46
+ }
47
+ }
33
48
  return hooks;
34
49
  }
35
50
 
@@ -42,6 +57,9 @@ export function platformFromHook(hookPath) {
42
57
  if (hookPath.includes(`${path.sep}mavis${path.sep}`)) {
43
58
  return 'mavis';
44
59
  }
60
+ if (hookPath.includes(`${path.sep}opencode${path.sep}`)) {
61
+ return 'opencode';
62
+ }
45
63
  if (hookPath.includes(`${path.sep}cursor${path.sep}`)) {
46
64
  return 'cursor';
47
65
  }
@@ -66,7 +84,11 @@ export function expectedLegacyHookCount(presetsRoot) {
66
84
  }
67
85
 
68
86
  export function expectedHookCount(presetsRoot) {
69
- return expectedLegacyHookCount(presetsRoot) + loadReleasedMavisStacks(presetsRoot).filter((stack) =>
87
+ const mavisCount = loadReleasedMavisStacks(presetsRoot).filter((stack) =>
70
88
  fs.existsSync(path.join(presetsRoot, 'mavis', stack, 'hooks', 'chain-team-phases.sh')),
71
89
  ).length;
90
+ const opencodeCount = loadReleasedOpencodeStacks(presetsRoot).filter((stack) =>
91
+ fs.existsSync(path.join(presetsRoot, 'opencode', stack, 'hooks', 'chain-team-phases.sh')),
92
+ ).length;
93
+ return expectedLegacyHookCount(presetsRoot) + mavisCount + opencodeCount;
72
94
  }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # Sync monorepo dogfood root `.mavis/` from Mavis next preset (packages SoT).
3
+ # Also syncs repo-root AGENTS.md from REPO_AGENTS.md.
4
+ # Usage (from repo root or any cwd):
5
+ # ./packages/ai-rules/scripts/sync-dogfood-mavis.sh
6
+ # AI_RULES_DOGFOOD_PRESET=next ./packages/ai-rules/scripts/sync-dogfood-mavis.sh
7
+ #
8
+ # Note: `.mavis/` is ai-rules-owned, not a MiniMax-native discovery path. Mavis
9
+ # only ships `next` in `releasedStacks` (see presets/mavis/platform-contract.json).
10
+ # Other stacks stay TBD until WP-F human gate.
11
+ set -euo pipefail
12
+
13
+ ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
14
+ PRESET_NAME="${AI_RULES_DOGFOOD_PRESET:-next}"
15
+ SRC="$ROOT/packages/ai-rules/presets/mavis/$PRESET_NAME"
16
+ DST="$ROOT/.mavis"
17
+
18
+ if [[ ! -d "$SRC" ]]; then
19
+ echo "error: preset not found: $SRC" >&2
20
+ exit 1
21
+ fi
22
+
23
+ echo "Dogfood sync: $SRC → $DST (preset=$PRESET_NAME)"
24
+
25
+ mkdir -p "$DST"
26
+
27
+ # Platform files at preset root
28
+ for f in MAVIS.md MCP.md mcp.json README.md; do
29
+ if [[ -f "$SRC/$f" ]]; then
30
+ cp "$SRC/$f" "$DST/$f"
31
+ fi
32
+ done
33
+
34
+ # `.mavis/AGENTS.md` is the Mavis entry point (mirrors `.cursor/AGENTS.md`).
35
+ if [[ -f "$SRC/MAVIS.md" && ! -f "$DST/AGENTS.md" ]]; then
36
+ cp "$SRC/MAVIS.md" "$DST/AGENTS.md"
37
+ echo "Seeded .mavis/AGENTS.md from MAVIS.md"
38
+ fi
39
+
40
+ # Repo-root AGENTS.md (cross-tool) from REPO_AGENTS.md
41
+ if [[ -f "$SRC/REPO_AGENTS.md" ]]; then
42
+ cp "$SRC/REPO_AGENTS.md" "$ROOT/AGENTS.md"
43
+ echo "Copied REPO_AGENTS.md → $ROOT/AGENTS.md"
44
+ fi
45
+
46
+ # Directory trees (preserve local team/tasks)
47
+ for dir in agents commands rules skills; do
48
+ if [[ -d "$SRC/$dir" ]]; then
49
+ mkdir -p "$DST/$dir"
50
+ rsync -a --delete "$SRC/$dir/" "$DST/$dir/"
51
+ fi
52
+ done
53
+
54
+ # Hooks: only chain-team-phases.sh is shipped; examples / hooks.json are
55
+ # intentionally absent (see presets/mavis/next/adapter-map.json).
56
+ if [[ -d "$SRC/hooks" ]]; then
57
+ mkdir -p "$DST/hooks"
58
+ rsync -a --delete "$SRC/hooks/" "$DST/hooks/"
59
+ if [[ -f "$DST/hooks/chain-team-phases.sh" ]]; then
60
+ chmod +x "$DST/hooks/chain-team-phases.sh"
61
+ fi
62
+ fi
63
+
64
+ if [[ -d "$SRC/team" ]]; then
65
+ mkdir -p "$DST/team"
66
+ # Sync fixtures + README; never delete local tasks/
67
+ if [[ -d "$SRC/team/fixtures" ]]; then
68
+ mkdir -p "$DST/team/fixtures"
69
+ rsync -a --delete "$SRC/team/fixtures/" "$DST/team/fixtures/"
70
+ fi
71
+ if [[ -f "$SRC/team/README.md" ]]; then
72
+ cp "$SRC/team/README.md" "$DST/team/README.md"
73
+ fi
74
+ if [[ -f "$SRC/team/.gitkeep" ]]; then
75
+ mkdir -p "$DST/team/tasks"
76
+ cp "$SRC/team/.gitkeep" "$DST/team/tasks/.gitkeep" 2>/dev/null || true
77
+ fi
78
+ # Living conventions: seed if absent; preserve consumer edits
79
+ if [[ -f "$SRC/team/conventions.md" ]]; then
80
+ if [[ ! -f "$DST/team/conventions.md" ]]; then
81
+ cp "$SRC/team/conventions.md" "$DST/team/conventions.md"
82
+ echo "Seeded team/conventions.md"
83
+ else
84
+ echo "Tip: skipped team/conventions.md (already present — living dogfood notes preserved)"
85
+ fi
86
+ fi
87
+ fi
88
+
89
+ echo "Done. Review: git status -- .mavis/ AGENTS.md"
90
+ echo "Tip: run packages/ai-rules/scripts/check-shared-core-drift.sh after core edits."
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # Sync monorepo dogfood root `.opencode/` from OpenCode next preset (packages SoT).
3
+ # Also syncs repo-root AGENTS.md from REPO_AGENTS.md.
4
+ # Usage (from repo root or any cwd):
5
+ # ./packages/ai-rules/scripts/sync-dogfood-opencode.sh
6
+ # AI_RULES_DOGFOOD_PRESET=next ./packages/ai-rules/scripts/sync-dogfood-opencode.sh
7
+ #
8
+ # Note: OpenCode only ships `next` in `releasedStacks`.
9
+ set -euo pipefail
10
+
11
+ ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
12
+ PRESET_NAME="${AI_RULES_DOGFOOD_PRESET:-next}"
13
+ SRC="$ROOT/packages/ai-rules/presets/opencode/$PRESET_NAME"
14
+ DST="$ROOT/.opencode"
15
+
16
+ if [[ ! -d "$SRC" ]]; then
17
+ echo "error: preset not found: $SRC" >&2
18
+ exit 1
19
+ fi
20
+
21
+ echo "Dogfood sync: $SRC → $DST (preset=$PRESET_NAME)"
22
+
23
+ mkdir -p "$DST"
24
+
25
+ for f in OPENCODE.md MCP.md opencode.json README.md; do
26
+ if [[ -f "$SRC/$f" ]]; then
27
+ cp "$SRC/$f" "$DST/$f"
28
+ fi
29
+ done
30
+
31
+ if [[ -f "$SRC/OPENCODE.md" && ! -f "$DST/AGENTS.md" ]]; then
32
+ cp "$SRC/OPENCODE.md" "$DST/AGENTS.md"
33
+ echo "Seeded .opencode/AGENTS.md from OPENCODE.md"
34
+ fi
35
+
36
+ if [[ -f "$SRC/REPO_AGENTS.md" ]]; then
37
+ cp "$SRC/REPO_AGENTS.md" "$ROOT/AGENTS.md"
38
+ echo "Copied REPO_AGENTS.md → $ROOT/AGENTS.md"
39
+ fi
40
+
41
+ if [[ -f "$SRC/opencode.json" ]]; then
42
+ cp "$SRC/opencode.json" "$ROOT/opencode.json"
43
+ echo "Copied opencode.json → $ROOT/opencode.json"
44
+ fi
45
+
46
+ for dir in agents commands rules skills; do
47
+ if [[ -d "$SRC/$dir" ]]; then
48
+ mkdir -p "$DST/$dir"
49
+ rsync -a --delete "$SRC/$dir/" "$DST/$dir/"
50
+ fi
51
+ done
52
+
53
+ if [[ -d "$SRC/hooks" ]]; then
54
+ mkdir -p "$DST/hooks"
55
+ rsync -a --delete "$SRC/hooks/" "$DST/hooks/"
56
+ if [[ -f "$DST/hooks/chain-team-phases.sh" ]]; then
57
+ chmod +x "$DST/hooks/chain-team-phases.sh"
58
+ fi
59
+ fi
60
+
61
+ if [[ -d "$SRC/team" ]]; then
62
+ mkdir -p "$DST/team"
63
+ if [[ -d "$SRC/team/fixtures" ]]; then
64
+ mkdir -p "$DST/team/fixtures"
65
+ rsync -a --delete "$SRC/team/fixtures/" "$DST/team/fixtures/"
66
+ fi
67
+ if [[ -f "$SRC/team/README.md" ]]; then
68
+ cp "$SRC/team/README.md" "$DST/team/README.md"
69
+ fi
70
+ if [[ -f "$SRC/team/.gitkeep" ]]; then
71
+ mkdir -p "$DST/team/tasks"
72
+ cp "$SRC/team/.gitkeep" "$DST/team/tasks/.gitkeep" 2>/dev/null || true
73
+ fi
74
+ if [[ -f "$SRC/team/conventions.md" ]]; then
75
+ if [[ ! -f "$DST/team/conventions.md" ]]; then
76
+ cp "$SRC/team/conventions.md" "$DST/team/conventions.md"
77
+ echo "Seeded team/conventions.md"
78
+ else
79
+ echo "Tip: skipped team/conventions.md (already present — living dogfood notes preserved)"
80
+ fi
81
+ fi
82
+ fi
83
+
84
+ echo "Done. Review: git status -- .opencode/ AGENTS.md opencode.json"
85
+ echo "Tip: run packages/ai-rules/scripts/check-shared-core-drift.sh after core edits."
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
10
10
  const packageDirectory = path.resolve(scriptDirectory, '..');
11
11
  const presetsDirectory = path.join(packageDirectory, 'presets');
12
- import { discoverChainHooks, expectedHookCount, loadReleasedMavisStacks, platformFromHook } from './lib/discover-hooks.mjs';
12
+ import { discoverChainHooks, expectedHookCount, loadReleasedMavisStacks, loadReleasedOpencodeStacks, platformFromHook } from './lib/discover-hooks.mjs';
13
13
 
14
14
  const hooks = discoverChainHooks(presetsDirectory);
15
15
 
@@ -424,6 +424,9 @@ for (const command of [
424
424
  ...loadReleasedMavisStacks(presetsDirectory).map((stack) =>
425
425
  path.join(presetsDirectory, 'mavis', stack, 'commands', 'task-continue.md'),
426
426
  ),
427
+ ...loadReleasedOpencodeStacks(presetsDirectory).map((stack) =>
428
+ path.join(presetsDirectory, 'opencode', stack, 'commands', 'task-continue.md'),
429
+ ),
427
430
  ]) {
428
431
  const source = fs.readFileSync(command, 'utf8');
429
432
  assert.match(source, /action":"end_human_gate"/, `${command}: must invoke installed hook end_human_gate action`);
@@ -44,7 +44,12 @@ assert.equal(
44
44
 
45
45
  // mcp-ts does not ship openspec-author (per preset-no-cross-stack-leakage).
46
46
  const HOOKS_WITH_OPENSPEC = hooks.filter((hook) => !hook.includes(`${path.sep}mcp-ts${path.sep}`));
47
- assert.equal(HOOKS_WITH_OPENSPEC.length, 19, 'expected 19 hooks to cover openspec-author');
47
+ const mcpTsHookCount = hooks.filter((hook) => hook.includes(`${path.sep}mcp-ts${path.sep}`)).length;
48
+ assert.equal(
49
+ HOOKS_WITH_OPENSPEC.length,
50
+ hooks.length - mcpTsHookCount,
51
+ `expected ${hooks.length - mcpTsHookCount} hooks to cover openspec-author`,
52
+ );
48
53
 
49
54
  function readJson(filePath) {
50
55
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -8,7 +8,7 @@ import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
- import { runInit, snapshotInstall } from './lib/cli-golden.mjs';
11
+ import { runInit, sha256File, snapshotInstall } from './lib/cli-golden.mjs';
12
12
 
13
13
  const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
14
14
  const packageDirectory = path.resolve(scriptDirectory, '..');
@@ -116,7 +116,7 @@ assert.equal(manifest.docsSeed.seeded, true);
116
116
  const existingAgents = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-agents-'));
117
117
  fs.writeFileSync(path.join(existingAgents, 'AGENTS.md'), '# consumer agents\n');
118
118
  runInit(cli, existingAgents, 'mavis', 'next');
119
- assert.match(fs.readFileSync(path.join(existingAgents, 'AGENTS.md'), 'utf8'), /Mavis compatibility entry/);
119
+ assert.match(fs.readFileSync(path.join(existingAgents, 'AGENTS.md'), 'utf8'), /Presence-based load/);
120
120
 
121
121
  const specsExist = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-specs-'));
122
122
  fs.mkdirSync(path.join(specsExist, 'docs', 'specs'), { recursive: true });
@@ -236,6 +236,173 @@ if (!fs.existsSync(mavisGoldenPath)) {
236
236
  assert.deepEqual(mavisFiles, expectedMavis.files, 'mavis/next golden drifted');
237
237
  }
238
238
 
239
+ const ocFresh = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-opencode-fresh-'));
240
+ const ocInitOut = execFileSync(
241
+ process.execPath,
242
+ [cli, 'init', 'opencode', '--preset', 'next', '--cwd', ocFresh],
243
+ { encoding: 'utf8' },
244
+ );
245
+ assert.match(ocInitOut, /\.opencode installed/);
246
+ assert.equal(fs.existsSync(path.join(ocFresh, 'AGENTS.md')), true);
247
+ assert.equal(fs.existsSync(path.join(ocFresh, '.opencode', 'AGENTS.md')), true);
248
+ assert.equal(fs.existsSync(path.join(ocFresh, '.opencode', 'rules', 'next-app-core.md')), true);
249
+ assert.equal(fs.existsSync(path.join(ocFresh, '.opencode', 'hooks', 'chain-team-phases.sh')), true);
250
+ assert.equal(fs.existsSync(path.join(ocFresh, 'opencode.json')), true);
251
+ assert.equal(fs.existsSync(path.join(ocFresh, '.opencode', 'adapter-map.json')), false);
252
+ assert.equal(fs.existsSync(path.join(ocFresh, 'docs', 'specs', 'INDEX.md')), true);
253
+ const ocManifest = readJson(path.join(ocFresh, '.opencode', '.ai-rules-install.json'));
254
+ assert.equal(ocManifest.schemaVersion, 1);
255
+ assert.equal(ocManifest.platform, 'opencode');
256
+ assert.equal(ocManifest.preset, 'next');
257
+ assert.ok(ocManifest.managedFiles['AGENTS.md']);
258
+ assert.ok(ocManifest.shippedMcpServerIds.includes('context7'));
259
+ assert.equal(ocManifest.docsSeed.seeded, true);
260
+
261
+ const ocAgents = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-opencode-agents-'));
262
+ fs.writeFileSync(path.join(ocAgents, 'AGENTS.md'), '# consumer agents\n');
263
+ runInit(cli, ocAgents, 'opencode', 'next');
264
+ assert.match(fs.readFileSync(path.join(ocAgents, 'AGENTS.md'), 'utf8'), /Presence-based load/);
265
+
266
+ const ocSpecs = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-opencode-specs-'));
267
+ fs.mkdirSync(path.join(ocSpecs, 'docs', 'specs'), { recursive: true });
268
+ runInit(cli, ocSpecs, 'opencode', 'next');
269
+ assert.equal(fs.existsSync(path.join(ocSpecs, 'docs', 'specs', 'INDEX.md')), false);
270
+ assert.equal(
271
+ readJson(path.join(ocSpecs, '.opencode', '.ai-rules-install.json')).docsSeed.reason,
272
+ 'directory-exists',
273
+ );
274
+
275
+ writeJson(path.join(ocFresh, 'opencode.json'), {
276
+ $schema: 'https://opencode.ai/config.json',
277
+ mcp: {
278
+ context7: {
279
+ type: 'local',
280
+ command: ['npx', '-y', '@upstash/context7-mcp@old'],
281
+ environment: { CONTEXT7_API_KEY: 'keep-me' },
282
+ },
283
+ storybook: { type: 'remote', url: 'http://localhost:6006/mcp' },
284
+ },
285
+ localOnly: true,
286
+ });
287
+ const ocRepeat = runInit(cli, ocFresh, 'opencode', 'next');
288
+ assert.match(ocRepeat, /kept extra servers: storybook/);
289
+ const ocMerged = readJson(path.join(ocFresh, 'opencode.json'));
290
+ assert.equal(ocMerged.localOnly, true);
291
+ assert.ok(ocMerged.mcp.storybook);
292
+ assert.equal(ocMerged.mcp.context7.environment.CONTEXT7_API_KEY, 'keep-me');
293
+
294
+ ocMerged.localNote = true;
295
+ writeJson(path.join(ocFresh, 'opencode.json'), ocMerged);
296
+ const ocModifiedRel = path.join(ocFresh, '.opencode', 'rules', 'next-app-core.md');
297
+ fs.appendFileSync(ocModifiedRel, '\n# local overlay note\n');
298
+ runClean(ocFresh, 'opencode');
299
+ assert.equal(fs.existsSync(ocModifiedRel), true);
300
+ assert.equal(fs.existsSync(path.join(ocFresh, '.opencode', '.ai-rules-install.json')), false);
301
+ assert.equal(fs.existsSync(path.join(ocFresh, 'AGENTS.md')), false);
302
+ assert.ok(readJson(path.join(ocFresh, 'opencode.json')).mcp.storybook);
303
+ assert.equal(readJson(path.join(ocFresh, 'opencode.json')).mcp.context7, undefined);
304
+ assert.equal(readJson(path.join(ocFresh, 'opencode.json')).localNote, true);
305
+
306
+ const ocMissing = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-opencode-missing-'));
307
+ fs.writeFileSync(path.join(ocMissing, 'keep.txt'), 'safe\n');
308
+ const ocMissingClean = runCli(ocMissing, ['clean', 'opencode']);
309
+ assert.equal(ocMissingClean.status, 1);
310
+ assert.match(`${ocMissingClean.stderr}${ocMissingClean.stdout}`, /nothing removed/);
311
+ assert.equal(fs.readFileSync(path.join(ocMissing, 'keep.txt'), 'utf8'), 'safe\n');
312
+
313
+ const ocRollback = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-opencode-rollback-'));
314
+ fs.writeFileSync(path.join(ocRollback, 'AGENTS.md'), '# prior\n');
315
+ const ocFail = runCli(ocRollback, ['init', 'opencode', '--preset', 'next'], {
316
+ AI_RULES_OPENCODE_FAIL_PHASE: 'write',
317
+ });
318
+ assert.equal(ocFail.status, 1);
319
+ assert.equal(fs.readFileSync(path.join(ocRollback, 'AGENTS.md'), 'utf8'), '# prior\n');
320
+ assert.equal(fs.existsSync(path.join(ocRollback, '.opencode')), false);
321
+
322
+ const ocUnreleased = runCli(ocFresh, ['init', 'opencode', '--preset', 'nuxt']);
323
+ assert.equal(ocUnreleased.status, 1);
324
+ assert.match(`${ocUnreleased.stderr}${ocUnreleased.stdout}`, /not released yet/i);
325
+
326
+ const ocGoldenDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-opencode-golden-'));
327
+ runInit(cli, ocGoldenDir, 'opencode', 'next');
328
+ const ocFiles = snapshotInstall(ocGoldenDir);
329
+ delete ocFiles['.opencode/.ai-rules-install.json'];
330
+ const ocGoldenPath = path.join(goldenRoot, 'opencode', 'next.json');
331
+ if (!fs.existsSync(ocGoldenPath)) {
332
+ writeJson(ocGoldenPath, {
333
+ schemaVersion: 1,
334
+ tool: 'opencode',
335
+ preset: 'next',
336
+ files: ocFiles,
337
+ });
338
+ } else {
339
+ const expectedOc = readJson(ocGoldenPath);
340
+ delete expectedOc.files['.opencode/.ai-rules-install.json'];
341
+ assert.deepEqual(ocFiles, expectedOc.files, 'opencode/next golden drifted');
342
+ }
343
+
344
+ const canonicalRepoAgents = fs.readFileSync(
345
+ path.join(packageDirectory, 'presets', 'cursor', 'next', 'REPO_AGENTS.md'),
346
+ 'utf8',
347
+ );
348
+ for (const platform of ['claude', 'mavis', 'opencode']) {
349
+ const other = fs.readFileSync(
350
+ path.join(packageDirectory, 'presets', platform, 'next', 'REPO_AGENTS.md'),
351
+ 'utf8',
352
+ );
353
+ assert.equal(other, canonicalRepoAgents, `${platform}/next/REPO_AGENTS.md must match cursor SoT`);
354
+ }
355
+
356
+ const lastInitHashes = [];
357
+ const lastInitDirs = [];
358
+ for (const tool of ['cursor', 'claude', 'mavis', 'opencode']) {
359
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `ai-rules-last-init-${tool}-`));
360
+ lastInitDirs.push(dir);
361
+ runInit(cli, dir, tool, 'next');
362
+ lastInitHashes.push(sha256File(path.join(dir, 'AGENTS.md')));
363
+ assert.match(fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8'), /Presence-based load/);
364
+ assert.match(fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8'), /Pipelines are per-tool/);
365
+ }
366
+ assert.equal(new Set(lastInitHashes).size, 1, 'root AGENTS.md must be identical after any last init');
367
+
368
+ const mixed = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-quad-clean-'));
369
+ runInit(cli, mixed, 'cursor', 'next');
370
+ runInit(cli, mixed, 'mavis', 'next');
371
+ assert.match(fs.readFileSync(path.join(mixed, 'AGENTS.md'), 'utf8'), /Presence-based load/);
372
+ const keepCursor = runClean(mixed, 'cursor');
373
+ assert.match(keepCursor, /Kept shared AGENTS.md/);
374
+ assert.equal(fs.existsSync(path.join(mixed, 'AGENTS.md')), true);
375
+ runClean(mixed, 'mavis');
376
+ assert.equal(fs.existsSync(path.join(mixed, 'AGENTS.md')), false);
377
+
378
+ const cursorOnly = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-cursor-only-clean-'));
379
+ runInit(cli, cursorOnly, 'cursor', 'next');
380
+ runClean(cursorOnly, 'cursor');
381
+ assert.equal(fs.existsSync(path.join(cursorOnly, 'AGENTS.md')), false);
382
+
383
+ const multiDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-init-multi-'));
384
+ const multiOut = runInit(cli, multiDir, 'multi', 'next');
385
+ assert.match(multiOut, /Done\. multi installed cursor, claude, mavis, opencode/);
386
+ assert.equal(fs.existsSync(path.join(multiDir, '.cursor', 'AGENTS.md')), true);
387
+ assert.equal(fs.existsSync(path.join(multiDir, '.claude', 'CLAUDE.md')), true);
388
+ assert.equal(fs.existsSync(path.join(multiDir, '.mavis', 'AGENTS.md')), true);
389
+ assert.equal(fs.existsSync(path.join(multiDir, '.opencode', 'AGENTS.md')), true);
390
+ assert.match(fs.readFileSync(path.join(multiDir, 'AGENTS.md'), 'utf8'), /init multi --preset next/);
391
+
392
+ const multiNuxt = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-init-multi-nuxt-'));
393
+ const multiNuxtOut = runInit(cli, multiNuxt, 'multi', 'nuxt');
394
+ assert.match(multiNuxtOut, /Skipping mavis/);
395
+ assert.match(multiNuxtOut, /Skipping opencode/);
396
+ assert.match(multiNuxtOut, /Done\. multi installed cursor, claude; skipped mavis, opencode/);
397
+ assert.equal(fs.existsSync(path.join(multiNuxt, '.cursor', 'AGENTS.md')), true);
398
+ assert.equal(fs.existsSync(path.join(multiNuxt, '.claude', 'CLAUDE.md')), true);
399
+ assert.equal(fs.existsSync(path.join(multiNuxt, '.mavis')), false);
400
+ assert.equal(fs.existsSync(path.join(multiNuxt, '.opencode')), false);
401
+
402
+ const multiClean = runCli(multiDir, ['clean', 'multi']);
403
+ assert.equal(multiClean.status, 1);
404
+ assert.match(`${multiClean.stderr}${multiClean.stdout}`, /clean multi is not supported/);
405
+
239
406
  for (const dir of [
240
407
  isolatedHome,
241
408
  fresh,
@@ -249,6 +416,17 @@ for (const dir of [
249
416
  failManifest,
250
417
  toolFirst,
251
418
  mavisGoldenDir,
419
+ ocFresh,
420
+ ocAgents,
421
+ ocSpecs,
422
+ ocMissing,
423
+ ocRollback,
424
+ ocGoldenDir,
425
+ mixed,
426
+ cursorOnly,
427
+ multiDir,
428
+ multiNuxt,
429
+ ...lastInitDirs,
252
430
  ]) {
253
431
  fs.rmSync(dir, { recursive: true, force: true });
254
432
  }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * P1 context budget: root AGENTS + .opencode/AGENTS combined nonempty lines ≤ 170.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { loadReleasedOpencodeStacks } from './lib/discover-hooks.mjs';
9
+
10
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
11
+ const presetsRoot = path.join(scriptDirectory, '..', 'presets');
12
+ const LINE_CAP = 170;
13
+ const SENTINELS = [
14
+ 'next-app-core',
15
+ 'package-manager',
16
+ 'code-quality-and-refactoring',
17
+ 'ai-local-overlay',
18
+ '.opencode/AGENTS.md',
19
+ 'status.json',
20
+ ];
21
+
22
+ function nonemptyLines(text) {
23
+ return text.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
24
+ }
25
+
26
+ const stacks = loadReleasedOpencodeStacks(presetsRoot);
27
+ if (stacks.length === 0) {
28
+ console.log('ok opencode context-budget: no released stacks');
29
+ process.exit(0);
30
+ }
31
+
32
+ let failed = 0;
33
+ for (const stack of stacks) {
34
+ const root = path.join(presetsRoot, 'opencode', stack);
35
+ const repoAgents = fs.readFileSync(path.join(root, 'REPO_AGENTS.md'), 'utf8');
36
+ const opencodeAgents = fs.readFileSync(path.join(root, 'OPENCODE.md'), 'utf8');
37
+ const combined = `${repoAgents}\n${opencodeAgents}`;
38
+ const count = nonemptyLines(repoAgents) + nonemptyLines(opencodeAgents);
39
+ if (count > LINE_CAP) {
40
+ console.error(`FAIL opencode/${stack}: entry nonempty lines ${count} > ${LINE_CAP}`);
41
+ failed += 1;
42
+ } else {
43
+ console.log(`ok opencode/${stack}: entry nonempty lines ${count} (cap ${LINE_CAP})`);
44
+ }
45
+ for (const sentinel of SENTINELS) {
46
+ if (!combined.includes(sentinel)) {
47
+ console.error(`FAIL opencode/${stack}: missing critical sentinel ${sentinel}`);
48
+ failed += 1;
49
+ }
50
+ }
51
+ }
52
+
53
+ process.exit(failed === 0 ? 0 : 1);