@bonesofspring/ai-rules 0.2.20 → 0.2.21

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 (405) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +17 -2
  3. package/bin/cli.js +83 -6
  4. package/package.json +5 -2
  5. package/presets/_shared/README.md +2 -1
  6. package/presets/_shared/assets/docs-specs/INDEX.md +4 -1
  7. package/presets/_shared/core/agent-team/agent-artifact-contracts.md +13 -0
  8. package/presets/_shared/core/agent-team/agent-task-metrics.md +1 -1
  9. package/presets/_shared/core/agent-team/agent-team-orchestrator.md +1 -3
  10. package/presets/_shared/core/meta/preset-pr-checklist.md +5 -1
  11. package/presets/_shared/core/meta/preset-token-budget.md +7 -3
  12. package/presets/_shared/core/meta/preset-twin-sync.md +2 -0
  13. package/presets/_shared/core/testing/testing-principles.md +70 -0
  14. package/presets/claude/android-kotlin/agents/README.md +2 -1
  15. package/presets/claude/android-kotlin/agents/feature-developer.md +1 -1
  16. package/presets/claude/android-kotlin/agents/openspec-author.md +163 -0
  17. package/presets/claude/android-kotlin/agents/task-analyst.md +1 -1
  18. package/presets/claude/android-kotlin/agents/task-router.md +5 -1
  19. package/presets/claude/android-kotlin/agents/tech-writer.md +4 -0
  20. package/presets/claude/android-kotlin/commands/README.md +0 -2
  21. package/presets/claude/android-kotlin/commands/task-continue.md +1 -1
  22. package/presets/claude/android-kotlin/commands/task.md +1 -5
  23. package/presets/claude/android-kotlin/commands/technical-retro.md +2 -2
  24. package/presets/claude/android-kotlin/hooks/chain-team-phases.sh +54 -2
  25. package/presets/claude/android-kotlin/rules/README.md +1 -0
  26. package/presets/claude/android-kotlin/rules/tooling-and-review/agent-team-orchestrator.md +7 -4
  27. package/presets/claude/android-kotlin/rules/tooling-and-review/ai-local-overlay.md +48 -0
  28. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  29. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-token-budget.md +7 -3
  30. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +2 -0
  31. package/presets/claude/go/README.md +1 -1
  32. package/presets/claude/go/agents/README.md +3 -2
  33. package/presets/claude/go/agents/openspec-author.md +163 -0
  34. package/presets/claude/go/agents/task-router.md +5 -1
  35. package/presets/claude/go/agents/tech-writer.md +4 -0
  36. package/presets/claude/go/commands/README.md +0 -2
  37. package/presets/claude/go/commands/task-continue.md +1 -1
  38. package/presets/claude/go/commands/task.md +1 -4
  39. package/presets/claude/go/hooks/chain-team-phases.sh +54 -3
  40. package/presets/claude/go/rules/README.md +1 -0
  41. package/presets/claude/go/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
  42. package/presets/claude/go/rules/tooling-and-review/ai-local-overlay.md +48 -0
  43. package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  44. package/presets/claude/go/rules/tooling-and-review/preset-token-budget.md +7 -3
  45. package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +2 -0
  46. package/presets/claude/ios-swift/agents/README.md +3 -2
  47. package/presets/claude/ios-swift/agents/feature-developer.md +1 -1
  48. package/presets/claude/ios-swift/agents/openspec-author.md +163 -0
  49. package/presets/claude/ios-swift/agents/task-analyst.md +1 -1
  50. package/presets/claude/ios-swift/agents/task-router.md +5 -1
  51. package/presets/claude/ios-swift/agents/tech-writer.md +4 -0
  52. package/presets/claude/ios-swift/commands/README.md +0 -2
  53. package/presets/claude/ios-swift/commands/task-continue.md +1 -1
  54. package/presets/claude/ios-swift/commands/task.md +1 -5
  55. package/presets/claude/ios-swift/commands/technical-retro.md +2 -2
  56. package/presets/claude/ios-swift/hooks/chain-team-phases.sh +54 -2
  57. package/presets/claude/ios-swift/rules/README.md +1 -0
  58. package/presets/claude/ios-swift/rules/tooling-and-review/agent-team-orchestrator.md +7 -4
  59. package/presets/claude/ios-swift/rules/tooling-and-review/ai-local-overlay.md +48 -0
  60. package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  61. package/presets/claude/ios-swift/rules/tooling-and-review/preset-token-budget.md +7 -3
  62. package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +2 -0
  63. package/presets/claude/java/CLAUDE.md +1 -1
  64. package/presets/claude/java/agents/README.md +3 -2
  65. package/presets/claude/java/agents/openspec-author.md +163 -0
  66. package/presets/claude/java/agents/task-router.md +5 -1
  67. package/presets/claude/java/agents/tech-writer.md +4 -0
  68. package/presets/claude/java/commands/README.md +0 -2
  69. package/presets/claude/java/commands/task-continue.md +1 -1
  70. package/presets/claude/java/commands/task.md +1 -4
  71. package/presets/claude/java/hooks/chain-team-phases.sh +54 -3
  72. package/presets/claude/java/rules/README.md +1 -0
  73. package/presets/claude/java/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
  74. package/presets/claude/java/rules/tooling-and-review/ai-local-overlay.md +48 -0
  75. package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  76. package/presets/claude/java/rules/tooling-and-review/preset-token-budget.md +7 -3
  77. package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +2 -0
  78. package/presets/claude/mcp-ts/agents/task-analyst.md +1 -1
  79. package/presets/claude/mcp-ts/commands/README.md +0 -2
  80. package/presets/claude/mcp-ts/commands/task-continue.md +1 -1
  81. package/presets/claude/mcp-ts/commands/task.md +1 -4
  82. package/presets/claude/mcp-ts/commands/technical-retro.md +2 -2
  83. package/presets/claude/mcp-ts/hooks/chain-team-phases.sh +40 -9
  84. package/presets/claude/mcp-ts/rules/README.md +1 -0
  85. package/presets/claude/mcp-ts/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
  86. package/presets/claude/mcp-ts/rules/tooling-and-review/ai-local-overlay.md +48 -0
  87. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  88. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-token-budget.md +7 -3
  89. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +2 -0
  90. package/presets/claude/next/README.md +1 -0
  91. package/presets/claude/next/agents/README.md +2 -2
  92. package/presets/claude/next/agents/build-verifier.md +11 -1
  93. package/presets/claude/next/agents/feature-developer.md +1 -1
  94. package/presets/claude/next/agents/openspec-author.md +163 -0
  95. package/presets/claude/next/agents/task-analyst.md +1 -1
  96. package/presets/claude/next/agents/task-router.md +5 -1
  97. package/presets/claude/next/agents/tech-writer.md +4 -0
  98. package/presets/claude/next/commands/README.md +0 -2
  99. package/presets/claude/next/commands/task-continue.md +1 -1
  100. package/presets/claude/next/commands/task.md +1 -4
  101. package/presets/claude/next/hooks/chain-team-phases.sh +54 -2
  102. package/presets/claude/next/rules/README.md +1 -0
  103. package/presets/claude/next/rules/testing/README.md +6 -0
  104. package/presets/claude/next/rules/testing/tests-e2e-structure.md +84 -48
  105. package/presets/claude/next/rules/testing/tests-unit.md +79 -50
  106. package/presets/claude/next/rules/tooling-and-review/agent-team-orchestrator.md +3 -4
  107. package/presets/claude/next/rules/tooling-and-review/ai-local-overlay.md +48 -0
  108. package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  109. package/presets/claude/next/rules/tooling-and-review/preset-token-budget.md +7 -3
  110. package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +2 -0
  111. package/presets/claude/next/team/README.md +0 -3
  112. package/presets/claude/nuxt/agents/README.md +1 -1
  113. package/presets/claude/nuxt/agents/feature-developer.md +1 -1
  114. package/presets/claude/nuxt/agents/openspec-author.md +163 -0
  115. package/presets/claude/nuxt/agents/task-analyst.md +1 -1
  116. package/presets/claude/nuxt/agents/task-router.md +5 -1
  117. package/presets/claude/nuxt/agents/tech-writer.md +4 -0
  118. package/presets/claude/nuxt/commands/README.md +0 -2
  119. package/presets/claude/nuxt/commands/task-continue.md +1 -1
  120. package/presets/claude/nuxt/commands/task.md +1 -5
  121. package/presets/claude/nuxt/commands/technical-retro.md +2 -2
  122. package/presets/claude/nuxt/hooks/chain-team-phases.sh +54 -2
  123. package/presets/claude/nuxt/rules/README.md +1 -0
  124. package/presets/claude/nuxt/rules/testing/README.md +6 -0
  125. package/presets/claude/nuxt/rules/testing/tests-e2e-structure.md +86 -43
  126. package/presets/claude/nuxt/rules/testing/tests-unit.md +58 -45
  127. package/presets/claude/nuxt/rules/tooling-and-review/agent-team-orchestrator.md +8 -5
  128. package/presets/claude/nuxt/rules/tooling-and-review/ai-local-overlay.md +48 -0
  129. package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  130. package/presets/claude/nuxt/rules/tooling-and-review/preset-token-budget.md +7 -3
  131. package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +2 -0
  132. package/presets/claude/nuxt/team/README.md +0 -3
  133. package/presets/claude/php-hexagonal/CLAUDE.md +1 -1
  134. package/presets/claude/php-hexagonal/agents/README.md +3 -2
  135. package/presets/claude/php-hexagonal/agents/openspec-author.md +163 -0
  136. package/presets/claude/php-hexagonal/agents/task-router.md +5 -1
  137. package/presets/claude/php-hexagonal/agents/tech-writer.md +4 -0
  138. package/presets/claude/php-hexagonal/commands/README.md +0 -2
  139. package/presets/claude/php-hexagonal/commands/task-continue.md +1 -1
  140. package/presets/claude/php-hexagonal/commands/task.md +1 -4
  141. package/presets/claude/php-hexagonal/hooks/chain-team-phases.sh +54 -3
  142. package/presets/claude/php-hexagonal/rules/README.md +1 -0
  143. package/presets/claude/php-hexagonal/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
  144. package/presets/claude/php-hexagonal/rules/tooling-and-review/ai-local-overlay.md +48 -0
  145. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  146. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-token-budget.md +7 -3
  147. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +2 -0
  148. package/presets/claude/php-laravel/agents/README.md +3 -2
  149. package/presets/claude/php-laravel/agents/openspec-author.md +163 -0
  150. package/presets/claude/php-laravel/agents/task-router.md +5 -1
  151. package/presets/claude/php-laravel/agents/tech-writer.md +4 -0
  152. package/presets/claude/php-laravel/commands/README.md +0 -2
  153. package/presets/claude/php-laravel/commands/task-continue.md +1 -1
  154. package/presets/claude/php-laravel/commands/task.md +1 -4
  155. package/presets/claude/php-laravel/hooks/chain-team-phases.sh +54 -3
  156. package/presets/claude/php-laravel/rules/README.md +1 -0
  157. package/presets/claude/php-laravel/rules/tooling-and-review/agent-team-orchestrator.md +2 -4
  158. package/presets/claude/php-laravel/rules/tooling-and-review/ai-local-overlay.md +48 -0
  159. package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  160. package/presets/claude/php-laravel/rules/tooling-and-review/preset-token-budget.md +7 -3
  161. package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +2 -0
  162. package/presets/claude/svelte/agents/README.md +1 -1
  163. package/presets/claude/svelte/agents/feature-developer.md +1 -1
  164. package/presets/claude/svelte/agents/openspec-author.md +163 -0
  165. package/presets/claude/svelte/agents/task-analyst.md +1 -1
  166. package/presets/claude/svelte/agents/task-router.md +5 -1
  167. package/presets/claude/svelte/agents/tech-writer.md +4 -0
  168. package/presets/claude/svelte/commands/README.md +0 -2
  169. package/presets/claude/svelte/commands/task-continue.md +1 -1
  170. package/presets/claude/svelte/commands/task.md +1 -5
  171. package/presets/claude/svelte/commands/technical-retro.md +2 -2
  172. package/presets/claude/svelte/hooks/chain-team-phases.sh +54 -2
  173. package/presets/claude/svelte/rules/README.md +1 -0
  174. package/presets/claude/svelte/rules/testing/README.md +6 -0
  175. package/presets/claude/svelte/rules/testing/tests-e2e-structure.md +87 -45
  176. package/presets/claude/svelte/rules/testing/tests-unit.md +70 -23
  177. package/presets/claude/svelte/rules/tooling-and-review/agent-team-orchestrator.md +8 -5
  178. package/presets/claude/svelte/rules/tooling-and-review/ai-local-overlay.md +48 -0
  179. package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +5 -1
  180. package/presets/claude/svelte/rules/tooling-and-review/preset-token-budget.md +7 -3
  181. package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +2 -0
  182. package/presets/claude/svelte/team/README.md +0 -3
  183. package/presets/cursor/android-kotlin/agents/README.md +2 -1
  184. package/presets/cursor/android-kotlin/agents/feature-developer.md +1 -1
  185. package/presets/cursor/android-kotlin/agents/openspec-author.md +164 -0
  186. package/presets/cursor/android-kotlin/agents/task-analyst.md +1 -1
  187. package/presets/cursor/android-kotlin/agents/task-router.md +5 -1
  188. package/presets/cursor/android-kotlin/agents/tech-writer.md +4 -0
  189. package/presets/cursor/android-kotlin/commands/README.md +0 -2
  190. package/presets/cursor/android-kotlin/commands/task-continue.md +1 -1
  191. package/presets/cursor/android-kotlin/commands/task.md +1 -5
  192. package/presets/cursor/android-kotlin/commands/technical-retro.md +2 -2
  193. package/presets/cursor/android-kotlin/hooks/chain-team-phases.sh +54 -2
  194. package/presets/cursor/android-kotlin/rules/agent-team-orchestrator.mdc +7 -4
  195. package/presets/cursor/android-kotlin/rules/ai-local-overlay.mdc +43 -0
  196. package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +5 -1
  197. package/presets/cursor/android-kotlin/rules/preset-token-budget.mdc +7 -3
  198. package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +2 -0
  199. package/presets/cursor/go/README.md +6 -6
  200. package/presets/cursor/go/agents/README.md +3 -2
  201. package/presets/cursor/go/agents/openspec-author.md +164 -0
  202. package/presets/cursor/go/agents/task-router.md +5 -1
  203. package/presets/cursor/go/agents/tech-writer.md +4 -0
  204. package/presets/cursor/go/commands/README.md +0 -2
  205. package/presets/cursor/go/commands/task-continue.md +1 -1
  206. package/presets/cursor/go/commands/task.md +1 -4
  207. package/presets/cursor/go/commands/technical-retro.md +2 -2
  208. package/presets/cursor/go/hooks/chain-team-phases.sh +54 -3
  209. package/presets/cursor/go/rules/agent-team-orchestrator.mdc +2 -4
  210. package/presets/cursor/go/rules/ai-local-overlay.mdc +43 -0
  211. package/presets/cursor/go/rules/preset-pr-checklist.mdc +5 -1
  212. package/presets/cursor/go/rules/preset-token-budget.mdc +7 -3
  213. package/presets/cursor/go/rules/preset-twin-sync.mdc +2 -0
  214. package/presets/cursor/ios-swift/README.md +6 -6
  215. package/presets/cursor/ios-swift/agents/README.md +3 -2
  216. package/presets/cursor/ios-swift/agents/feature-developer.md +1 -1
  217. package/presets/cursor/ios-swift/agents/openspec-author.md +164 -0
  218. package/presets/cursor/ios-swift/agents/task-analyst.md +1 -1
  219. package/presets/cursor/ios-swift/agents/task-router.md +5 -1
  220. package/presets/cursor/ios-swift/agents/tech-writer.md +4 -0
  221. package/presets/cursor/ios-swift/commands/README.md +1 -3
  222. package/presets/cursor/ios-swift/commands/task-continue.md +1 -1
  223. package/presets/cursor/ios-swift/commands/task.md +1 -5
  224. package/presets/cursor/ios-swift/commands/technical-retro.md +2 -2
  225. package/presets/cursor/ios-swift/hooks/chain-team-phases.sh +54 -2
  226. package/presets/cursor/ios-swift/rules/agent-team-orchestrator.mdc +7 -4
  227. package/presets/cursor/ios-swift/rules/ai-local-overlay.mdc +43 -0
  228. package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +5 -1
  229. package/presets/cursor/ios-swift/rules/preset-token-budget.mdc +7 -3
  230. package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +2 -0
  231. package/presets/cursor/java/AGENTS.md +1 -1
  232. package/presets/cursor/java/agents/README.md +3 -2
  233. package/presets/cursor/java/agents/openspec-author.md +164 -0
  234. package/presets/cursor/java/agents/task-router.md +5 -1
  235. package/presets/cursor/java/agents/tech-writer.md +4 -0
  236. package/presets/cursor/java/commands/README.md +0 -2
  237. package/presets/cursor/java/commands/task-continue.md +1 -1
  238. package/presets/cursor/java/commands/task.md +1 -4
  239. package/presets/cursor/java/commands/technical-retro.md +2 -2
  240. package/presets/cursor/java/hooks/chain-team-phases.sh +54 -3
  241. package/presets/cursor/java/rules/agent-team-orchestrator.mdc +2 -4
  242. package/presets/cursor/java/rules/ai-local-overlay.mdc +43 -0
  243. package/presets/cursor/java/rules/preset-pr-checklist.mdc +5 -1
  244. package/presets/cursor/java/rules/preset-token-budget.mdc +7 -3
  245. package/presets/cursor/java/rules/preset-twin-sync.mdc +2 -0
  246. package/presets/cursor/mcp-ts/agents/task-analyst.md +1 -1
  247. package/presets/cursor/mcp-ts/commands/README.md +0 -2
  248. package/presets/cursor/mcp-ts/commands/task-continue.md +1 -1
  249. package/presets/cursor/mcp-ts/commands/task.md +1 -4
  250. package/presets/cursor/mcp-ts/commands/technical-retro.md +2 -2
  251. package/presets/cursor/mcp-ts/hooks/chain-team-phases.sh +40 -9
  252. package/presets/cursor/mcp-ts/rules/agent-team-orchestrator.mdc +2 -4
  253. package/presets/cursor/mcp-ts/rules/ai-local-overlay.mdc +43 -0
  254. package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +5 -1
  255. package/presets/cursor/mcp-ts/rules/preset-token-budget.mdc +7 -3
  256. package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +2 -0
  257. package/presets/cursor/next/README.md +8 -6
  258. package/presets/cursor/next/REPO_AGENTS.md +8 -0
  259. package/presets/cursor/next/agents/README.md +2 -2
  260. package/presets/cursor/next/agents/build-verifier.md +11 -1
  261. package/presets/cursor/next/agents/feature-developer.md +1 -1
  262. package/presets/cursor/next/agents/openspec-author.md +164 -0
  263. package/presets/cursor/next/agents/task-analyst.md +1 -1
  264. package/presets/cursor/next/agents/task-router.md +5 -1
  265. package/presets/cursor/next/agents/tech-writer.md +4 -0
  266. package/presets/cursor/next/commands/README.md +0 -2
  267. package/presets/cursor/next/commands/task-continue.md +1 -1
  268. package/presets/cursor/next/commands/task.md +1 -4
  269. package/presets/cursor/next/commands/technical-retro.md +2 -2
  270. package/presets/cursor/next/hooks/chain-team-phases.sh +54 -2
  271. package/presets/cursor/next/rules/README.md +6 -3
  272. package/presets/cursor/next/rules/agent-team-orchestrator.mdc +7 -5
  273. package/presets/cursor/next/rules/ai-local-overlay.mdc +43 -0
  274. package/presets/cursor/next/rules/preset-pr-checklist.mdc +5 -1
  275. package/presets/cursor/next/rules/preset-token-budget.mdc +7 -3
  276. package/presets/cursor/next/rules/preset-twin-sync.mdc +2 -0
  277. package/presets/cursor/next/rules/tests-e2e-structure.mdc +83 -43
  278. package/presets/cursor/next/rules/tests-unit.mdc +78 -45
  279. package/presets/cursor/next/team/README.md +0 -3
  280. package/presets/cursor/nuxt/README.md +6 -6
  281. package/presets/cursor/nuxt/agents/README.md +1 -1
  282. package/presets/cursor/nuxt/agents/feature-developer.md +1 -1
  283. package/presets/cursor/nuxt/agents/openspec-author.md +164 -0
  284. package/presets/cursor/nuxt/agents/task-analyst.md +1 -1
  285. package/presets/cursor/nuxt/agents/task-router.md +5 -1
  286. package/presets/cursor/nuxt/agents/tech-writer.md +4 -0
  287. package/presets/cursor/nuxt/commands/README.md +0 -2
  288. package/presets/cursor/nuxt/commands/task-continue.md +1 -1
  289. package/presets/cursor/nuxt/commands/task.md +1 -5
  290. package/presets/cursor/nuxt/commands/technical-retro.md +2 -2
  291. package/presets/cursor/nuxt/hooks/chain-team-phases.sh +54 -2
  292. package/presets/cursor/nuxt/rules/README.md +3 -1
  293. package/presets/cursor/nuxt/rules/agent-team-orchestrator.mdc +8 -5
  294. package/presets/cursor/nuxt/rules/ai-local-overlay.mdc +43 -0
  295. package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +5 -1
  296. package/presets/cursor/nuxt/rules/preset-token-budget.mdc +7 -3
  297. package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +2 -0
  298. package/presets/cursor/nuxt/rules/tests-e2e-structure.mdc +87 -43
  299. package/presets/cursor/nuxt/rules/tests-unit.mdc +62 -47
  300. package/presets/cursor/nuxt/team/README.md +0 -3
  301. package/presets/cursor/php-hexagonal/AGENTS.md +1 -1
  302. package/presets/cursor/php-hexagonal/agents/README.md +3 -2
  303. package/presets/cursor/php-hexagonal/agents/openspec-author.md +164 -0
  304. package/presets/cursor/php-hexagonal/agents/task-router.md +5 -1
  305. package/presets/cursor/php-hexagonal/agents/tech-writer.md +4 -0
  306. package/presets/cursor/php-hexagonal/commands/README.md +0 -2
  307. package/presets/cursor/php-hexagonal/commands/task-continue.md +1 -1
  308. package/presets/cursor/php-hexagonal/commands/task.md +1 -4
  309. package/presets/cursor/php-hexagonal/commands/technical-retro.md +2 -2
  310. package/presets/cursor/php-hexagonal/hooks/chain-team-phases.sh +54 -3
  311. package/presets/cursor/php-hexagonal/rules/agent-team-orchestrator.mdc +2 -4
  312. package/presets/cursor/php-hexagonal/rules/ai-local-overlay.mdc +43 -0
  313. package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +5 -1
  314. package/presets/cursor/php-hexagonal/rules/preset-token-budget.mdc +7 -3
  315. package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +2 -0
  316. package/presets/cursor/php-laravel/AGENTS.md +1 -1
  317. package/presets/cursor/php-laravel/agents/README.md +3 -2
  318. package/presets/cursor/php-laravel/agents/openspec-author.md +164 -0
  319. package/presets/cursor/php-laravel/agents/task-router.md +5 -1
  320. package/presets/cursor/php-laravel/agents/tech-writer.md +4 -0
  321. package/presets/cursor/php-laravel/commands/README.md +0 -2
  322. package/presets/cursor/php-laravel/commands/task-continue.md +1 -1
  323. package/presets/cursor/php-laravel/commands/task.md +1 -4
  324. package/presets/cursor/php-laravel/commands/technical-retro.md +2 -2
  325. package/presets/cursor/php-laravel/hooks/chain-team-phases.sh +54 -3
  326. package/presets/cursor/php-laravel/rules/agent-team-orchestrator.mdc +2 -4
  327. package/presets/cursor/php-laravel/rules/ai-local-overlay.mdc +43 -0
  328. package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +5 -1
  329. package/presets/cursor/php-laravel/rules/preset-token-budget.mdc +7 -3
  330. package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +2 -0
  331. package/presets/cursor/svelte/README.md +6 -6
  332. package/presets/cursor/svelte/agents/README.md +1 -1
  333. package/presets/cursor/svelte/agents/feature-developer.md +1 -1
  334. package/presets/cursor/svelte/agents/openspec-author.md +164 -0
  335. package/presets/cursor/svelte/agents/task-analyst.md +1 -1
  336. package/presets/cursor/svelte/agents/task-router.md +5 -1
  337. package/presets/cursor/svelte/agents/tech-writer.md +4 -0
  338. package/presets/cursor/svelte/commands/README.md +0 -2
  339. package/presets/cursor/svelte/commands/task-continue.md +1 -1
  340. package/presets/cursor/svelte/commands/task.md +1 -5
  341. package/presets/cursor/svelte/commands/technical-retro.md +2 -2
  342. package/presets/cursor/svelte/hooks/chain-team-phases.sh +54 -2
  343. package/presets/cursor/svelte/rules/README.md +3 -1
  344. package/presets/cursor/svelte/rules/agent-team-orchestrator.mdc +8 -5
  345. package/presets/cursor/svelte/rules/ai-local-overlay.mdc +43 -0
  346. package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +5 -1
  347. package/presets/cursor/svelte/rules/preset-token-budget.mdc +7 -3
  348. package/presets/cursor/svelte/rules/preset-twin-sync.mdc +2 -0
  349. package/presets/cursor/svelte/rules/tests-e2e-structure.mdc +87 -43
  350. package/presets/cursor/svelte/rules/tests-unit.mdc +72 -23
  351. package/presets/cursor/svelte/team/README.md +0 -3
  352. package/scripts/_lib/embed-stems.json +31 -0
  353. package/scripts/aggregate-agent-task-metrics.mjs +23 -0
  354. package/scripts/check-chain-hook-coverage.sh +109 -0
  355. package/scripts/check-local-overlay.sh +136 -0
  356. package/scripts/check-preset-i18n.sh +2 -2
  357. package/scripts/check-preset-leakage.sh +4 -1
  358. package/scripts/check-preset-structure.sh +21 -1
  359. package/scripts/check-preset-token-budget.sh +20 -10
  360. package/scripts/check-rule-eval.mjs +16 -0
  361. package/scripts/check-shared-core-drift.sh +10 -32
  362. package/scripts/close-agent-task-human-gate.mjs +22 -5
  363. package/scripts/sync-shared-core-embeds.sh +9 -20
  364. package/scripts/test-agent-task-metrics-hooks.mjs +1 -1
  365. package/scripts/test-chain-team-phases-coverage.mjs +411 -0
  366. package/presets/claude/android-kotlin/commands/feature-continue.md +0 -23
  367. package/presets/claude/android-kotlin/commands/feature-start.md +0 -41
  368. package/presets/claude/go/commands/feature-continue.md +0 -23
  369. package/presets/claude/go/commands/feature-start.md +0 -38
  370. package/presets/claude/ios-swift/commands/feature-continue.md +0 -23
  371. package/presets/claude/ios-swift/commands/feature-start.md +0 -41
  372. package/presets/claude/java/commands/feature-continue.md +0 -23
  373. package/presets/claude/java/commands/feature-start.md +0 -38
  374. package/presets/claude/mcp-ts/commands/feature-continue.md +0 -23
  375. package/presets/claude/mcp-ts/commands/feature-start.md +0 -41
  376. package/presets/claude/next/commands/feature-continue.md +0 -23
  377. package/presets/claude/next/commands/feature-start.md +0 -38
  378. package/presets/claude/nuxt/commands/feature-continue.md +0 -23
  379. package/presets/claude/nuxt/commands/feature-start.md +0 -41
  380. package/presets/claude/php-hexagonal/commands/feature-continue.md +0 -23
  381. package/presets/claude/php-hexagonal/commands/feature-start.md +0 -38
  382. package/presets/claude/php-laravel/commands/feature-continue.md +0 -23
  383. package/presets/claude/php-laravel/commands/feature-start.md +0 -38
  384. package/presets/claude/svelte/commands/feature-continue.md +0 -23
  385. package/presets/claude/svelte/commands/feature-start.md +0 -41
  386. package/presets/cursor/android-kotlin/commands/feature-continue.md +0 -23
  387. package/presets/cursor/android-kotlin/commands/feature-start.md +0 -41
  388. package/presets/cursor/go/commands/feature-continue.md +0 -23
  389. package/presets/cursor/go/commands/feature-start.md +0 -41
  390. package/presets/cursor/ios-swift/commands/feature-continue.md +0 -23
  391. package/presets/cursor/ios-swift/commands/feature-start.md +0 -41
  392. package/presets/cursor/java/commands/feature-continue.md +0 -23
  393. package/presets/cursor/java/commands/feature-start.md +0 -41
  394. package/presets/cursor/mcp-ts/commands/feature-continue.md +0 -23
  395. package/presets/cursor/mcp-ts/commands/feature-start.md +0 -41
  396. package/presets/cursor/next/commands/feature-continue.md +0 -23
  397. package/presets/cursor/next/commands/feature-start.md +0 -41
  398. package/presets/cursor/nuxt/commands/feature-continue.md +0 -23
  399. package/presets/cursor/nuxt/commands/feature-start.md +0 -41
  400. package/presets/cursor/php-hexagonal/commands/feature-continue.md +0 -23
  401. package/presets/cursor/php-hexagonal/commands/feature-start.md +0 -41
  402. package/presets/cursor/php-laravel/commands/feature-continue.md +0 -23
  403. package/presets/cursor/php-laravel/commands/feature-start.md +0 -41
  404. package/presets/cursor/svelte/commands/feature-continue.md +0 -23
  405. package/presets/cursor/svelte/commands/feature-start.md +0 -41
@@ -16,7 +16,8 @@ from pathlib import Path
16
16
 
17
17
  presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
18
18
  STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
19
- BODY_CAP = 140
19
+ BODY_CAP = 140 # applied to 3-rule trios (Claude session-start, Cursor trio when len==3)
20
+ BODY_CAP_CURSOR_4 = 170 # Cursor now allows 4 always-on (next-app-core, package-manager, code-quality, ai-local-overlay)
20
21
  MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*[^>]+?\s*-->\s*$")
21
22
  ALWAYS_APPLY_RE = re.compile(r"(?m)^alwaysApply:\s*true\s*$")
22
23
  PATHS_RE = re.compile(r"(?m)^paths:\s*")
@@ -70,7 +71,7 @@ failed = 0
70
71
 
71
72
  print("Preset token / session-start budget (Option A)")
72
73
  print(f"Presets: {presets}")
73
- print(f"Body line cap (trio sum): {BODY_CAP}")
74
+ print(f"Body line cap (Claude trio): {BODY_CAP}; (Cursor 4-set): {BODY_CAP_CURSOR_4}")
74
75
  print()
75
76
 
76
77
  for stack in STACKS:
@@ -87,9 +88,15 @@ for stack in STACKS:
87
88
  print(f"FAIL: meta preset-* must not be alwaysApply: {p.name}")
88
89
  failed += 1
89
90
 
90
- if len(always) != 3:
91
- print(f"FAIL: Cursor alwaysApply count is {len(always)}, expected exactly 3")
91
+ if len(always) not in (3, 4):
92
+ print(f"FAIL: Cursor alwaysApply count is {len(always)}, expected 3 or 4 (4th slot reserved for ai-local-overlay)")
92
93
  failed += 1
94
+ elif len(always) == 4:
95
+ # 4-set: ensure the 4th rule is ai-local-overlay (or declares itself as such).
96
+ extra = [p for p in always if p.stem not in {"code-quality-and-refactoring", "package-manager", "next-app-core", "nuxt-app-core", "svelte-app-core", "ios-app-core", "android-app-core", "go-app-core", "java-app-core", "mcp-ts-app-core", "php-app-core", "laravel-app-core", "architecture-boundaries", "mcp-server-boundaries"}]
97
+ if not any(p.stem == "ai-local-overlay" for p in always) and extra:
98
+ print(f"FAIL: Cursor alwaysApply=4 but no ai-local-overlay.mdc; the 4th slot is reserved for the local overlay rule")
99
+ failed += 1
93
100
 
94
101
  # --- Claude no-paths (excl README) ---
95
102
  session = claude_session_start(claude_rules)
@@ -108,10 +115,12 @@ for stack in STACKS:
108
115
  )
109
116
  failed += 1
110
117
 
111
- # --- Body sum of intended trio (Cursor alwaysApply + Claude session) ---
112
- # Prefer Cursor trio for budget; also check Claude trio independently.
118
+ # --- Body sum of intended session-start set (Cursor alwaysApply + Claude session) ---
119
+ # Cursor allows 3 or 4 rules (4th slot reserved for ai-local-overlay); Claude is exactly 3.
113
120
  for label, files in (("Cursor", always), ("Claude", session)):
114
- if len(files) != 3:
121
+ if label == "Cursor" and len(files) not in (3, 4):
122
+ continue
123
+ if label == "Claude" and len(files) != 3:
115
124
  continue
116
125
  total = 0
117
126
  for path in files:
@@ -121,10 +130,11 @@ for stack in STACKS:
121
130
  total += n
122
131
  name = path.name if label == "Cursor" else path.relative_to(claude_rules).as_posix()
123
132
  print(f" {label} body lines {name}: {n}")
124
- print(f" {label} trio body sum: {total}")
125
- if total > BODY_CAP:
133
+ cap = BODY_CAP_CURSOR_4 if label == "Cursor" and len(always) == 4 else BODY_CAP
134
+ print(f" {label} session-start body sum: {total} (cap {cap}, files={len(files)})")
135
+ if total > cap:
126
136
  print(
127
- f"FAIL: {label} session-start body sum {total} > {BODY_CAP} ({stack})"
137
+ f"FAIL: {label} session-start body sum {total} > {cap} ({stack})"
128
138
  )
129
139
  failed += 1
130
140
 
@@ -99,6 +99,22 @@ function checkFile(relPath, minPairs, requireImpact) {
99
99
  ) && ok;
100
100
  }
101
101
 
102
+ // Order check: each Incorrect heading must precede its matching Correct heading.
103
+ // Without this, an agent reading the rule may learn a "correct" pattern before
104
+ // seeing the "incorrect" example it contrasts with.
105
+ if (incorrectCount > 0 && correctCount > 0) {
106
+ const incRe = /^#{2,3}\s+Incorrect\s*$/gim;
107
+ const corRe = /^#{2,3}\s+Correct\s*$/gim;
108
+ const firstInc = text.search(incRe);
109
+ const firstCor = text.search(corRe);
110
+ if (firstInc >= 0 && firstCor >= 0 && firstInc >= firstCor) {
111
+ ok =
112
+ fail(
113
+ `${relPath}: first "Incorrect" heading (offset ${firstInc}) must precede first "Correct" heading (offset ${firstCor})`,
114
+ ) && ok;
115
+ }
116
+ }
117
+
102
118
  const incorrectBodies = sectionBodies(text, 'Incorrect');
103
119
  const correctBodies = sectionBodies(text, 'Correct');
104
120
  for (let i = 0; i < Math.min(incorrectBodies.length, correctBodies.length); i++) {
@@ -7,48 +7,26 @@ set -euo pipefail
7
7
  ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
8
8
  export AI_RULES_PRESETS_ROOT="$ROOT/packages/ai-rules/presets"
9
9
 
10
- python3 <<'PY'
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
+ EMBED_LIST_FILE="$SCRIPT_DIR/_lib/embed-stems.json"
12
+
13
+ python3 - "$EMBED_LIST_FILE" <<'PY'
11
14
  from __future__ import annotations
12
15
 
16
+ import json
13
17
  import os
14
18
  import re
15
19
  import sys
16
20
  from pathlib import Path
17
21
 
22
+ embed_list_file = Path(sys.argv[1])
23
+ _stems = json.loads(embed_list_file.read_text(encoding="utf-8"))
24
+ EMBED = _stems["embed"]
25
+ LINEAGE = _stems["lineage"]
26
+
18
27
  presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
19
28
  core_root = presets / "_shared" / "core"
20
29
 
21
- EMBED = [
22
- "quality/code-quality-and-refactoring.md",
23
- "quality/anti-sycophancy-discipline.md",
24
- "quality/design-guidance.md",
25
- "quality/mcp-usage.md",
26
- "meta/preset-layering.md",
27
- "meta/preset-twin-sync.md",
28
- "meta/preset-token-budget.md",
29
- "meta/preset-pr-checklist.md",
30
- "meta/preset-no-cross-stack-leakage.md",
31
- "review/code-review-mr.md",
32
- "review/technical-retro.md",
33
- "review/mockup-review.md",
34
- "review/ux-design.md",
35
- "agent-team/agent-team-intake.md",
36
- "architecture/product-specs.md",
37
- "architecture/product-specs-authoring.md",
38
- # Whole-file embed: skill debug-investigation = core + ## Stack notes only.
39
- "quality/debug-investigation.md",
40
- ]
41
-
42
- LINEAGE = [
43
- "architecture/feature-delivery-workflow.md",
44
- "architecture/reference-features.template.md",
45
- "agent-team/agent-team-orchestrator.md",
46
- # Section snippets in fat agents/skills — body is not the whole file.
47
- "quality/verification-before-completion.md",
48
- "review/receiving-code-review.md",
49
- "agent-team/task-classification.md",
50
- ]
51
-
52
30
  # Full-line sync marker only (do not strip prose that *mentions* the marker).
53
31
  MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*[^>]+?\s*-->\s*$")
54
32
 
@@ -13,18 +13,35 @@ import path from 'node:path';
13
13
 
14
14
  function parseArgs(argv) {
15
15
  const options = { cwd: process.cwd(), platform: 'cursor', slug: null };
16
+ // Read the next token from argv for value-taking flags. Returns the value or null
17
+ // when the flag is missing its value; the caller exits with a friendly error.
18
+ function nextValue(flag, i) {
19
+ const next = argv[i + 1];
20
+ if (next === undefined || next.startsWith('-')) {
21
+ console.error(`Missing value for ${flag} (use ${flag} <value>)`);
22
+ process.exit(2);
23
+ }
24
+ return next;
25
+ }
16
26
  for (let i = 2; i < argv.length; i += 1) {
17
27
  const arg = argv[i];
18
- if (arg === '--cwd') options.cwd = path.resolve(argv[++i]);
19
- else if (arg === '--platform') options.platform = argv[++i];
20
- else if (arg === '--slug') options.slug = argv[++i];
21
- else if (arg === '--activate-next') {
28
+ if (arg === '--cwd') {
29
+ options.cwd = path.resolve(nextValue(arg, i));
30
+ i += 1;
31
+ } else if (arg === '--platform') {
32
+ options.platform = nextValue(arg, i);
33
+ i += 1;
34
+ } else if (arg === '--slug') {
35
+ options.slug = nextValue(arg, i);
36
+ i += 1;
37
+ } else if (arg === '--activate-next') {
22
38
  /* always activate via hook payload */
23
39
  } else if (arg === '--team-root') {
24
40
  // Compatibility with earlier docs: derive platform + cwd from team root.
25
- const teamRoot = path.resolve(argv[++i]);
41
+ const teamRoot = path.resolve(nextValue(arg, i));
26
42
  options.cwd = path.resolve(teamRoot, '..', '..');
27
43
  options.platform = path.basename(path.dirname(teamRoot)) === '.claude' ? 'claude' : 'cursor';
44
+ i += 1;
28
45
  } else {
29
46
  console.error(`Unknown argument: ${arg}`);
30
47
  process.exit(2);
@@ -8,35 +8,24 @@ set -euo pipefail
8
8
  ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
9
9
  export AI_RULES_PRESETS_ROOT="$ROOT/packages/ai-rules/presets"
10
10
 
11
- python3 <<'PY'
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ EMBED_LIST_FILE="$SCRIPT_DIR/_lib/embed-stems.json"
13
+
14
+ python3 - "$EMBED_LIST_FILE" <<'PY'
12
15
  from __future__ import annotations
13
16
 
17
+ import json
14
18
  import os
15
19
  import re
20
+ import sys
16
21
  from pathlib import Path
17
22
 
23
+ embed_list_file = Path(sys.argv[1])
24
+ EMBED = json.loads(embed_list_file.read_text(encoding="utf-8"))["embed"]
25
+
18
26
  presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
19
27
  core_root = presets / "_shared" / "core"
20
28
 
21
- EMBED = [
22
- "quality/code-quality-and-refactoring.md",
23
- "quality/anti-sycophancy-discipline.md",
24
- "quality/design-guidance.md",
25
- "quality/mcp-usage.md",
26
- "meta/preset-layering.md",
27
- "meta/preset-twin-sync.md",
28
- "meta/preset-token-budget.md",
29
- "meta/preset-pr-checklist.md",
30
- "meta/preset-no-cross-stack-leakage.md",
31
- "review/code-review-mr.md",
32
- "review/technical-retro.md",
33
- "review/mockup-review.md",
34
- "review/ux-design.md",
35
- "agent-team/agent-team-intake.md",
36
- "architecture/product-specs.md",
37
- "architecture/product-specs-authoring.md",
38
- ]
39
-
40
29
  MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*([^>]+?)\s*-->\s*$")
41
30
 
42
31
 
@@ -14,7 +14,7 @@ const hooks = [
14
14
  ...fs.readdirSync(path.join(presetsDirectory, 'claude')).map((stack) => path.join(presetsDirectory, 'claude', stack, 'hooks', 'chain-team-phases.sh')),
15
15
  ];
16
16
 
17
- assert.equal(hooks.length, 14, 'expected every packaged Cursor and Claude hook');
17
+ assert.equal(hooks.length, 20, 'expected every packaged Cursor and Claude hook (10 stacks × 2 platforms)');
18
18
 
19
19
  function readJson(filePath) {
20
20
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+ // Targeted tests for chain-team-phases.sh fixes that prevent `/task` pipelines
3
+ // from silently skipping or stalling on modern roles.
4
+ //
5
+ // Coverage:
6
+ // 1. openspec-author handoff path: the AGENT_HANDOFF['openspec-author']
7
+ // entry must be emitted so the orchestrator invokes the agent; the
8
+ // DEFAULT_ARTIFACT_OUTPUT_IDS['openspec-author'] entry must match the
9
+ // manifest's `openspec-change` receipt so the hook advances instead of
10
+ // stalling in `awaiting_artifact_receipt`.
11
+ // 2. QA / unit / playwright / instrumentation / integration / xcuitest
12
+ // agents must have both AGENT_HANDOFF and DEFAULT_ARTIFACT_OUTPUT_IDS
13
+ // entries (the symmetric check is in check-chain-hook-coverage.sh; here
14
+ // we drive a full pipeline step to make sure receipts reconcile).
15
+ // 3. Pipeline.json parse error: a malformed pipeline.json that still
16
+ // carries an `intent` field must NOT fall through to legacyFlow; the
17
+ // hook must write `state: blocked` and emit a followup_message that
18
+ // mentions `pipeline.json` so the orchestrator surfaces the error.
19
+ // 4. Legacy flow fallback: a missing pipeline.json must still invoke
20
+ // legacyFlow() (analysis → development → review → testing chain).
21
+ // 5. writeStatus durability: status.json must be durable (fsync) before the
22
+ // hook returns. We assert that a second reader immediately after the
23
+ // hook returns sees the new state.
24
+
25
+ import assert from 'node:assert/strict';
26
+ import { execFileSync, spawn } from 'node:child_process';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
32
+ const packageDirectory = path.resolve(scriptDirectory, '..');
33
+ const presetsDirectory = path.join(packageDirectory, 'presets');
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
+ ];
39
+
40
+ assert.equal(hooks.length, 20, `expected 20 packaged hooks (10 stacks × 2 platforms), got ${hooks.length}`);
41
+
42
+ // mcp-ts does not ship openspec-author (per preset-no-cross-stack-leakage).
43
+ 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');
45
+
46
+ function readJson(filePath) {
47
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
48
+ }
49
+
50
+ function writeJson(filePath, value) {
51
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
52
+ }
53
+
54
+ function invokeHook(hook, workingDirectory, attemptId, payload = {}) {
55
+ return execFileSync('bash', [hook], {
56
+ cwd: workingDirectory,
57
+ encoding: 'utf8',
58
+ input: JSON.stringify({ status: 'completed', attemptId, ...payload }),
59
+ });
60
+ }
61
+
62
+ function platformFor(hook) {
63
+ return hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
64
+ }
65
+
66
+ function writeTask(root, hook, slug, pipeline, status, metrics, artifacts = {}) {
67
+ const platform = platformFor(hook);
68
+ const teamRoot = path.join(root, `.${platform}`, 'team');
69
+ const taskDirectory = path.join(teamRoot, 'tasks', slug);
70
+ fs.mkdirSync(taskDirectory, { recursive: true });
71
+ writeJson(path.join(teamRoot, 'active-task.json'), { slug });
72
+ writeJson(path.join(taskDirectory, 'pipeline.json'), pipeline);
73
+ writeJson(path.join(taskDirectory, 'status.json'), status);
74
+ writeJson(path.join(taskDirectory, 'metrics.json'), metrics);
75
+ for (const [name, content] of Object.entries(artifacts)) {
76
+ fs.writeFileSync(path.join(taskDirectory, name), content);
77
+ }
78
+ return { taskDirectory, platform };
79
+ }
80
+
81
+ const todayIso = '2026-08-21T00:00:00.000Z';
82
+ const earlierIso = '2026-08-21T00:00:01.000Z';
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Test 1 — openspec-author handoff: the hook must emit a followup_message that
86
+ // references openspec-author and the openspec-change scope, and the default
87
+ // artifact id must match the manifest receipt for `state` to advance.
88
+ // ---------------------------------------------------------------------------
89
+ for (const hook of HOOKS_WITH_OPENSPEC) {
90
+ const root = fs.mkdtempSync(path.join(packageDirectory, '.openspec-handoff-'));
91
+ try {
92
+ const runId = 'openspec-handoff-run';
93
+ const platform = platformFor(hook);
94
+ const analystAttemptId = `${runId}:step-0:task-analyst:attempt-1`;
95
+ const { taskDirectory } = writeTask(
96
+ root,
97
+ hook,
98
+ 'openspec-handoff',
99
+ {
100
+ intent: 'openspec-change',
101
+ profile: 'standard',
102
+ steps: [
103
+ { agent: 'task-analyst', label: 'Analyze' },
104
+ { agent: 'openspec-author', label: 'Author spec change', scope: 'openspec:MEDCARD-11762' },
105
+ ],
106
+ humanGates: [],
107
+ },
108
+ {
109
+ slug: 'openspec-handoff',
110
+ intent: 'openspec-change',
111
+ pipelineIndex: 0,
112
+ currentAgent: 'task-analyst',
113
+ phase: 'executing',
114
+ state: 'completed',
115
+ activeAttempts: {
116
+ [analystAttemptId]: {
117
+ agent: 'task-analyst',
118
+ stepIndex: 0,
119
+ attempt: 1,
120
+ startedAt: todayIso,
121
+ parallelGroupId: null,
122
+ },
123
+ },
124
+ metricsRunId: runId,
125
+ },
126
+ {
127
+ schemaVersion: 1,
128
+ task: { runId, slug: 'openspec-handoff', preset: 'ai-rules', stack: 'test', platform, intent: 'openspec-change', profile: 'standard' },
129
+ events: [
130
+ { eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
131
+ { eventId: `${runId}:analyst-start`, type: 'attempt_started', attemptId: analystAttemptId, agent: 'task-analyst', stepIndex: 0, attempt: 1, at: todayIso },
132
+ ],
133
+ },
134
+ );
135
+
136
+ const handoffRaw = invokeHook(hook, root, analystAttemptId);
137
+ const handoff = JSON.parse(handoffRaw).followup_message;
138
+ assert.match(handoff, /openspec-author/, `${hook}: handoff did not mention openspec-author`);
139
+ assert.match(handoff, /openspec:MEDCARD-11762/, `${hook}: handoff did not echo the openspec scope`);
140
+
141
+ const statusAfter = readJson(path.join(taskDirectory, 'status.json'));
142
+ assert.equal(statusAfter.pipelineIndex, 1, `${hook}: hook did not advance past the analyst step`);
143
+ assert.equal(statusAfter.currentAgent, 'openspec-author', `${hook}: next agent was not openspec-author`);
144
+ const activeAttempts = Object.values(statusAfter.activeAttempts || {});
145
+ assert.ok(activeAttempts.some((entry) => entry.agent === 'openspec-author'), `${hook}: openspec-author attempt was not activated`);
146
+
147
+ // Now finish the openspec-author attempt with a properly bound receipt
148
+ // (id: openspec-change) and assert the hook advances to the next step.
149
+ const openspecAttempt = Object.entries(statusAfter.activeAttempts).find(([, entry]) => entry.agent === 'openspec-author');
150
+ const openspecAttemptId = openspecAttempt[0];
151
+ // Use a timestamp after the attempt's actual startedAt — the hook filters
152
+ // out manifest receipts whose completedAt predates the attempt's start.
153
+ const freshIso = new Date().toISOString();
154
+ fs.writeFileSync(
155
+ path.join(taskDirectory, 'artifact-manifest.json'),
156
+ JSON.stringify(
157
+ {
158
+ schemaVersion: 1,
159
+ slug: 'openspec-handoff',
160
+ updatedAt: freshIso,
161
+ artifacts: [
162
+ {
163
+ id: 'openspec-change',
164
+ producer: 'openspec-author',
165
+ status: 'completed',
166
+ authoritativePaths: ['openspec/changes/MEDCARD-11762/proposal.md'],
167
+ acceptanceCriteriaIds: ['AC-1'],
168
+ taskIds: ['T1'],
169
+ timestamps: { createdAt: freshIso, updatedAt: freshIso, completedAt: freshIso },
170
+ receipt: {
171
+ outcome: 'completed',
172
+ attemptId: openspecAttemptId,
173
+ summary: 'Proposal + design + tasks emitted; openspec validate --strict PASS',
174
+ evidencePaths: ['openspec/changes/MEDCARD-11762/proposal.md'],
175
+ },
176
+ },
177
+ ],
178
+ },
179
+ null,
180
+ 2,
181
+ ) + '\n',
182
+ );
183
+ writeJson(path.join(taskDirectory, 'status.json'), { ...statusAfter, state: 'completed' });
184
+ const finishRaw = invokeHook(hook, root, openspecAttemptId);
185
+ const finishHandoff = JSON.parse(finishRaw).followup_message;
186
+ assert.match(finishHandoff, /Pipeline complete/, `${hook}: pipeline should complete after openspec-author finishes`);
187
+ const finalStatus = readJson(path.join(taskDirectory, 'status.json'));
188
+ assert.equal(finalStatus.phase, 'done', `${hook}: phase did not transition to done`);
189
+ assert.equal(finalStatus.state, 'completed', `${hook}: state did not reach completed after openspec-author`);
190
+ } finally {
191
+ fs.rmSync(root, { recursive: true, force: true });
192
+ }
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Test 2 — qa-tester + unit-test-* handoff: a QA step in a pipeline must
197
+ // advance. Before the fix, DEFAULT_ARTIFACT_OUTPUT_IDS lacked the QA entries
198
+ // so `artifactOutputIdFor` returned the agent name itself; manifest receipts
199
+ // landed under the right id (qa / unit-test-spec) and the hook stalled in
200
+ // awaiting_artifact_receipt.
201
+ // ---------------------------------------------------------------------------
202
+ for (const hook of hooks) {
203
+ const root = fs.mkdtempSync(path.join(packageDirectory, '.qa-tester-handoff-'));
204
+ try {
205
+ const platform = platformFor(hook);
206
+ const runId = 'qa-tester-run';
207
+ const devAttemptId = `${runId}:step-0:feature-developer:attempt-1`;
208
+ const { taskDirectory } = writeTask(
209
+ root,
210
+ hook,
211
+ 'qa-tester-handoff',
212
+ {
213
+ intent: 'test-only',
214
+ profile: 'standard',
215
+ steps: [
216
+ { agent: 'feature-developer', label: 'Implement' },
217
+ { agent: 'qa-tester', label: 'QA' },
218
+ ],
219
+ humanGates: [],
220
+ },
221
+ {
222
+ slug: 'qa-tester-handoff',
223
+ intent: 'test-only',
224
+ pipelineIndex: 0,
225
+ currentAgent: 'feature-developer',
226
+ phase: 'executing',
227
+ state: 'completed',
228
+ activeAttempts: {
229
+ [devAttemptId]: {
230
+ agent: 'feature-developer',
231
+ stepIndex: 0,
232
+ attempt: 1,
233
+ startedAt: todayIso,
234
+ parallelGroupId: null,
235
+ },
236
+ },
237
+ metricsRunId: runId,
238
+ },
239
+ {
240
+ schemaVersion: 1,
241
+ task: { runId, slug: 'qa-tester-handoff', preset: 'ai-rules', stack: 'test', platform, intent: 'test-only', profile: 'standard' },
242
+ events: [
243
+ { eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
244
+ { eventId: `${runId}:dev-start`, type: 'attempt_started', attemptId: devAttemptId, agent: 'feature-developer', stepIndex: 0, attempt: 1, at: todayIso },
245
+ ],
246
+ },
247
+ );
248
+ const handoff = JSON.parse(invokeHook(hook, root, devAttemptId)).followup_message;
249
+ assert.match(handoff, /qa-tester/, `${hook}: handoff did not mention qa-tester`);
250
+ const statusAfter = readJson(path.join(taskDirectory, 'status.json'));
251
+ assert.equal(statusAfter.pipelineIndex, 1, `${hook}: hook did not advance to qa-tester step`);
252
+ assert.equal(statusAfter.currentAgent, 'qa-tester', `${hook}: currentAgent was not qa-tester`);
253
+ } finally {
254
+ fs.rmSync(root, { recursive: true, force: true });
255
+ }
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Test 3 — pipeline.json parse error with intent: the hook must surface the
260
+ // error and write state: blocked; legacyFlow must NOT be silently used.
261
+ // ---------------------------------------------------------------------------
262
+ for (const hook of hooks) {
263
+ const root = fs.mkdtempSync(path.join(packageDirectory, '.pipeline-misparse-'));
264
+ try {
265
+ const platform = platformFor(hook);
266
+ const runId = 'misparse-run';
267
+ const devAttemptId = `${runId}:step-0:feature-developer:attempt-1`;
268
+ const taskDirectory = path.join(root, `.${platform}`, 'team', 'tasks', 'misparse');
269
+ fs.mkdirSync(taskDirectory, { recursive: true });
270
+ fs.writeFileSync(path.join(root, `.${platform}`, 'team', 'active-task.json'), JSON.stringify({ slug: 'misparse' }));
271
+ // Malformed pipeline.json with `intent` field — must NOT fall back to legacy.
272
+ fs.writeFileSync(
273
+ path.join(taskDirectory, 'pipeline.json'),
274
+ '{ "intent": "openspec-change", "steps": [\n { "agent": "openspec-author" }\n', // truncated JSON
275
+ );
276
+ writeJson(path.join(taskDirectory, 'status.json'), {
277
+ slug: 'misparse',
278
+ intent: 'openspec-change',
279
+ pipelineIndex: 0,
280
+ currentAgent: 'feature-developer',
281
+ phase: 'executing',
282
+ state: 'completed',
283
+ activeAttempts: {
284
+ [devAttemptId]: {
285
+ agent: 'feature-developer',
286
+ stepIndex: 0,
287
+ attempt: 1,
288
+ startedAt: todayIso,
289
+ parallelGroupId: null,
290
+ },
291
+ },
292
+ metricsRunId: runId,
293
+ });
294
+ writeJson(path.join(taskDirectory, 'metrics.json'), {
295
+ schemaVersion: 1,
296
+ task: { runId, slug: 'misparse', preset: 'ai-rules', stack: 'test', platform, intent: 'openspec-change', profile: 'standard' },
297
+ events: [
298
+ { eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
299
+ { eventId: `${runId}:start`, type: 'attempt_started', attemptId: devAttemptId, agent: 'feature-developer', stepIndex: 0, attempt: 1, at: todayIso },
300
+ ],
301
+ });
302
+
303
+ const raw = invokeHook(hook, root, devAttemptId);
304
+ const parsed = JSON.parse(raw);
305
+ const followup = parsed.followup_message || '';
306
+ assert.match(followup, /pipeline\.json is malformed/, `${hook}: hook did not surface parse error in followup_message (got: ${followup})`);
307
+ const status = readJson(path.join(taskDirectory, 'status.json'));
308
+ assert.equal(status.state, 'blocked', `${hook}: malformed pipeline with intent did not block the pipeline (got: ${status.state})`);
309
+ assert.equal(status.phase, 'blocked', `${hook}: phase did not transition to blocked`);
310
+ const metrics = readJson(path.join(taskDirectory, 'metrics.json'));
311
+ assert.ok(
312
+ metrics.events.some((event) => event.type === 'pipeline_parse_error'),
313
+ `${hook}: pipeline_parse_error metric event was not emitted`,
314
+ );
315
+ } finally {
316
+ fs.rmSync(root, { recursive: true, force: true });
317
+ }
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // Test 4 — legacyFlow fallback: when pipeline.json is absent, the hook must
322
+ // continue to use legacyFlow (analysis → development → review → testing).
323
+ // ---------------------------------------------------------------------------
324
+ for (const hook of hooks) {
325
+ const root = fs.mkdtempSync(path.join(packageDirectory, '.legacy-fallback-'));
326
+ try {
327
+ const platform = platformFor(hook);
328
+ const taskDirectory = path.join(root, `.${platform}`, 'team', 'tasks', 'legacy-slug');
329
+ fs.mkdirSync(taskDirectory, { recursive: true });
330
+ fs.writeFileSync(path.join(root, `.${platform}`, 'team', 'active-task.json'), JSON.stringify({ slug: 'legacy-slug' }));
331
+ // No pipeline.json — legacy flow triggers.
332
+ writeJson(path.join(taskDirectory, 'status.json'), {
333
+ slug: 'legacy-slug',
334
+ intent: 'feature',
335
+ phase: 'development',
336
+ state: 'completed',
337
+ currentAgent: 'feature-developer',
338
+ pipelineIndex: 0,
339
+ });
340
+ const raw = invokeHook(hook, root, null);
341
+ const parsed = JSON.parse(raw);
342
+ const followup = parsed.followup_message || '';
343
+ assert.match(followup, /build-verifier/, `${hook}: legacy fallback did not invoke build-verifier (got: ${followup})`);
344
+ } finally {
345
+ fs.rmSync(root, { recursive: true, force: true });
346
+ }
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Test 5 — writeStatus durability: after the hook returns, the new
351
+ // status.json must already be on disk and reflect the post-step state. This
352
+ // is the property that lets the orchestrator's next read see the human-gate
353
+ // transition (no stale-read race).
354
+ // ---------------------------------------------------------------------------
355
+ for (const hook of HOOKS_WITH_OPENSPEC) {
356
+ const root = fs.mkdtempSync(path.join(packageDirectory, '.write-status-durability-'));
357
+ try {
358
+ const runId = 'durability-run';
359
+ const platform = platformFor(hook);
360
+ const analystAttemptId = `${runId}:step-0:task-analyst:attempt-1`;
361
+ const { taskDirectory } = writeTask(
362
+ root,
363
+ hook,
364
+ 'durability-slug',
365
+ {
366
+ intent: 'feature',
367
+ profile: 'standard',
368
+ steps: [
369
+ { agent: 'task-analyst', label: 'Analyze' },
370
+ { agent: 'openspec-author', label: 'Spec change' },
371
+ ],
372
+ humanGates: ['after:task-analyst'],
373
+ },
374
+ {
375
+ slug: 'durability-slug',
376
+ intent: 'feature',
377
+ pipelineIndex: 0,
378
+ currentAgent: 'task-analyst',
379
+ phase: 'executing',
380
+ state: 'completed',
381
+ activeAttempts: {
382
+ [analystAttemptId]: {
383
+ agent: 'task-analyst',
384
+ stepIndex: 0,
385
+ attempt: 1,
386
+ startedAt: todayIso,
387
+ parallelGroupId: null,
388
+ },
389
+ },
390
+ metricsRunId: runId,
391
+ },
392
+ {
393
+ schemaVersion: 1,
394
+ task: { runId, slug: 'durability-slug', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'standard' },
395
+ events: [
396
+ { eventId: `${runId}:task_started`, type: 'task_started', at: todayIso },
397
+ { eventId: `${runId}:start`, type: 'attempt_started', attemptId: analystAttemptId, agent: 'task-analyst', stepIndex: 0, attempt: 1, at: todayIso },
398
+ ],
399
+ },
400
+ );
401
+ // Sync + invoke hook; immediately read status.json without fsync of our own.
402
+ invokeHook(hook, root, analystAttemptId);
403
+ const onDisk = JSON.parse(fs.readFileSync(path.join(taskDirectory, 'status.json'), 'utf8'));
404
+ assert.equal(onDisk.awaitingHumanGate, true, `${hook}: human gate was not durably written to status.json`);
405
+ assert.equal(onDisk.state, 'awaiting_approval', `${hook}: state did not durably transition to awaiting_approval`);
406
+ } finally {
407
+ fs.rmSync(root, { recursive: true, force: true });
408
+ }
409
+ }
410
+
411
+ console.log(`Verified openspec-author handoff, qa-tester handoff, pipeline misparse blocking, legacy fallback, and writeStatus durability in ${hooks.length} hooks.`);