@bonesofspring/ai-rules 0.2.21 → 0.2.23

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 (363) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +6 -2
  3. package/bin/cli.js +32 -8
  4. package/bin/mavis-install.js +441 -0
  5. package/package.json +9 -2
  6. package/presets/_shared/core/agent-team/agent-artifact-contracts.md +2 -0
  7. package/presets/_shared/core/meta/preset-pr-checklist.md +1 -0
  8. package/presets/_shared/core/meta/preset-twin-sync.md +17 -1
  9. package/presets/claude/android-kotlin/agents/build-verifier.md +2 -0
  10. package/presets/claude/android-kotlin/agents/code-reviewer.md +2 -0
  11. package/presets/claude/android-kotlin/agents/debugger.md +1 -0
  12. package/presets/claude/android-kotlin/agents/feature-developer.md +3 -0
  13. package/presets/claude/android-kotlin/agents/qa-tester.md +2 -0
  14. package/presets/claude/android-kotlin/agents/task-router.md +4 -0
  15. package/presets/claude/android-kotlin/hooks/chain-team-phases.sh +51 -11
  16. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  17. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +17 -1
  18. package/presets/claude/go/agents/build-verifier.md +4 -0
  19. package/presets/claude/go/agents/code-reviewer.md +4 -0
  20. package/presets/claude/go/agents/debugger.md +4 -0
  21. package/presets/claude/go/agents/feature-developer.md +5 -0
  22. package/presets/claude/go/agents/qa-tester.md +4 -0
  23. package/presets/claude/go/agents/task-router.md +4 -0
  24. package/presets/claude/go/hooks/chain-team-phases.sh +51 -11
  25. package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  26. package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +17 -1
  27. package/presets/claude/ios-swift/agents/build-verifier.md +2 -0
  28. package/presets/claude/ios-swift/agents/code-reviewer.md +2 -0
  29. package/presets/claude/ios-swift/agents/debugger.md +1 -0
  30. package/presets/claude/ios-swift/agents/feature-developer.md +3 -0
  31. package/presets/claude/ios-swift/agents/qa-tester.md +2 -0
  32. package/presets/claude/ios-swift/agents/task-router.md +4 -0
  33. package/presets/claude/ios-swift/hooks/chain-team-phases.sh +51 -11
  34. package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  35. package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +17 -1
  36. package/presets/claude/java/agents/build-verifier.md +4 -0
  37. package/presets/claude/java/agents/code-reviewer.md +4 -0
  38. package/presets/claude/java/agents/debugger.md +4 -0
  39. package/presets/claude/java/agents/feature-developer.md +5 -0
  40. package/presets/claude/java/agents/qa-tester.md +4 -0
  41. package/presets/claude/java/agents/task-router.md +4 -0
  42. package/presets/claude/java/hooks/chain-team-phases.sh +51 -11
  43. package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  44. package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +17 -1
  45. package/presets/claude/mcp-ts/agents/build-verifier.md +4 -0
  46. package/presets/claude/mcp-ts/agents/feature-developer.md +5 -0
  47. package/presets/claude/mcp-ts/agents/task-router.md +1 -1
  48. package/presets/claude/mcp-ts/hooks/chain-team-phases.sh +51 -11
  49. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  50. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +17 -1
  51. package/presets/claude/next/agents/build-verifier.md +2 -0
  52. package/presets/claude/next/agents/code-reviewer.md +2 -0
  53. package/presets/claude/next/agents/debugger.md +4 -0
  54. package/presets/claude/next/agents/feature-developer.md +3 -0
  55. package/presets/claude/next/agents/qa-tester.md +4 -0
  56. package/presets/claude/next/agents/unit-test-generator.md +1 -1
  57. package/presets/claude/next/agents/unit-test-planner.md +1 -1
  58. package/presets/claude/next/hooks/chain-team-phases.sh +51 -11
  59. package/presets/claude/next/rules/api-and-data/http-client.md +1 -1
  60. package/presets/claude/next/rules/architecture/reference-features.md +1 -1
  61. package/presets/claude/next/rules/testing/README.md +1 -1
  62. package/presets/claude/next/rules/testing/tests-e2e-structure.md +2 -0
  63. package/presets/claude/next/rules/testing/tests-unit.md +3 -5
  64. package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  65. package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +17 -1
  66. package/presets/claude/next/rules/ui-and-accessibility/react-ui.md +1 -1
  67. package/presets/claude/next/skills/playwright-e2e/SKILL.md +5 -3
  68. package/presets/claude/next/skills/unit-testing/SKILL.md +6 -4
  69. package/presets/claude/nuxt/agents/build-verifier.md +2 -0
  70. package/presets/claude/nuxt/agents/code-reviewer.md +2 -0
  71. package/presets/claude/nuxt/agents/debugger.md +4 -0
  72. package/presets/claude/nuxt/agents/feature-developer.md +3 -0
  73. package/presets/claude/nuxt/agents/qa-tester.md +4 -0
  74. package/presets/claude/nuxt/hooks/chain-team-phases.sh +51 -11
  75. package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  76. package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +17 -1
  77. package/presets/claude/php-hexagonal/agents/build-verifier.md +4 -0
  78. package/presets/claude/php-hexagonal/agents/code-reviewer.md +4 -0
  79. package/presets/claude/php-hexagonal/agents/debugger.md +4 -0
  80. package/presets/claude/php-hexagonal/agents/feature-developer.md +5 -0
  81. package/presets/claude/php-hexagonal/agents/qa-tester.md +4 -0
  82. package/presets/claude/php-hexagonal/agents/task-router.md +4 -0
  83. package/presets/claude/php-hexagonal/hooks/chain-team-phases.sh +51 -11
  84. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  85. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +17 -1
  86. package/presets/claude/php-laravel/agents/build-verifier.md +4 -0
  87. package/presets/claude/php-laravel/agents/code-reviewer.md +4 -0
  88. package/presets/claude/php-laravel/agents/debugger.md +4 -0
  89. package/presets/claude/php-laravel/agents/feature-developer.md +5 -0
  90. package/presets/claude/php-laravel/agents/qa-tester.md +4 -0
  91. package/presets/claude/php-laravel/agents/task-router.md +1 -0
  92. package/presets/claude/php-laravel/hooks/chain-team-phases.sh +51 -11
  93. package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  94. package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +17 -1
  95. package/presets/claude/svelte/agents/build-verifier.md +2 -0
  96. package/presets/claude/svelte/agents/code-reviewer.md +2 -0
  97. package/presets/claude/svelte/agents/debugger.md +4 -0
  98. package/presets/claude/svelte/agents/feature-developer.md +3 -0
  99. package/presets/claude/svelte/agents/qa-tester.md +4 -0
  100. package/presets/claude/svelte/hooks/chain-team-phases.sh +51 -11
  101. package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  102. package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +17 -1
  103. package/presets/cursor/android-kotlin/agents/build-verifier.md +2 -0
  104. package/presets/cursor/android-kotlin/agents/code-reviewer.md +2 -0
  105. package/presets/cursor/android-kotlin/agents/debugger.md +1 -0
  106. package/presets/cursor/android-kotlin/agents/feature-developer.md +3 -0
  107. package/presets/cursor/android-kotlin/agents/qa-tester.md +2 -0
  108. package/presets/cursor/android-kotlin/agents/task-router.md +4 -0
  109. package/presets/cursor/android-kotlin/hooks/chain-team-phases.sh +51 -11
  110. package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +1 -0
  111. package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +17 -1
  112. package/presets/cursor/go/agents/build-verifier.md +4 -0
  113. package/presets/cursor/go/agents/code-reviewer.md +4 -0
  114. package/presets/cursor/go/agents/debugger.md +4 -0
  115. package/presets/cursor/go/agents/feature-developer.md +5 -0
  116. package/presets/cursor/go/agents/qa-tester.md +4 -0
  117. package/presets/cursor/go/agents/task-router.md +4 -0
  118. package/presets/cursor/go/hooks/chain-team-phases.sh +51 -11
  119. package/presets/cursor/go/rules/preset-pr-checklist.mdc +1 -0
  120. package/presets/cursor/go/rules/preset-twin-sync.mdc +17 -1
  121. package/presets/cursor/ios-swift/agents/build-verifier.md +2 -0
  122. package/presets/cursor/ios-swift/agents/code-reviewer.md +2 -0
  123. package/presets/cursor/ios-swift/agents/debugger.md +1 -0
  124. package/presets/cursor/ios-swift/agents/feature-developer.md +3 -0
  125. package/presets/cursor/ios-swift/agents/qa-tester.md +2 -0
  126. package/presets/cursor/ios-swift/agents/task-router.md +4 -0
  127. package/presets/cursor/ios-swift/hooks/chain-team-phases.sh +51 -11
  128. package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +1 -0
  129. package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +17 -1
  130. package/presets/cursor/java/agents/build-verifier.md +4 -0
  131. package/presets/cursor/java/agents/code-reviewer.md +4 -0
  132. package/presets/cursor/java/agents/debugger.md +4 -0
  133. package/presets/cursor/java/agents/feature-developer.md +5 -0
  134. package/presets/cursor/java/agents/qa-tester.md +4 -0
  135. package/presets/cursor/java/agents/task-router.md +4 -0
  136. package/presets/cursor/java/hooks/chain-team-phases.sh +51 -11
  137. package/presets/cursor/java/rules/preset-pr-checklist.mdc +1 -0
  138. package/presets/cursor/java/rules/preset-twin-sync.mdc +17 -1
  139. package/presets/cursor/mcp-ts/agents/build-verifier.md +4 -0
  140. package/presets/cursor/mcp-ts/agents/feature-developer.md +5 -0
  141. package/presets/cursor/mcp-ts/agents/task-router.md +1 -1
  142. package/presets/cursor/mcp-ts/hooks/chain-team-phases.sh +51 -11
  143. package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +1 -0
  144. package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +17 -1
  145. package/presets/cursor/next/agents/build-verifier.md +2 -0
  146. package/presets/cursor/next/agents/code-reviewer.md +2 -0
  147. package/presets/cursor/next/agents/debugger.md +4 -0
  148. package/presets/cursor/next/agents/feature-developer.md +3 -0
  149. package/presets/cursor/next/agents/qa-tester.md +4 -0
  150. package/presets/cursor/next/agents/unit-test-generator.md +1 -1
  151. package/presets/cursor/next/agents/unit-test-planner.md +1 -1
  152. package/presets/cursor/next/hooks/chain-team-phases.sh +51 -11
  153. package/presets/cursor/next/rules/http-client.mdc +1 -1
  154. package/presets/cursor/next/rules/preset-pr-checklist.mdc +1 -0
  155. package/presets/cursor/next/rules/preset-twin-sync.mdc +17 -1
  156. package/presets/cursor/next/rules/react-ui.mdc +1 -1
  157. package/presets/cursor/next/rules/reference-features.mdc +1 -1
  158. package/presets/cursor/next/rules/tests-e2e-structure.mdc +2 -0
  159. package/presets/cursor/next/rules/tests-unit.mdc +3 -5
  160. package/presets/cursor/next/skills/playwright-e2e/SKILL.md +5 -3
  161. package/presets/cursor/next/skills/unit-testing/SKILL.md +6 -4
  162. package/presets/cursor/nuxt/agents/build-verifier.md +2 -0
  163. package/presets/cursor/nuxt/agents/code-reviewer.md +2 -0
  164. package/presets/cursor/nuxt/agents/debugger.md +4 -0
  165. package/presets/cursor/nuxt/agents/feature-developer.md +3 -0
  166. package/presets/cursor/nuxt/agents/qa-tester.md +4 -0
  167. package/presets/cursor/nuxt/hooks/chain-team-phases.sh +51 -11
  168. package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +1 -0
  169. package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +17 -1
  170. package/presets/cursor/php-hexagonal/agents/build-verifier.md +4 -0
  171. package/presets/cursor/php-hexagonal/agents/code-reviewer.md +4 -0
  172. package/presets/cursor/php-hexagonal/agents/debugger.md +4 -0
  173. package/presets/cursor/php-hexagonal/agents/feature-developer.md +5 -0
  174. package/presets/cursor/php-hexagonal/agents/qa-tester.md +4 -0
  175. package/presets/cursor/php-hexagonal/agents/task-router.md +4 -0
  176. package/presets/cursor/php-hexagonal/hooks/chain-team-phases.sh +51 -11
  177. package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +1 -0
  178. package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +17 -1
  179. package/presets/cursor/php-laravel/agents/build-verifier.md +4 -0
  180. package/presets/cursor/php-laravel/agents/code-reviewer.md +4 -0
  181. package/presets/cursor/php-laravel/agents/debugger.md +4 -0
  182. package/presets/cursor/php-laravel/agents/feature-developer.md +5 -0
  183. package/presets/cursor/php-laravel/agents/qa-tester.md +4 -0
  184. package/presets/cursor/php-laravel/agents/task-router.md +1 -0
  185. package/presets/cursor/php-laravel/hooks/chain-team-phases.sh +51 -11
  186. package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +1 -0
  187. package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +17 -1
  188. package/presets/cursor/svelte/agents/build-verifier.md +2 -0
  189. package/presets/cursor/svelte/agents/code-reviewer.md +2 -0
  190. package/presets/cursor/svelte/agents/debugger.md +4 -0
  191. package/presets/cursor/svelte/agents/feature-developer.md +3 -0
  192. package/presets/cursor/svelte/agents/qa-tester.md +4 -0
  193. package/presets/cursor/svelte/hooks/chain-team-phases.sh +51 -11
  194. package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +1 -0
  195. package/presets/cursor/svelte/rules/preset-twin-sync.mdc +17 -1
  196. package/presets/mavis/README.md +60 -0
  197. package/presets/mavis/next/MAVIS.md +67 -0
  198. package/presets/mavis/next/MCP.md +36 -0
  199. package/presets/mavis/next/README.md +15 -0
  200. package/presets/mavis/next/REPO_AGENTS.md +94 -0
  201. package/presets/mavis/next/adapter-map.json +959 -0
  202. package/presets/mavis/next/agents/README.md +206 -0
  203. package/presets/mavis/next/agents/accessibility-reviewer.md +61 -0
  204. package/presets/mavis/next/agents/api-contract-reviewer.md +57 -0
  205. package/presets/mavis/next/agents/build-verifier.md +110 -0
  206. package/presets/mavis/next/agents/ci-investigator.md +63 -0
  207. package/presets/mavis/next/agents/code-reviewer.md +61 -0
  208. package/presets/mavis/next/agents/codebase-analyzer.md +21 -0
  209. package/presets/mavis/next/agents/debugger.md +80 -0
  210. package/presets/mavis/next/agents/feature-developer.md +101 -0
  211. package/presets/mavis/next/agents/migration-specialist.md +67 -0
  212. package/presets/mavis/next/agents/mockup-reviewer.md +53 -0
  213. package/presets/mavis/next/agents/openspec-author.md +157 -0
  214. package/presets/mavis/next/agents/performance-auditor.md +68 -0
  215. package/presets/mavis/next/agents/playwright-test-generator.md +28 -0
  216. package/presets/mavis/next/agents/playwright-test-healer.md +28 -0
  217. package/presets/mavis/next/agents/playwright-test-planner.md +21 -0
  218. package/presets/mavis/next/agents/qa-tester.md +52 -0
  219. package/presets/mavis/next/agents/security-reviewer.md +54 -0
  220. package/presets/mavis/next/agents/solution-architect.md +68 -0
  221. package/presets/mavis/next/agents/task-analyst.md +166 -0
  222. package/presets/mavis/next/agents/task-router.md +178 -0
  223. package/presets/mavis/next/agents/tech-writer.md +57 -0
  224. package/presets/mavis/next/agents/unit-test-generator.md +31 -0
  225. package/presets/mavis/next/agents/unit-test-healer.md +32 -0
  226. package/presets/mavis/next/agents/unit-test-planner.md +54 -0
  227. package/presets/mavis/next/commands/README.md +49 -0
  228. package/presets/mavis/next/commands/spec-start.md +24 -0
  229. package/presets/mavis/next/commands/task-continue.md +50 -0
  230. package/presets/mavis/next/commands/task.md +50 -0
  231. package/presets/mavis/next/commands/technical-retro.md +80 -0
  232. package/presets/mavis/next/hooks/README.md +22 -0
  233. package/presets/mavis/next/hooks/chain-team-phases.sh +845 -0
  234. package/presets/mavis/next/mcp.json +19 -0
  235. package/presets/mavis/next/rules/README.md +16 -0
  236. package/presets/mavis/next/rules/agent-team-intake.md +11 -0
  237. package/presets/mavis/next/rules/agent-team-orchestrator.md +210 -0
  238. package/presets/mavis/next/rules/ai-local-overlay.md +37 -0
  239. package/presets/mavis/next/rules/anti-sycophancy-discipline.md +23 -0
  240. package/presets/mavis/next/rules/api-services.md +72 -0
  241. package/presets/mavis/next/rules/architecture-boundaries-ui.md +9 -0
  242. package/presets/mavis/next/rules/architecture-boundaries.md +86 -0
  243. package/presets/mavis/next/rules/arrow-functions.md +25 -0
  244. package/presets/mavis/next/rules/code-quality-and-refactoring.md +40 -0
  245. package/presets/mavis/next/rules/code-review-mr.md +40 -0
  246. package/presets/mavis/next/rules/css-property-order-stylelint.md +5 -0
  247. package/presets/mavis/next/rules/design-guidance.md +99 -0
  248. package/presets/mavis/next/rules/feature-delivery-workflow.md +49 -0
  249. package/presets/mavis/next/rules/http-client.md +55 -0
  250. package/presets/mavis/next/rules/layer-barrel-exports.md +68 -0
  251. package/presets/mavis/next/rules/mcp-usage.md +86 -0
  252. package/presets/mavis/next/rules/mockup-review.md +107 -0
  253. package/presets/mavis/next/rules/navigation-router-stack.md +56 -0
  254. package/presets/mavis/next/rules/navigation-router-ui.md +10 -0
  255. package/presets/mavis/next/rules/next-app-core.md +29 -0
  256. package/presets/mavis/next/rules/next-app-router.md +44 -0
  257. package/presets/mavis/next/rules/no-props-spread.md +56 -0
  258. package/presets/mavis/next/rules/no-type-assertion-as-import-export.md +51 -0
  259. package/presets/mavis/next/rules/package-manager.md +11 -0
  260. package/presets/mavis/next/rules/playwright-agents.md +67 -0
  261. package/presets/mavis/next/rules/post-change-lint.md +35 -0
  262. package/presets/mavis/next/rules/preset-layering.md +27 -0
  263. package/presets/mavis/next/rules/preset-no-cross-stack-leakage.md +90 -0
  264. package/presets/mavis/next/rules/preset-pr-checklist.md +35 -0
  265. package/presets/mavis/next/rules/preset-token-budget.md +36 -0
  266. package/presets/mavis/next/rules/preset-twin-sync.md +65 -0
  267. package/presets/mavis/next/rules/product-specs-authoring.md +112 -0
  268. package/presets/mavis/next/rules/product-specs.md +56 -0
  269. package/presets/mavis/next/rules/public-imports.md +50 -0
  270. package/presets/mavis/next/rules/react-a11y-coding.md +31 -0
  271. package/presets/mavis/next/rules/react-next-anti-hallucination.md +60 -0
  272. package/presets/mavis/next/rules/react-ui.md +105 -0
  273. package/presets/mavis/next/rules/reference-features.md +45 -0
  274. package/presets/mavis/next/rules/security-next.md +45 -0
  275. package/presets/mavis/next/rules/store-rtk.md +78 -0
  276. package/presets/mavis/next/rules/technical-retro.md +11 -0
  277. package/presets/mavis/next/rules/tests-e2e-structure.md +72 -0
  278. package/presets/mavis/next/rules/tests-unit.md +72 -0
  279. package/presets/mavis/next/rules/types-jsdoc.md +43 -0
  280. package/presets/mavis/next/rules/ux-design.md +99 -0
  281. package/presets/mavis/next/skills/README.md +21 -0
  282. package/presets/mavis/next/skills/ci-investigation/SKILL.md +39 -0
  283. package/presets/mavis/next/skills/code-review/SKILL.md +39 -0
  284. package/presets/mavis/next/skills/debug-investigation/SKILL.md +26 -0
  285. package/presets/mavis/next/skills/feature-delivery/SKILL.md +34 -0
  286. package/presets/mavis/next/skills/mockup-review/SKILL.md +24 -0
  287. package/presets/mavis/next/skills/playwright-e2e/SKILL.md +28 -0
  288. package/presets/mavis/next/skills/react-performance/SKILL.md +39 -0
  289. package/presets/mavis/next/skills/spec-authoring/SKILL.md +21 -0
  290. package/presets/mavis/next/skills/spec-discovery/SKILL.md +53 -0
  291. package/presets/mavis/next/skills/technical-retro/SKILL.md +49 -0
  292. package/presets/mavis/next/skills/unit-testing/SKILL.md +29 -0
  293. package/presets/mavis/next/skills/ux-design/SKILL.md +31 -0
  294. package/presets/mavis/next/skills/write-adr/SKILL.md +36 -0
  295. package/presets/mavis/next/team/README.md +148 -0
  296. package/presets/mavis/next/team/conventions.md +21 -0
  297. package/presets/mavis/next/team/fixtures/artifact-manifest.example.json +79 -0
  298. package/presets/mavis/next/team/fixtures/bugfix-standard.json +25 -0
  299. package/presets/mavis/next/team/fixtures/feature-full.json +29 -0
  300. package/presets/mavis/next/team/fixtures/feature-light.json +27 -0
  301. package/presets/mavis/next/team/fixtures/feature-with-figma-url.json +17 -0
  302. package/presets/mavis/next/team/fixtures/feature-with-ux-no-figma.json +17 -0
  303. package/presets/mavis/next/team/fixtures/memlog.example.md +9 -0
  304. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/human-gate/metrics.json +69 -0
  305. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/missing-ledger/status.json +5 -0
  306. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/parallel-receipts/metrics.json +79 -0
  307. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/serial-retry/metrics.json +124 -0
  308. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/validation-failed/metrics.json +124 -0
  309. package/presets/mavis/next/team/fixtures/metrics.example.json +54 -0
  310. package/presets/mavis/next/team/fixtures/mockup-review-fail-embedded-status.json +8 -0
  311. package/presets/mavis/next/team/fixtures/mockup-review-standalone.json +14 -0
  312. package/presets/mavis/next/team/fixtures/ux-design-standalone.json +15 -0
  313. package/presets/mavis/next/team/tasks/.gitkeep +0 -0
  314. package/presets/mavis/platform-contract.json +131 -0
  315. package/scripts/capture-cli-golden.mjs +44 -0
  316. package/scripts/check-chain-hook-coverage.sh +1 -0
  317. package/scripts/check-mavis-adapter-map.mjs +294 -0
  318. package/scripts/check-mavis-local-overlay.mjs +56 -0
  319. package/scripts/check-mavis-platform-contract.mjs +388 -0
  320. package/scripts/check-mavis-token-budget.mjs +48 -0
  321. package/scripts/check-preset-leakage.sh +35 -3
  322. package/scripts/check-preset-structure.sh +73 -0
  323. package/scripts/check-preset-token-budget.sh +100 -0
  324. package/scripts/check-shared-core-drift.sh +18 -0
  325. package/scripts/check-task-router-intents.sh +154 -0
  326. package/scripts/fixtures/cli-golden/claude/next.json +130 -0
  327. package/scripts/fixtures/cli-golden/cursor/next.json +132 -0
  328. package/scripts/fixtures/cli-golden/mavis/next.json +125 -0
  329. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/AGENTS.md +1 -0
  330. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/extra.mdc +1 -0
  331. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/sample.mdc +2 -0
  332. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/MAVIS.md +1 -0
  333. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/adapter-map.json +26 -0
  334. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/rules/sample.md +2 -0
  335. package/scripts/fixtures/mavis-adapter-map/valid/cursor/AGENTS.md +1 -0
  336. package/scripts/fixtures/mavis-adapter-map/valid/cursor/rules/sample.mdc +2 -0
  337. package/scripts/fixtures/mavis-adapter-map/valid/mavis/MAVIS.md +1 -0
  338. package/scripts/fixtures/mavis-adapter-map/valid/mavis/adapter-map.json +26 -0
  339. package/scripts/fixtures/mavis-adapter-map/valid/mavis/rules/sample.md +2 -0
  340. package/scripts/fixtures/mavis-platform-contract/invalid-minimax-destination.json +131 -0
  341. package/scripts/fixtures/mavis-platform-contract/invalid-missing-capability.json +126 -0
  342. package/scripts/fixtures/mavis-platform-contract/invalid-released-unknown-stack.json +133 -0
  343. package/scripts/fixtures/mavis-platform-contract/invalid-unknown-native-path.json +131 -0
  344. package/scripts/fixtures/mavis-platform-contract/invalid-version-pin.json +132 -0
  345. package/scripts/fixtures/mavis-platform-contract/valid.json +131 -0
  346. package/scripts/fixtures/mavis-probe/AGENTS.md +6 -0
  347. package/scripts/fixtures/mavis-probe/agents/probe-agent.md +6 -0
  348. package/scripts/fixtures/mavis-probe/fake-mcode.mjs +98 -0
  349. package/scripts/fixtures/mavis-probe/output.schema.json +28 -0
  350. package/scripts/fixtures/mavis-probe/rules/probe.md +5 -0
  351. package/scripts/fixtures/mavis-probe/skills/probe/SKILL.md +11 -0
  352. package/scripts/generate-mavis-next.mjs +704 -0
  353. package/scripts/lib/cli-golden.mjs +50 -0
  354. package/scripts/lib/discover-hooks.mjs +72 -0
  355. package/scripts/probe-mavis-capabilities.mjs +480 -0
  356. package/scripts/test-agent-task-metrics-hooks.mjs +120 -16
  357. package/scripts/test-chain-team-phases-coverage.mjs +110 -8
  358. package/scripts/test-cli-init-clean.mjs +256 -0
  359. package/scripts/test-cli-mcp-merge.mjs +23 -0
  360. package/scripts/test-mavis-context-budget.mjs +55 -0
  361. package/scripts/test-mavis-orchestration.mjs +649 -0
  362. package/scripts/test-probe-mavis-capabilities.mjs +205 -0
  363. package/scripts/test-task-router-intents-fixtures.mjs +109 -0
@@ -9,12 +9,15 @@ 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
- const hooks = [
13
- ...fs.readdirSync(path.join(presetsDirectory, 'cursor')).map((stack) => path.join(presetsDirectory, 'cursor', stack, 'hooks', 'chain-team-phases.sh')),
14
- ...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'hooks', 'chain-team-phases.sh')),
15
- ];
12
+ import { discoverChainHooks, expectedHookCount, loadReleasedMavisStacks, platformFromHook } from './lib/discover-hooks.mjs';
16
13
 
17
- assert.equal(hooks.length, 20, 'expected every packaged Cursor and Claude hook (10 stacks × 2 platforms)');
14
+ const hooks = discoverChainHooks(presetsDirectory);
15
+
16
+ assert.equal(
17
+ hooks.length,
18
+ expectedHookCount(presetsDirectory),
19
+ `expected ${expectedHookCount(presetsDirectory)} packaged hooks`,
20
+ );
18
21
 
19
22
  function readJson(filePath) {
20
23
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -55,7 +58,7 @@ function invokeHookAsync(hook, workingDirectory, attemptId) {
55
58
  }
56
59
 
57
60
  function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {}) {
58
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
61
+ const platform = platformFromHook(hook);
59
62
  const teamRoot = path.join(root, `.${platform}`, 'team');
60
63
  const taskDirectory = path.join(teamRoot, 'tasks', slug);
61
64
  fs.mkdirSync(taskDirectory, { recursive: true });
@@ -70,7 +73,7 @@ function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {})
70
73
  }
71
74
 
72
75
  function prepareTask(root, hook) {
73
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
76
+ const platform = platformFromHook(hook);
74
77
  const teamRoot = path.join(root, `.${platform}`, 'team');
75
78
  const taskDirectory = path.join(teamRoot, 'tasks', 'reverse-parallel');
76
79
  fs.mkdirSync(taskDirectory, { recursive: true });
@@ -162,7 +165,7 @@ for (const hook of hooks) {
162
165
  for (const hook of hooks) {
163
166
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-retry-'));
164
167
  try {
165
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
168
+ const platform = platformFromHook(hook);
166
169
  const runId = 'serial-retry-run';
167
170
  const reviewerAttemptId = `${runId}:step-5:security-reviewer:attempt-1`;
168
171
  const taskDirectory = writeTask(
@@ -242,7 +245,7 @@ for (const hook of hooks) {
242
245
  for (const hook of hooks) {
243
246
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-no-chain-'));
244
247
  try {
245
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
248
+ const platform = platformFromHook(hook);
246
249
  const runId = 'no-chain-run';
247
250
  const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
248
251
  const taskDirectory = writeTask(
@@ -293,7 +296,7 @@ for (const hook of hooks) {
293
296
  for (const hook of hooks) {
294
297
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-mixed-parallel-'));
295
298
  try {
296
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
299
+ const platform = platformFromHook(hook);
297
300
  const runId = 'mixed-parallel-run';
298
301
  const alphaAttemptId = `${runId}:step-1:code-reviewer:attempt-1`;
299
302
  const betaAttemptId = `${runId}:step-1:security-reviewer:attempt-1`;
@@ -360,7 +363,7 @@ for (const hook of hooks) {
360
363
  for (const hook of hooks) {
361
364
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-skip-'));
362
365
  try {
363
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
366
+ const platform = platformFromHook(hook);
364
367
  const runId = 'skip-run';
365
368
  const starterAttemptId = `${runId}:step-0:starter:attempt-1`;
366
369
  const taskDirectory = writeTask(
@@ -418,6 +421,9 @@ for (const hook of hooks) {
418
421
  for (const command of [
419
422
  ...fs.readdirSync(path.join(presetsDirectory, 'cursor')).map((stack) => path.join(presetsDirectory, 'cursor', stack, 'commands', 'task-continue.md')),
420
423
  ...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'commands', 'task-continue.md')),
424
+ ...loadReleasedMavisStacks(presetsDirectory).map((stack) =>
425
+ path.join(presetsDirectory, 'mavis', stack, 'commands', 'task-continue.md'),
426
+ ),
421
427
  ]) {
422
428
  const source = fs.readFileSync(command, 'utf8');
423
429
  assert.match(source, /action":"end_human_gate"/, `${command}: must invoke installed hook end_human_gate action`);
@@ -436,7 +442,7 @@ for (const hook of hooks) {
436
442
  // pre-resume status → hook end_human_gate (no manual pipelineIndex++) → invoke from status.currentAgent.
437
443
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-gate-'));
438
444
  try {
439
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
445
+ const platform = platformFromHook(hook);
440
446
  const stack = path.basename(path.dirname(path.dirname(hook)));
441
447
  const presetCommand = path.join(path.dirname(path.dirname(hook)), 'commands', 'task-continue.md');
442
448
  const installedHook = path.join(root, `.${platform}`, 'hooks', 'chain-team-phases.sh');
@@ -510,7 +516,7 @@ for (const hook of hooks) {
510
516
  for (const hook of hooks) {
511
517
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-manifest-reconcile-'));
512
518
  try {
513
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
519
+ const platform = platformFromHook(hook);
514
520
  const runId = 'manifest-reconcile-run';
515
521
  const reviewerAttemptId = `${runId}:step-1:code-reviewer:attempt-1`;
516
522
  const nowIso = '2026-01-01T00:00:00.000Z';
@@ -617,7 +623,7 @@ for (const hook of hooks) {
617
623
  for (const hook of hooks) {
618
624
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-blocked-receipt-'));
619
625
  try {
620
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
626
+ const platform = platformFromHook(hook);
621
627
  const runId = 'blocked-receipt-run';
622
628
  const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
623
629
  const startedAt = '2026-02-01T00:00:00.000Z';
@@ -708,7 +714,7 @@ for (const hook of hooks) {
708
714
  for (const hook of hooks) {
709
715
  const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-stale-receipt-'));
710
716
  try {
711
- const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
717
+ const platform = platformFromHook(hook);
712
718
  const runId = 'stale-receipt-run';
713
719
  const attemptId = `${runId}:step-2:code-reviewer:attempt-2`;
714
720
  const startedAt = '2026-02-01T00:00:00.000Z';
@@ -892,4 +898,102 @@ for (const hook of hooks) {
892
898
  }
893
899
  }
894
900
 
895
- console.log(`Verified retry, parallel, skip, gate, manifest-reconcile, and aggregation lifecycle in ${hooks.length} hooks.`);
901
+ // ---------------------------------------------------------------------------
902
+ // Test — OD-1: parallel-stop lock race. The hook's waitForMetricsLock must
903
+ // actually block a second writer when the lock file is held. We pre-acquire
904
+ // `metrics.json.lock` (fs.openSync(_, 'wx') — same primitive the hook uses)
905
+ // in the test, then invoke the hook concurrently; the hook must NOT acquire
906
+ // the lock until the test releases it. Without the real lock primitive the
907
+ // hook would race through, run the metrics write, and leave a corrupt ledger.
908
+ // ---------------------------------------------------------------------------
909
+ for (const hook of hooks) {
910
+ const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-lock-race-'));
911
+ try {
912
+ const platform = platformFromHook(hook);
913
+ const runId = 'lock-race-run';
914
+ const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
915
+ const taskDirectory = writeTask(
916
+ root,
917
+ hook,
918
+ 'lock-race',
919
+ { autoChain: true, steps: [{ agent: 'feature-developer' }] },
920
+ {
921
+ slug: 'lock-race',
922
+ intent: 'feature',
923
+ pipelineIndex: 0,
924
+ currentAgent: 'feature-developer',
925
+ phase: 'executing',
926
+ state: 'completed',
927
+ activeAttempts: {
928
+ [attemptId]: {
929
+ agent: 'feature-developer',
930
+ stepIndex: 0,
931
+ attempt: 1,
932
+ startedAt: '2026-01-01T00:00:00.000Z',
933
+ parallelGroupId: null,
934
+ },
935
+ },
936
+ metricsRunId: runId,
937
+ },
938
+ {
939
+ schemaVersion: 1,
940
+ task: { runId, slug: 'lock-race', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'test' },
941
+ events: [
942
+ { eventId: `${runId}:task_started`, type: 'task_started', at: '2026-01-01T00:00:00.000Z' },
943
+ { eventId: `${runId}:start`, type: 'attempt_started', attemptId, agent: 'feature-developer', stepIndex: 0, attempt: 1, at: '2026-01-01T00:00:00.000Z' },
944
+ ],
945
+ },
946
+ );
947
+ const metricsPath = path.join(taskDirectory, 'metrics.json');
948
+ const lockPath = `${metricsPath}.lock`;
949
+
950
+ // Pre-acquire the lock the same way the hook does (fs.openSync 'wx').
951
+ const holderFd = fs.openSync(lockPath, 'wx');
952
+
953
+ // Spawn the hook while the lock is held. The hook must wait.
954
+ const child = spawn('bash', [hook], { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] });
955
+ let stderr = '';
956
+ let stdout = '';
957
+ child.stderr.setEncoding('utf8');
958
+ child.stdout.setEncoding('utf8');
959
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
960
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
961
+ child.stdin.end(JSON.stringify({ status: 'completed', attemptId }));
962
+
963
+ // Give the child time to enter waitForMetricsLock and hit the EEXIST retry path.
964
+ const holdMs = 60;
965
+ await new Promise((resolve) => setTimeout(resolve, holdMs));
966
+ assert.ok(
967
+ child.exitCode === null,
968
+ `${hook}: hook exited prematurely while lock was held (stdout=${stdout.slice(0, 200)} stderr=${stderr.slice(0, 200)})`,
969
+ );
970
+
971
+ // Release the lock — child should now proceed.
972
+ const releaseAt = Date.now();
973
+ fs.closeSync(holderFd);
974
+ fs.unlinkSync(lockPath);
975
+
976
+ const exitCode = await new Promise((resolve, reject) => {
977
+ child.on('error', reject);
978
+ child.on('close', resolve);
979
+ });
980
+ const elapsed = Date.now() - releaseAt;
981
+ assert.equal(exitCode, 0, `${hook}: hook did not exit 0 after lock release (stderr=${stderr.slice(0, 400)})`);
982
+
983
+ // The hook's waitForMetricsLock busy-waits on EEXIST in 10 ms slices; with a
984
+ // 60 ms hold the child should have been blocked for >= holdMs and run to
985
+ // completion in well under the 2 s ceiling. Both assertions guard against
986
+ // regressions to the fake Atomics.wait impl (which never blocked).
987
+ assert.ok(elapsed >= 1, `${hook}: hook finished suspiciously fast after lock release (${elapsed} ms)`);
988
+ assert.ok(elapsed < 1500, `${hook}: hook took ${elapsed} ms after lock release — lock impl may be stuck`);
989
+ const ledger = readJson(metricsPath);
990
+ const finished = ledger.events.find(
991
+ (event) => event.type === 'attempt_finished' && event.attemptId === attemptId,
992
+ );
993
+ assert.ok(finished, `${hook}: lock-race did not emit attempt_finished; ledger=${JSON.stringify(ledger.events.map((e) => e.type))}`);
994
+ } finally {
995
+ fs.rmSync(root, { recursive: true, force: true });
996
+ }
997
+ }
998
+
999
+ console.log(`Verified retry, parallel, skip, gate, manifest-reconcile, lock-race, and aggregation lifecycle in ${hooks.length} hooks.`);
@@ -32,16 +32,19 @@ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
32
32
  const packageDirectory = path.resolve(scriptDirectory, '..');
33
33
  const presetsDirectory = path.join(packageDirectory, 'presets');
34
34
 
35
- const hooks = [
36
- ...fs.readdirSync(path.join(presetsDirectory, 'cursor')).map((stack) => path.join(presetsDirectory, 'cursor', stack, 'hooks', 'chain-team-phases.sh')),
37
- ...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'hooks', 'chain-team-phases.sh')),
38
- ];
35
+ import { discoverChainHooks, expectedHookCount, platformFromHook } from './lib/discover-hooks.mjs';
39
36
 
40
- assert.equal(hooks.length, 20, `expected 20 packaged hooks (10 stacks × 2 platforms), got ${hooks.length}`);
37
+ const hooks = discoverChainHooks(presetsDirectory);
38
+
39
+ assert.equal(
40
+ hooks.length,
41
+ expectedHookCount(presetsDirectory),
42
+ `expected ${expectedHookCount(presetsDirectory)} packaged hooks, got ${hooks.length}`,
43
+ );
41
44
 
42
45
  // mcp-ts does not ship openspec-author (per preset-no-cross-stack-leakage).
43
46
  const HOOKS_WITH_OPENSPEC = hooks.filter((hook) => !hook.includes(`${path.sep}mcp-ts${path.sep}`));
44
- assert.equal(HOOKS_WITH_OPENSPEC.length, 18, 'expected 18 hooks to cover openspec-author');
47
+ assert.equal(HOOKS_WITH_OPENSPEC.length, 19, 'expected 19 hooks to cover openspec-author');
45
48
 
46
49
  function readJson(filePath) {
47
50
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -60,7 +63,7 @@ function invokeHook(hook, workingDirectory, attemptId, payload = {}) {
60
63
  }
61
64
 
62
65
  function platformFor(hook) {
63
- return hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
66
+ return platformFromHook(hook);
64
67
  }
65
68
 
66
69
  function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {}) {
@@ -408,4 +411,103 @@ for (const hook of HOOKS_WITH_OPENSPEC) {
408
411
  }
409
412
  }
410
413
 
411
- console.log(`Verified openspec-author handoff, qa-tester handoff, pipeline misparse blocking, legacy fallback, and writeStatus durability in ${hooks.length} hooks.`);
414
+ // ---------------------------------------------------------------------------
415
+ // Test 6 — stale `attemptId` rejection (WP-1 receipt-binding callout):
416
+ // the hook's `readBoundReceiptOutcome` rejects entries whose `receipt.attemptId`
417
+ // does not match the active attempt, even when `completedAt` is fresh. A
418
+ // receipt bound to a prior attempt ID (e.g. attempt-1 reused after a retry
419
+ // recycled the slot) must not advance the pipeline.
420
+ // ---------------------------------------------------------------------------
421
+ for (const hook of hooks) {
422
+ const root = fs.mkdtempSync(path.join(packageDirectory, '.stale-attempt-id-'));
423
+ try {
424
+ const platform = platformFor(hook);
425
+ const runId = 'stale-attempt-id-run';
426
+ const devAttemptId = `${runId}:step-0:feature-developer:attempt-2`;
427
+ const freshIso = new Date().toISOString();
428
+ writeTask(
429
+ root,
430
+ hook,
431
+ 'stale-attempt-id',
432
+ {
433
+ autoChain: true,
434
+ steps: [
435
+ { agent: 'feature-developer', artifactOutputId: 'implementation' },
436
+ { agent: 'build-verifier', artifactInputIds: ['implementation'], artifactOutputId: 'validation' },
437
+ ],
438
+ },
439
+ {
440
+ slug: 'stale-attempt-id',
441
+ intent: 'feature',
442
+ pipelineIndex: 0,
443
+ currentAgent: 'feature-developer',
444
+ phase: 'executing',
445
+ state: 'in_progress',
446
+ activeAttempts: {
447
+ [devAttemptId]: {
448
+ agent: 'feature-developer',
449
+ stepIndex: 0,
450
+ attempt: 2,
451
+ startedAt: freshIso,
452
+ parallelGroupId: null,
453
+ },
454
+ },
455
+ metricsRunId: runId,
456
+ },
457
+ {
458
+ schemaVersion: 1,
459
+ task: { runId, slug: 'stale-attempt-id', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'standard' },
460
+ events: [
461
+ { eventId: `${runId}:task_started`, type: 'task_started', at: freshIso },
462
+ { eventId: `${runId}:dev-start`, type: 'attempt_started', attemptId: devAttemptId, agent: 'feature-developer', stepIndex: 0, attempt: 2, at: freshIso },
463
+ ],
464
+ },
465
+ {
466
+ 'artifact-manifest.json': `${JSON.stringify(
467
+ {
468
+ schemaVersion: 1,
469
+ slug: 'stale-attempt-id',
470
+ updatedAt: freshIso,
471
+ artifacts: [
472
+ {
473
+ id: 'implementation',
474
+ producer: 'feature-developer',
475
+ status: 'completed',
476
+ authoritativePaths: ['implementation.md'],
477
+ acceptanceCriteriaIds: ['AC-1'],
478
+ taskIds: ['T1'],
479
+ timestamps: { createdAt: freshIso, updatedAt: freshIso, completedAt: freshIso },
480
+ receipt: {
481
+ outcome: 'completed',
482
+ // Stale: belongs to attempt-1 of the same (step, agent); the active attempt is attempt-2.
483
+ attemptId: `${runId}:step-0:feature-developer:attempt-1`,
484
+ summary: 'Receipt bound to a prior attempt; should be rejected.',
485
+ evidencePaths: ['implementation.md'],
486
+ },
487
+ },
488
+ ],
489
+ },
490
+ null,
491
+ 2,
492
+ )}\n`,
493
+ },
494
+ );
495
+
496
+ const taskDirectory = path.join(root, `.${platform}`, 'team', 'tasks', 'stale-attempt-id');
497
+ const parsed = JSON.parse(invokeHook(hook, root, devAttemptId));
498
+ assert.deepEqual(parsed, {}, `${hook}: stale-attemptId receipt must not hand off`);
499
+ const status = readJson(path.join(taskDirectory, 'status.json'));
500
+ assert.equal(status.pipelineIndex, 0, `${hook}: stale-attemptId receipt advanced the pipeline`);
501
+ assert.equal(status.currentAgent, 'feature-developer', `${hook}: stale-attemptId receipt changed the active producer`);
502
+ assert.equal(status.state, 'awaiting_artifact_receipt', `${hook}: stale-attemptId did not enter receipt-wait state (got: ${status.state})`);
503
+ assert.ok(status.activeAttempts[devAttemptId], `${hook}: stale-attemptId consumed the active attempt`);
504
+ const finished = readJson(path.join(taskDirectory, 'metrics.json')).events.filter(
505
+ (event) => event.type === 'attempt_finished' && event.attemptId === devAttemptId,
506
+ );
507
+ assert.equal(finished.length, 0, `${hook}: stale-attemptId emitted terminal metrics`);
508
+ } finally {
509
+ fs.rmSync(root, { recursive: true, force: true });
510
+ }
511
+ }
512
+
513
+ console.log(`Verified openspec-author handoff, qa-tester handoff, pipeline misparse blocking, legacy fallback, writeStatus durability, and stale attemptId rejection in ${hooks.length} hooks.`);
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI init/clean goldens and Mavis P0 scenarios.
4
+ */
5
+ import assert from 'node:assert/strict';
6
+ import { execFileSync } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { runInit, snapshotInstall } from './lib/cli-golden.mjs';
12
+
13
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
14
+ const packageDirectory = path.resolve(scriptDirectory, '..');
15
+ const cli = path.join(packageDirectory, 'bin', 'cli.js');
16
+ const goldenRoot = path.join(scriptDirectory, 'fixtures', 'cli-golden');
17
+
18
+ function readJson(filePath) {
19
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
20
+ }
21
+
22
+ function writeJson(filePath, value) {
23
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
24
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
25
+ }
26
+
27
+ function runClean(cwd, tool) {
28
+ return execFileSync(process.execPath, [cli, 'clean', tool, '--cwd', cwd], {
29
+ encoding: 'utf8',
30
+ });
31
+ }
32
+
33
+ function runCli(cwd, args, env = {}) {
34
+ try {
35
+ return {
36
+ status: 0,
37
+ stdout: execFileSync(process.execPath, [cli, ...args, '--cwd', cwd], {
38
+ encoding: 'utf8',
39
+ env: { ...process.env, ...env },
40
+ }),
41
+ };
42
+ } catch (error) {
43
+ return {
44
+ status: error.status ?? 1,
45
+ stdout: `${error.stdout || ''}`,
46
+ stderr: `${error.stderr || ''}`,
47
+ };
48
+ }
49
+ }
50
+
51
+ function assertGolden(tool, preset) {
52
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `ai-rules-golden-check-${tool}-`));
53
+ try {
54
+ runInit(cli, tmp, tool, preset);
55
+ const expected = readJson(path.join(goldenRoot, tool, `${preset}.json`));
56
+ const actual = snapshotInstall(tmp);
57
+ if (tool === 'mavis') {
58
+ delete actual['.mavis/.ai-rules-install.json'];
59
+ delete expected.files['.mavis/.ai-rules-install.json'];
60
+ }
61
+ assert.deepEqual(actual, expected.files, `${tool}/${preset} golden drifted`);
62
+ } finally {
63
+ fs.rmSync(tmp, { recursive: true, force: true });
64
+ }
65
+ }
66
+
67
+ function listHomeMinimax(home) {
68
+ const root = path.join(home, '.minimax');
69
+ if (!fs.existsSync(root)) {
70
+ return [];
71
+ }
72
+ const out = [];
73
+ const visit = (dir) => {
74
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
75
+ const full = path.join(dir, entry.name);
76
+ out.push(path.relative(root, full));
77
+ if (entry.isDirectory()) {
78
+ visit(full);
79
+ }
80
+ }
81
+ };
82
+ visit(root);
83
+ return out.sort();
84
+ }
85
+
86
+ assertGolden('cursor', 'next');
87
+ assertGolden('claude', 'next');
88
+
89
+ const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-home-'));
90
+ fs.mkdirSync(path.join(isolatedHome, '.minimax'));
91
+ const homeBefore = listHomeMinimax(isolatedHome);
92
+
93
+ const fresh = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-fresh-'));
94
+ const initOut = execFileSync(
95
+ process.execPath,
96
+ [cli, 'init', 'mavis', '--preset', 'next', '--cwd', fresh],
97
+ { encoding: 'utf8', env: { ...process.env, HOME: isolatedHome } },
98
+ );
99
+ assert.match(initOut, /\.mavis installed/);
100
+ assert.match(initOut, /do not write ~\/\.minimax\/mcp\.json/i);
101
+ assert.equal(fs.existsSync(path.join(fresh, 'AGENTS.md')), true);
102
+ assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'AGENTS.md')), true);
103
+ assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'rules', 'next-app-core.md')), true);
104
+ assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'hooks', 'chain-team-phases.sh')), true);
105
+ assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'mcp.json')), true);
106
+ assert.equal(fs.existsSync(path.join(fresh, '.mavis', 'adapter-map.json')), false);
107
+ assert.equal(fs.existsSync(path.join(fresh, 'docs', 'specs', 'INDEX.md')), true);
108
+ const manifest = readJson(path.join(fresh, '.mavis', '.ai-rules-install.json'));
109
+ assert.equal(manifest.schemaVersion, 1);
110
+ assert.equal(manifest.platform, 'mavis');
111
+ assert.equal(manifest.preset, 'next');
112
+ assert.ok(manifest.managedFiles['AGENTS.md']);
113
+ assert.ok(manifest.shippedMcpServerIds.includes('context7'));
114
+ assert.equal(manifest.docsSeed.seeded, true);
115
+
116
+ const existingAgents = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-agents-'));
117
+ fs.writeFileSync(path.join(existingAgents, 'AGENTS.md'), '# consumer agents\n');
118
+ runInit(cli, existingAgents, 'mavis', 'next');
119
+ assert.match(fs.readFileSync(path.join(existingAgents, 'AGENTS.md'), 'utf8'), /Mavis compatibility entry/);
120
+
121
+ const specsExist = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-specs-'));
122
+ fs.mkdirSync(path.join(specsExist, 'docs', 'specs'), { recursive: true });
123
+ runInit(cli, specsExist, 'mavis', 'next');
124
+ assert.equal(fs.existsSync(path.join(specsExist, 'docs', 'specs', 'INDEX.md')), false);
125
+ assert.equal(
126
+ readJson(path.join(specsExist, '.mavis', '.ai-rules-install.json')).docsSeed.reason,
127
+ 'directory-exists',
128
+ );
129
+
130
+ writeJson(path.join(fresh, '.mavis', 'mcp.json'), {
131
+ mcpServers: {
132
+ context7: {
133
+ command: 'npx',
134
+ args: ['-y', '@upstash/context7-mcp@old'],
135
+ env: { CONTEXT7_API_KEY: 'keep-me' },
136
+ },
137
+ storybook: { url: 'http://localhost:6006/mcp' },
138
+ },
139
+ localOnly: true,
140
+ });
141
+ const repeat = runInit(cli, fresh, 'mavis', 'next');
142
+ assert.match(repeat, /kept extra servers: storybook/);
143
+ const merged = readJson(path.join(fresh, '.mavis', 'mcp.json'));
144
+ assert.equal(merged.localOnly, true);
145
+ assert.ok(merged.mcpServers.storybook);
146
+ assert.equal(merged.mcpServers.context7.env.CONTEXT7_API_KEY, 'keep-me');
147
+
148
+ merged.localNote = true;
149
+ writeJson(path.join(fresh, '.mavis', 'mcp.json'), merged);
150
+ const modifiedRel = path.join(fresh, '.mavis', 'rules', 'next-app-core.md');
151
+ fs.appendFileSync(modifiedRel, '\n# local overlay note\n');
152
+ runClean(fresh, 'mavis');
153
+ assert.equal(fs.existsSync(modifiedRel), true);
154
+ assert.equal(fs.existsSync(path.join(fresh, '.mavis', '.ai-rules-install.json')), false);
155
+ assert.equal(fs.existsSync(path.join(fresh, 'AGENTS.md')), false);
156
+ assert.ok(readJson(path.join(fresh, '.mavis', 'mcp.json')).mcpServers.storybook);
157
+ assert.equal(readJson(path.join(fresh, '.mavis', 'mcp.json')).mcpServers.context7, undefined);
158
+ assert.equal(readJson(path.join(fresh, '.mavis', 'mcp.json')).localNote, true);
159
+
160
+ const missing = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-missing-'));
161
+ fs.writeFileSync(path.join(missing, 'keep.txt'), 'safe\n');
162
+ const missingClean = runCli(missing, ['clean', 'mavis']);
163
+ assert.equal(missingClean.status, 1);
164
+ assert.match(`${missingClean.stderr}${missingClean.stdout}`, /nothing removed/);
165
+ assert.equal(fs.readFileSync(path.join(missing, 'keep.txt'), 'utf8'), 'safe\n');
166
+
167
+ const corrupt = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-corrupt-'));
168
+ fs.mkdirSync(path.join(corrupt, '.mavis'), { recursive: true });
169
+ fs.writeFileSync(path.join(corrupt, '.mavis', '.ai-rules-install.json'), '{not json');
170
+ fs.writeFileSync(path.join(corrupt, '.mavis', 'keep.md'), 'keep\n');
171
+ const corruptClean = runCli(corrupt, ['clean', 'mavis']);
172
+ assert.equal(corruptClean.status, 1);
173
+ assert.equal(fs.readFileSync(path.join(corrupt, '.mavis', 'keep.md'), 'utf8'), 'keep\n');
174
+
175
+ const twice = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-twice-'));
176
+ runInit(cli, twice, 'mavis', 'next');
177
+ runClean(twice, 'mavis');
178
+ const secondClean = runCli(twice, ['clean', 'mavis']);
179
+ assert.equal(secondClean.status, 1);
180
+
181
+ const rollback = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-rollback-'));
182
+ fs.writeFileSync(path.join(rollback, 'AGENTS.md'), '# prior\n');
183
+ const fail = runCli(rollback, ['init', 'mavis', '--preset', 'next'], {
184
+ AI_RULES_MAVIS_FAIL_PHASE: 'write',
185
+ });
186
+ assert.equal(fail.status, 1);
187
+ assert.equal(fs.readFileSync(path.join(rollback, 'AGENTS.md'), 'utf8'), '# prior\n');
188
+ assert.equal(fs.existsSync(path.join(rollback, '.mavis')), false);
189
+
190
+ const failMcp = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-fail-mcp-'));
191
+ fs.writeFileSync(path.join(failMcp, 'AGENTS.md'), '# prior mcp\n');
192
+ const failMcpRun = runCli(failMcp, ['init', 'mavis', '--preset', 'next'], {
193
+ AI_RULES_MAVIS_FAIL_PHASE: 'mcp',
194
+ });
195
+ assert.equal(failMcpRun.status, 1);
196
+ assert.equal(fs.readFileSync(path.join(failMcp, 'AGENTS.md'), 'utf8'), '# prior mcp\n');
197
+ assert.equal(fs.existsSync(path.join(failMcp, '.mavis')), false);
198
+
199
+ const failManifest = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-fail-manifest-'));
200
+ fs.writeFileSync(path.join(failManifest, 'AGENTS.md'), '# prior manifest\n');
201
+ const failManifestRun = runCli(failManifest, ['init', 'mavis', '--preset', 'next'], {
202
+ AI_RULES_MAVIS_FAIL_PHASE: 'manifest',
203
+ });
204
+ assert.equal(failManifestRun.status, 1);
205
+ assert.equal(fs.readFileSync(path.join(failManifest, 'AGENTS.md'), 'utf8'), '# prior manifest\n');
206
+ assert.equal(fs.existsSync(path.join(failManifest, '.mavis')), false);
207
+
208
+ const toolFirst = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-tool-first-'));
209
+ execFileSync(process.execPath, [cli, 'mavis', 'init', '--preset', 'next', '--cwd', toolFirst], {
210
+ encoding: 'utf8',
211
+ });
212
+ assert.equal(fs.existsSync(path.join(toolFirst, '.mavis', 'AGENTS.md')), true);
213
+
214
+ const unreleased = runCli(fresh, ['init', 'mavis', '--preset', 'nuxt']);
215
+ assert.equal(unreleased.status, 1);
216
+ assert.match(`${unreleased.stderr}${unreleased.stdout}`, /not released yet/i);
217
+ assert.match(`${unreleased.stderr}${unreleased.stdout}`, /Released stacks: next/);
218
+
219
+ assert.deepEqual(listHomeMinimax(isolatedHome), homeBefore);
220
+
221
+ const mavisGoldenDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mavis-golden-'));
222
+ runInit(cli, mavisGoldenDir, 'mavis', 'next');
223
+ const mavisFiles = snapshotInstall(mavisGoldenDir);
224
+ delete mavisFiles['.mavis/.ai-rules-install.json'];
225
+ const mavisGoldenPath = path.join(goldenRoot, 'mavis', 'next.json');
226
+ if (!fs.existsSync(mavisGoldenPath)) {
227
+ writeJson(mavisGoldenPath, {
228
+ schemaVersion: 1,
229
+ tool: 'mavis',
230
+ preset: 'next',
231
+ files: mavisFiles,
232
+ });
233
+ } else {
234
+ const expectedMavis = readJson(mavisGoldenPath);
235
+ delete expectedMavis.files['.mavis/.ai-rules-install.json'];
236
+ assert.deepEqual(mavisFiles, expectedMavis.files, 'mavis/next golden drifted');
237
+ }
238
+
239
+ for (const dir of [
240
+ isolatedHome,
241
+ fresh,
242
+ existingAgents,
243
+ specsExist,
244
+ missing,
245
+ corrupt,
246
+ twice,
247
+ rollback,
248
+ failMcp,
249
+ failManifest,
250
+ toolFirst,
251
+ mavisGoldenDir,
252
+ ]) {
253
+ fs.rmSync(dir, { recursive: true, force: true });
254
+ }
255
+
256
+ console.log('test-cli-init-clean: ok');
@@ -131,6 +131,29 @@ try {
131
131
  assert.ok(restored.mcpServers.playwright);
132
132
  fs.rmSync(deletedDir, { recursive: true, force: true });
133
133
 
134
+ const mavisDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-rules-mcp-mavis-'));
135
+ const mavisMcp = path.join(mavisDir, '.mavis', 'mcp.json');
136
+ writeJson(mavisMcp, {
137
+ mcpServers: {
138
+ context7: {
139
+ command: 'npx',
140
+ args: ['-y', '@upstash/context7-mcp@old'],
141
+ env: { CONTEXT7_API_KEY: 'keep-mavis' },
142
+ },
143
+ storybook: { url: 'http://localhost:6006/mcp' },
144
+ },
145
+ localOnly: true,
146
+ });
147
+ const mavisOut = init(mavisDir, 'mavis', 'next');
148
+ assert.match(mavisOut, /kept extra servers: storybook/);
149
+ const mavisMerged = readJson(mavisMcp);
150
+ assert.equal(mavisMerged.localOnly, true);
151
+ assert.ok(mavisMerged.mcpServers.storybook);
152
+ assert.equal(mavisMerged.mcpServers.context7.env.CONTEXT7_API_KEY, 'keep-mavis');
153
+ assert.ok(mavisMerged.mcpServers.playwright);
154
+ assert.equal(fs.existsSync(path.join(mavisDir, '.minimax')), false);
155
+ fs.rmSync(mavisDir, { recursive: true, force: true });
156
+
134
157
  console.log('test-cli-mcp-merge: ok');
135
158
  } finally {
136
159
  fs.rmSync(tmp, { recursive: true, force: true });