@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
@@ -0,0 +1,845 @@
1
+ #!/usr/bin/env bash
2
+ # Chains agent team steps from pipeline.json after subagentStop.
3
+ # Falls back to legacy phase-based flow when pipeline.json is absent.
4
+
5
+ set -euo pipefail
6
+
7
+ input=$(cat)
8
+
9
+ INPUT="$input" node <<'NODE'
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const TEAM_ROOT = path.join(process.cwd(), '.mavis', 'team');
14
+
15
+ const inputRaw = process.env.INPUT || '';
16
+ let hookInput;
17
+ try {
18
+ hookInput = JSON.parse(inputRaw);
19
+ } catch {
20
+ process.stdout.write('{}');
21
+ process.exit(0);
22
+ }
23
+
24
+ if (hookInput.status !== 'completed') {
25
+ process.stdout.write('{}');
26
+ process.exit(0);
27
+ }
28
+
29
+ const activePath = path.join(TEAM_ROOT, 'active-task.json');
30
+
31
+ if (!fs.existsSync(activePath)) {
32
+ process.stdout.write('{}');
33
+ process.exit(0);
34
+ }
35
+
36
+ let active;
37
+ try {
38
+ active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
39
+ } catch {
40
+ process.stdout.write('{}');
41
+ process.exit(0);
42
+ }
43
+
44
+ const slug = active.slug;
45
+ if (!slug) {
46
+ process.stdout.write('{}');
47
+ process.exit(0);
48
+ }
49
+
50
+ const taskDir = path.join(TEAM_ROOT, 'tasks', slug);
51
+ const statusPath = path.join(taskDir, 'status.json');
52
+ const pipelinePath = path.join(taskDir, 'pipeline.json');
53
+ const METRICS_PATH = path.join(taskDir, 'metrics.json');
54
+ const METRICS_LOCK = `${METRICS_PATH}.lock`;
55
+
56
+ if (!fs.existsSync(statusPath)) {
57
+ process.stdout.write('{}');
58
+ process.exit(0);
59
+ }
60
+
61
+ let status;
62
+ const now = new Date().toISOString();
63
+ const STACK = 'next';
64
+ const PLATFORM = 'cursor';
65
+
66
+ function waitForMetricsLock() {
67
+ // OD-1: real advisory lock via create-exclusive (fs.openSync(_, 'wx')).
68
+ // The previous impl spun on Atomics.wait against an unrelated SharedArrayBuffer
69
+ // and never blocked on the lock file, so two concurrent subagentStop events
70
+ // could both acquire the lock and corrupt metrics.json. Retry EEXIST with a
71
+ // 10 ms sleep up to 200 attempts (2 s ceiling). On any other error, throw.
72
+ for (let attempt = 0; attempt < 200; attempt += 1) {
73
+ try {
74
+ const fd = fs.openSync(METRICS_LOCK, 'wx');
75
+ try { fs.closeSync(fd); } catch {}
76
+ return;
77
+ } catch (error) {
78
+ if (!error || error.code !== 'EEXIST') throw error;
79
+ const until = Date.now() + 10;
80
+ while (Date.now() < until) { /* 10 ms busy-wait */ }
81
+ }
82
+ }
83
+ throw new Error('metrics lock timeout');
84
+ }
85
+ let taskLockHeld = false;
86
+ function releaseTaskLock() {
87
+ if (!taskLockHeld) return;
88
+ taskLockHeld = false;
89
+ try { fs.unlinkSync(METRICS_LOCK); } catch {}
90
+ }
91
+ waitForMetricsLock();
92
+ taskLockHeld = true;
93
+ process.on('exit', releaseTaskLock);
94
+ try {
95
+ status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
96
+ } catch {
97
+ releaseTaskLock();
98
+ process.stdout.write('{}');
99
+ process.exit(0);
100
+ }
101
+ function mutateMetrics(mutator) {
102
+ const acquiredHere = !taskLockHeld;
103
+ if (acquiredHere) waitForMetricsLock();
104
+ try {
105
+ let metrics;
106
+ try { metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8')); } catch {
107
+ const runId = status.metricsRunId || `${slug}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
108
+ status.metricsRunId = runId;
109
+ metrics = { schemaVersion: 1, task: { runId, slug, preset: 'ai-rules', stack: STACK, platform: PLATFORM, intent: status.intent || 'unknown', profile: 'unknown' }, events: [] };
110
+ metrics.events.push({ eventId: `${runId}:task_started`, type: 'task_started', at: now, source: 'hook', attributionConfidence: 'high' });
111
+ }
112
+ mutator(metrics);
113
+ const temporary = `${METRICS_PATH}.${process.pid}.${Date.now()}.tmp`;
114
+ fs.writeFileSync(temporary, JSON.stringify(metrics, null, 2) + '\n');
115
+ fs.renameSync(temporary, METRICS_PATH);
116
+ } finally {
117
+ if (acquiredHere) { try { fs.unlinkSync(METRICS_LOCK); } catch {} }
118
+ }
119
+ }
120
+ function appendMetric(event) { mutateMetrics((metrics) => metrics.events.push({ eventId: `${metrics.task.runId}:${Date.now()}:${Math.random().toString(16).slice(2)}`, ...event })); }
121
+ function nextAttemptOrdinal(stepIndex, agent) {
122
+ try {
123
+ const metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
124
+ return metrics.events.reduce((highest, event) => event.type === 'attempt_started' && event.stepIndex === stepIndex && event.agent === agent ? Math.max(highest, Number(event.attempt) || 0) : highest, 0) + 1;
125
+ } catch { return 1; }
126
+ }
127
+ function roleFor(agent) {
128
+ if (/analyst|architect|router|migration/.test(agent || '')) return 'planning';
129
+ if (/verifier/.test(agent || '')) return 'validation';
130
+ if (/reviewer|auditor/.test(agent || '')) return 'review';
131
+ if (/test|qa/.test(agent || '')) return 'testing';
132
+ if (/writer/.test(agent || '')) return 'docs';
133
+ return 'implementation';
134
+ }
135
+ const DEFAULT_ARTIFACT_OUTPUT_IDS = {
136
+ 'task-router': 'routing',
137
+ 'task-analyst': 'analysis',
138
+ 'solution-architect': 'architecture',
139
+ 'migration-specialist': 'migration-plan',
140
+ debugger: 'debug',
141
+ 'ci-investigator': 'ci',
142
+ 'feature-developer': 'implementation',
143
+ 'build-verifier': 'validation',
144
+ 'code-reviewer': 'review',
145
+ 'security-reviewer': 'security-review',
146
+ 'accessibility-reviewer': 'a11y-review',
147
+ 'performance-auditor': 'perf-report',
148
+ 'mockup-reviewer': 'mockup-review',
149
+ 'api-contract-reviewer': 'api-contract-review',
150
+ 'tech-writer': 'documentation',
151
+ 'openspec-author': 'openspec-change',
152
+ 'qa-tester': 'qa',
153
+ 'unit-test-planner': 'unit-test-plan',
154
+ 'unit-test-generator': 'unit-test-spec',
155
+ 'unit-test-healer': 'unit-test-heal',
156
+ 'playwright-test-planner': 'e2e-plan',
157
+ 'playwright-test-generator': 'e2e-spec',
158
+ 'playwright-test-healer': 'e2e-heal',
159
+ };
160
+ function artifactOutputIdFor(step, agent) {
161
+ if (step && typeof step.artifactOutputId === 'string') return step.artifactOutputId;
162
+ if (step?.artifactOutputIds && typeof step.artifactOutputIds[agent] === 'string') return step.artifactOutputIds[agent];
163
+ return DEFAULT_ARTIFACT_OUTPUT_IDS[agent] || agent;
164
+ }
165
+ function activateAttempts(step, stepIndex) {
166
+ const active = { ...(status.activeAttempts || {}) }; const agents = getStepAgents(step);
167
+ const parallelGroupId = step.parallel && agents.length > 1 ? `${status.metricsRunId || slug}:step-${stepIndex}` : null;
168
+ for (const agent of agents) {
169
+ const attempt = nextAttemptOrdinal(stepIndex, agent); const runId = status.metricsRunId || `${slug}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
170
+ status.metricsRunId = runId; const attemptId = `${runId}:step-${stepIndex}:${agent}:attempt-${attempt}`;
171
+ if (active[attemptId]) continue;
172
+ active[attemptId] = { agent, stepIndex, attempt, startedAt: now, parallelGroupId };
173
+ appendMetric({ type: 'attempt_started', at: now, attemptId, stepIndex, stepLabel: step.label || null, agent, role: roleFor(agent), attempt, parallelGroupId, source: 'hook', attributionConfidence: 'high' });
174
+ }
175
+ status.activeAttempts = active;
176
+ }
177
+ function resolvedAttempt() {
178
+ const active = status.activeAttempts || {}; const entries = Object.entries(active);
179
+ const stableAttemptId = hookInput.attemptId;
180
+ if (stableAttemptId) {
181
+ return active[stableAttemptId]
182
+ ? { attemptId: stableAttemptId, entry: active[stableAttemptId], confidence: 'high' }
183
+ : null;
184
+ }
185
+ if (entries.length === 1) return { attemptId: entries[0][0], entry: entries[0][1], confidence: 'high' };
186
+ try { const receipts = JSON.parse(fs.readFileSync(path.join(taskDir, '.metrics-receipts.json'), 'utf8')); const receipt = Array.isArray(receipts) ? receipts.find((item) => active[item.attemptId]) : null; if (receipt) return { attemptId: receipt.attemptId, entry: active[receipt.attemptId], confidence: 'fallback', source: 'agent' }; } catch {}
187
+ return null;
188
+ }
189
+ function readBoundReceiptOutcome(agent, step, resolved) {
190
+ if (!agent || !resolved) return { outcome: null, receiptRequired: false };
191
+ const manifestPath = path.join(taskDir, 'artifact-manifest.json');
192
+ if (!fs.existsSync(manifestPath)) return { outcome: null, receiptRequired: false };
193
+ try {
194
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
195
+ const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
196
+ const expectedOutputId = artifactOutputIdFor(step, agent);
197
+ const attemptStartedAt = Date.parse(resolved.entry.startedAt);
198
+ const owned = artifacts
199
+ .filter((entry) => {
200
+ if (!entry || entry.producer !== agent || !entry.receipt || typeof entry.receipt.outcome !== 'string') return false;
201
+ const boundByAttempt = typeof entry.receipt.attemptId === 'string' && entry.receipt.attemptId === resolved.attemptId;
202
+ const boundByOutput = entry.receipt.attemptId == null && entry.id === expectedOutputId;
203
+ if (!boundByAttempt && !boundByOutput) return false;
204
+ const terminalAt = Date.parse(entry.timestamps?.completedAt || entry.timestamps?.updatedAt || '');
205
+ return Number.isFinite(terminalAt) && (!Number.isFinite(attemptStartedAt) || terminalAt >= attemptStartedAt);
206
+ })
207
+ .sort((a, b) => {
208
+ const ta = Date.parse(a.timestamps?.completedAt || a.timestamps?.updatedAt || 0) || 0;
209
+ const tb = Date.parse(b.timestamps?.completedAt || b.timestamps?.updatedAt || 0) || 0;
210
+ return tb - ta;
211
+ });
212
+ return { outcome: owned[0]?.receipt?.outcome || null, receiptRequired: true };
213
+ } catch {
214
+ return { outcome: null, receiptRequired: false };
215
+ }
216
+ }
217
+ function reconcileStatusFromManifest(agent, step, resolved) {
218
+ const terminal = new Set(['completed', 'blocked', 'changes_requested', 'validation_failed']);
219
+ const { outcome, receiptRequired } = readBoundReceiptOutcome(agent, step, resolved);
220
+ if (terminal.has(outcome)) {
221
+ status.state = outcome;
222
+ return outcome;
223
+ }
224
+ if (receiptRequired) {
225
+ status.state = 'awaiting_artifact_receipt';
226
+ status.phase = 'blocked';
227
+ return status.state;
228
+ }
229
+ if (terminal.has(status.state)) return status.state;
230
+ // Host reported subagent completed and no blocking receipt → treat as completed for metrics/advance.
231
+ if (hookInput.status === 'completed') {
232
+ status.state = 'completed';
233
+ return 'completed';
234
+ }
235
+ return status.state;
236
+ }
237
+
238
+ function finishActiveAttempt() {
239
+ const resolved = resolvedAttempt(); if (!resolved) return null;
240
+ const durationMs = Date.parse(now) - Date.parse(resolved.entry.startedAt);
241
+ const outcome = ['completed', 'blocked', 'changes_requested', 'validation_failed'].includes(status.state) ? status.state : 'unknown';
242
+ appendMetric({ type: 'attempt_finished', at: now, attemptId: resolved.attemptId, outcome, durationMs: Number.isFinite(durationMs) && durationMs >= 0 ? durationMs : null, retryAfterFix: status.retryAfterFix || null, source: resolved.source || 'hook', attributionConfidence: resolved.confidence });
243
+ const active = { ...(status.activeAttempts || {}) }; delete active[resolved.attemptId]; status.activeAttempts = active; status.lastCompletedAttempt = { attemptId: resolved.attemptId, completedAt: now }; return { ...resolved, outcome };
244
+ }
245
+ function beginHumanGate() { const eventId = `${status.metricsRunId || slug}:gate:${Date.now()}`; appendMetric({ eventId, type: 'human_gate_started', at: now, source: 'hook', attributionConfidence: 'high' }); status.activeHumanGateEventId = eventId; }
246
+ function endHumanGateIfResumed() {
247
+ if (!status.activeHumanGateEventId || status.awaitingHumanGate || status.state === 'awaiting_approval') return;
248
+ appendMetric({ type: 'human_gate_ended', at: now, gateStartedEventId: status.activeHumanGateEventId, source: 'hook', attributionConfidence: 'high' });
249
+ delete status.activeHumanGateEventId;
250
+ }
251
+ function completeTaskMetrics() { appendMetric({ type: 'task_completed', at: now, outcome: status.state === 'completed' ? 'completed' : 'unknown', source: 'hook', attributionConfidence: 'high' }); }
252
+ function recordSkippedStep(step, stepIndex) {
253
+ for (const agent of getStepAgents(step)) {
254
+ const runId = status.metricsRunId || `${slug}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
255
+ status.metricsRunId = runId;
256
+ const attempt = nextAttemptOrdinal(stepIndex, agent);
257
+ appendMetric({ type: 'attempt_skipped', at: now, attemptId: `${runId}:step-${stepIndex}:${agent}:attempt-${attempt}`, stepIndex, stepLabel: step.label || null, agent, role: roleFor(agent), attempt, outcome: 'skipped', source: 'hook', attributionConfidence: 'high' });
258
+ }
259
+ }
260
+
261
+
262
+ function writeStatus(next) {
263
+ status = { ...status, ...next, slug, updatedAt: now };
264
+ const temporary = `${statusPath}.${process.pid}.${Date.now()}.tmp`;
265
+ // Open + write + fsync + rename so the durable file lands on disk before
266
+ // the orchestrator reads it. Closes the race window where a subagent that
267
+ // completed milliseconds before writeStatus() could otherwise invoke the
268
+ // next step on a stale status (in particular the human-gate write).
269
+ const fd = fs.openSync(temporary, 'w');
270
+ try {
271
+ fs.writeSync(fd, JSON.stringify(status, null, 2) + '\n');
272
+ try { fs.fsyncSync(fd); } catch {}
273
+ } finally {
274
+ fs.closeSync(fd);
275
+ }
276
+ fs.renameSync(temporary, statusPath);
277
+ }
278
+
279
+ function out(message) {
280
+ process.stdout.write(JSON.stringify({ followup_message: message }));
281
+ }
282
+
283
+ function readArtifact(name) {
284
+ const p = path.join(taskDir, name);
285
+ if (!fs.existsSync(p)) return '';
286
+ try {
287
+ return fs.readFileSync(p, 'utf8');
288
+ } catch {
289
+ return '';
290
+ }
291
+ }
292
+
293
+ function getStepAgents(step) {
294
+ if (!step) return [];
295
+ return Array.isArray(step.agent) ? step.agent : [step.agent];
296
+ }
297
+
298
+ function skipIfMatches(skipIf, dir) {
299
+ if (!skipIf) return false;
300
+ if (skipIf === 'debugger.fixed') {
301
+ const text = readArtifact('debug-report.md');
302
+ return /fixApplied:\s*true/i.test(text) || /\*\*Fix applied:\*\*\s*true/i.test(text);
303
+ }
304
+ if (skipIf === 'ci-investigator.resolved') {
305
+ const text = readArtifact('ci-report.md');
306
+ return /fixApplied:\s*true/i.test(text) || /## Verdict\s*\nPASS/i.test(text);
307
+ }
308
+ return false;
309
+ }
310
+
311
+ function findNextExecutableStep(steps, startIdx, dir) {
312
+ for (let i = startIdx; i < steps.length; i++) {
313
+ if (!skipIfMatches(steps[i].skipIf, dir)) return { step: steps[i], index: i };
314
+ recordSkippedStep(steps[i], i);
315
+ }
316
+ return null;
317
+ }
318
+
319
+ const AGENT_HANDOFF = {
320
+ 'task-analyst': (s) =>
321
+ `Invoke task-analyst for slug "${s}". Write brief.md and decomposition.md under .mavis/team/tasks/${s}/. ` +
322
+ `Upsert the analysis receipt in artifact-manifest.json; do not mutate status.json lifecycle fields. Do not use legacy phase analysis.`,
323
+ debugger: (s) =>
324
+ `Invoke debugger subagent for slug "${s}". Investigate root cause; write debug-report.md with fixApplied: true/false. ` +
325
+ `Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
326
+ 'solution-architect': (s) =>
327
+ `Invoke solution-architect for slug "${s}". Read brief.md; write architecture.md. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
328
+ 'migration-specialist': (s) =>
329
+ `Invoke migration-specialist for slug "${s}". Read brief.md; write migration-plan.md. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
330
+ 'api-contract-reviewer': (s) =>
331
+ `Invoke api-contract-reviewer (readonly) for slug "${s}". Write api-contract-review.md. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
332
+ 'ci-investigator': (s) =>
333
+ `Invoke ci-investigator for slug "${s}". Diagnose CI failure; write ci-report.md; apply minimal fix. ` +
334
+ `Use ci-investigation skill. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
335
+ 'feature-developer': (s) =>
336
+ `Invoke feature-developer for slug "${s}". Read brief.md, decomposition.md, validation-report.md, review.md, architecture.md, migration-plan.md, api-contract-review.md, debug-report.md if present. ` +
337
+ `Follow feature-delivery-workflow.md and reference-features.md. Include unit tests for mappers/logic before handoff. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
338
+ 'build-verifier': (s) =>
339
+ `Invoke build-verifier (readonly) for slug "${s}". Run lint:js, lint:css, type-check, and targeted unit tests from app/. ` +
340
+ `Write validation-report.md and emit receipt outcome completed or validation_failed. Do not edit production code.`,
341
+ 'accessibility-reviewer': (s) =>
342
+ `Invoke accessibility-reviewer (readonly) for slug "${s}". Review UI diff; write a11y-review.md. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
343
+ 'performance-auditor': (s) =>
344
+ `Invoke performance-auditor (readonly) for slug "${s}". Write perf-report.md. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
345
+ 'mockup-reviewer': (s) =>
346
+ `Invoke mockup-reviewer (readonly) for slug "${s}". Write mockup-review.md. Load requestable mockup-review skill/rule. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
347
+ 'code-reviewer': (s) =>
348
+ `Invoke code-reviewer subagent (readonly) for slug "${s}". Review git diff vs brief.md per code-review-mr.md. ` +
349
+ `Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
350
+ 'security-reviewer': (s) =>
351
+ `Invoke security-reviewer (readonly) for slug "${s}". Write security-review.md. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
352
+ 'qa-tester': (s, scope) =>
353
+ `Invoke qa-tester for slug "${s}"${scope ? ` (scope: ${scope})` : ''}. ` +
354
+ `Add/update tests per brief. Follow playwright-agents.md. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
355
+ 'unit-test-planner': (s) =>
356
+ `Invoke unit-test-planner for slug "${s}". Write unit-test-plan.md. Use unit-testing skill. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
357
+ 'unit-test-generator': (s) =>
358
+ `Invoke unit-test-generator for slug "${s}". Implement specs from unit-test-plan.md. Use unit-testing skill. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
359
+ 'unit-test-healer': (s) =>
360
+ `Invoke unit-test-healer for slug "${s}". Fix failing unit tests. Use unit-testing and debug-investigation skills. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
361
+ 'playwright-test-planner': (s) =>
362
+ `Invoke playwright-test-planner for slug "${s}". Create/update app/__tests__/e2e/**/*.cases.md. ` +
363
+ `Follow playwright-agents.md and the playwright-e2e skill. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
364
+ 'playwright-test-generator': (s) =>
365
+ `Invoke playwright-test-generator for slug "${s}". Generate/update *.spec.ts from matching *.cases.md. ` +
366
+ `Use playwright MCP when available. Follow playwright-agents.md and the playwright-e2e skill. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
367
+ 'playwright-test-healer': (s) =>
368
+ `Invoke playwright-test-healer for slug "${s}". Fix failing/flaky e2e while preserving cases.md assertions. ` +
369
+ `Use playwright MCP when available. Follow playwright-agents.md and debug-investigation. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
370
+ 'tech-writer': (s) =>
371
+ `Invoke tech-writer for slug "${s}". Write documentation.md from task artifacts. Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`,
372
+ 'openspec-author': (s, scope) =>
373
+ `Invoke openspec-author for slug "${s}"${scope ? ` (scope: ${scope})` : ''}. ` +
374
+ `Read openspec/config.yaml schema + rules; if openspec/** is absent, emit blocked ("openspec/** not initialized"). ` +
375
+ `Author openspec/changes/<change-id>/{proposal.md, design.md, tasks.md, .openspec.yaml} and the capability spec delta per phase (explore|propose|apply|archive). ` +
376
+ `Run \`openspec validate <change-id> --type change --strict\` and \`openspec validate --specs --strict\` (Phase 3); surface FAIL output verbatim. ` +
377
+ `Upsert the owned terminal receipt (artifactOutputId \`openspec-change\`) in artifact-manifest.json with validator exit code + evidence paths; do not mutate status.json lifecycle fields. `,
378
+ };
379
+
380
+ const REVIEW_AGENTS = new Set([
381
+ 'code-reviewer',
382
+ 'accessibility-reviewer',
383
+ 'security-reviewer',
384
+ 'api-contract-reviewer',
385
+ ]);
386
+
387
+ function metricsHandoff(agent) {
388
+ const matches = Object.entries(status.activeAttempts || {}).filter(([, entry]) => entry.agent === agent);
389
+ if (matches.length === 1) {
390
+ return ` Metrics attemptId: ${matches[0][0]}; write owned artifact-manifest receipt outcome. If the hook cannot identify a parallel stop, append a minimal { attemptId, outcome } receipt to .metrics-receipts.json; do not mutate status.json or calculate duration.`;
391
+ }
392
+ return ' Metrics: use the supplied attemptId when available; for a parallel completion, write a minimal { attemptId, outcome } receipt to .metrics-receipts.json rather than relying on currentAgent. Do not calculate duration.';
393
+ }
394
+
395
+ function artifactHandoff(step, agent) {
396
+ const inputIds = Array.isArray(step?.artifactInputIds) ? step.artifactInputIds.join(', ') : 'manifest defaults';
397
+ return ` Artifact inputs: ${inputIds}; owned output ID: ${artifactOutputIdFor(step, agent)}. Bind the terminal receipt to the supplied attemptId.`;
398
+ }
399
+
400
+ function handoffForAgent(agent, scope, step = null) {
401
+ const fn = AGENT_HANDOFF[agent];
402
+ const handoff = fn ? fn(slug, scope) : `Invoke ${agent} subagent for slug "${slug}". Upsert the owned terminal receipt in artifact-manifest.json; do not mutate status.json lifecycle fields.`;
403
+ return handoff + artifactHandoff(step, agent) + metricsHandoff(agent);
404
+ }
405
+
406
+ function handoffForParallelStep(step) {
407
+ const agents = getStepAgents(step);
408
+ const attemptIds = Object.entries(status.activeAttempts || {})
409
+ .filter(([, entry]) => agents.includes(entry.agent))
410
+ .map(([attemptId, entry]) => `${entry.agent}=${attemptId}`);
411
+ return (
412
+ `Invoke IN PARALLEL (one message, multiple Task calls): ${agents.join(' AND ')} for slug "${slug}". ` +
413
+ `Each agent writes its artifact and owned receipt; the hook records parallelCompleted from stops. Do not mutate status.json lifecycle fields. ` +
414
+ `Artifact inputs: ${Array.isArray(step.artifactInputIds) ? step.artifactInputIds.join(', ') : 'manifest defaults'}; owned output IDs: ${agents.map((agent) => `${agent}=${artifactOutputIdFor(step, agent)}`).join('; ')}. ` +
415
+ `Metrics attemptIds: ${attemptIds.join('; ')}. If the hook cannot identify a parallel stop, append a minimal { attemptId, outcome } receipt to .metrics-receipts.json; do not calculate duration.`
416
+ );
417
+ }
418
+
419
+ function hasGateAfter(agent, humanGates) {
420
+ return Array.isArray(humanGates) && humanGates.includes(`after:${agent}`);
421
+ }
422
+
423
+ function hasGateAfterStep(step, humanGates) {
424
+ return getStepAgents(step).some((a) => hasGateAfter(a, humanGates));
425
+ }
426
+
427
+ function legacyFlow() {
428
+ if (status.phase === 'analysis') {
429
+ process.stdout.write('{}');
430
+ process.exit(0);
431
+ }
432
+ if (status.phase === 'development' && (status.state === 'completed' || status.state === 'in_progress')) {
433
+ writeStatus({ phase: 'executing', state: 'in_progress', currentAgent: 'build-verifier' });
434
+ out(handoffForAgent('build-verifier'));
435
+ process.exit(0);
436
+ }
437
+ if (status.currentAgent === 'build-verifier' && status.state === 'completed') {
438
+ writeStatus({ phase: 'executing', state: 'in_progress', currentAgent: 'code-reviewer' });
439
+ out(handoffForAgent('code-reviewer'));
440
+ process.exit(0);
441
+ }
442
+ if (status.phase === 'review' && status.state === 'completed') {
443
+ writeStatus({ phase: 'executing', state: 'in_progress', currentAgent: 'qa-tester' });
444
+ out(handoffForAgent('qa-tester'));
445
+ process.exit(0);
446
+ }
447
+ function scheduleLegacyRetry(agent, retryAfterFix) {
448
+ const stepIndex = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
449
+ const predecessorAttemptId = status.lastCompletedAttempt?.attemptId || null;
450
+ appendMetric({ type: 'retry_scheduled', at: now, attemptId: predecessorAttemptId, nextAgent: agent, retryAfterFix: retryAfterFix || null, source: 'hook', attributionConfidence: 'unknown' });
451
+ activateAttempts({ agent }, stepIndex);
452
+ }
453
+ if (status.state === 'changes_requested') {
454
+ const reviewer = status.currentAgent;
455
+ if (!reviewer) {
456
+ process.stdout.write('{}');
457
+ process.exit(0);
458
+ }
459
+ scheduleLegacyRetry('feature-developer', reviewer);
460
+ writeStatus({
461
+ currentAgent: 'feature-developer',
462
+ state: 'in_progress',
463
+ phase: 'executing',
464
+ retryAfterFix: reviewer,
465
+ });
466
+ out(handoffForAgent('feature-developer') + ` After fixes, re-run ${reviewer}.`);
467
+ process.exit(0);
468
+ }
469
+ if (status.state === 'validation_failed' && status.currentAgent === 'build-verifier') {
470
+ scheduleLegacyRetry('feature-developer', 'build-verifier');
471
+ writeStatus({
472
+ currentAgent: 'feature-developer',
473
+ state: 'in_progress',
474
+ phase: 'executing',
475
+ retryAfterFix: 'build-verifier',
476
+ });
477
+ out(handoffForAgent('feature-developer') + ' Read validation-report.md. After fixes, re-run build-verifier.');
478
+ process.exit(0);
479
+ }
480
+ if (status.state === 'completed' && status.retryAfterFix) {
481
+ const retry = status.retryAfterFix;
482
+ scheduleLegacyRetry(retry, null);
483
+ writeStatus({ currentAgent: retry, state: 'in_progress', retryAfterFix: null });
484
+ out(handoffForAgent(retry));
485
+ process.exit(0);
486
+ }
487
+ if (status.phase === 'testing' && status.state === 'completed') {
488
+ writeStatus({ phase: 'done', state: 'completed' });
489
+ out(`Pipeline complete for "${slug}". Run /technical-retro ${slug}.`);
490
+ process.exit(0);
491
+ }
492
+ process.stdout.write('{}');
493
+ process.exit(0);
494
+ }
495
+
496
+
497
+ if (hookInput.action === 'end_human_gate') {
498
+ status.awaitingHumanGate = false;
499
+ // A2 (retro §8): pin a single runId per pipeline so subsequent events don't
500
+ // fragment the metrics ledger across multiple runId sequences. Prefer
501
+ // metrics.json.task.runId when metrics already exist, then status.metricsRunId,
502
+ // then mint a fresh seed only on first ever emit.
503
+ try {
504
+ if (fs.existsSync(METRICS_PATH)) {
505
+ const existing = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
506
+ if (existing && existing.task && typeof existing.task.runId === 'string') {
507
+ status.metricsRunId = existing.task.runId;
508
+ }
509
+ }
510
+ } catch {}
511
+ if (status.state === 'awaiting_approval') {
512
+ status.state = 'in_progress';
513
+ status.phase = status.phase === 'human_gate' ? 'executing' : status.phase;
514
+ }
515
+ if (status.activeHumanGateEventId) {
516
+ appendMetric({
517
+ type: 'human_gate_ended',
518
+ at: now,
519
+ gateStartedEventId: status.activeHumanGateEventId,
520
+ source: 'orchestrator',
521
+ attributionConfidence: 'high',
522
+ });
523
+ delete status.activeHumanGateEventId;
524
+ }
525
+ if (hookInput.activateNext && fs.existsSync(pipelinePath)) {
526
+ let resumePipeline = null;
527
+ try { resumePipeline = JSON.parse(fs.readFileSync(pipelinePath, 'utf8')); } catch {}
528
+ if (resumePipeline) {
529
+ const resumeSteps = resumePipeline.steps || [];
530
+ const resumeStart = (typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0) + 1;
531
+ const resumeNext = findNextExecutableStep(resumeSteps, resumeStart, taskDir);
532
+ if (resumeNext) {
533
+ const resumeAgents = getStepAgents(resumeNext.step);
534
+ activateAttempts(resumeNext.step, resumeNext.index);
535
+ writeStatus({
536
+ pipelineIndex: resumeNext.index,
537
+ currentAgent: resumeAgents[0],
538
+ phase: 'executing',
539
+ state: 'in_progress',
540
+ awaitingHumanGate: false,
541
+ parallelCompleted: [],
542
+ });
543
+ if (resumeNext.step.parallel && resumeAgents.length > 1) out(handoffForParallelStep(resumeNext.step));
544
+ else out(handoffForAgent(resumeAgents[0], resumeNext.step.scope, resumeNext.step));
545
+ process.exit(0);
546
+ }
547
+ }
548
+ }
549
+ // A2 (retro §8): if gate closes the final human gate of the pipeline
550
+ // (no resume happens, or current step's gate was the last), surface
551
+ // state="awaiting_approval" so a subsequent /task-continue doesn't misinterpret
552
+ // the post-gate idle state as "still in progress".
553
+ {
554
+ let gateWasTerminal = false;
555
+ if (!hookInput.activateNext && fs.existsSync(pipelinePath)) {
556
+ try {
557
+ const pipe = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
558
+ const steps = pipe.steps || [];
559
+ const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
560
+ const cur = steps[idx];
561
+ const gates = Array.isArray(pipe.humanGates) ? pipe.humanGates : [];
562
+ const hasGate = (step) => Array.isArray(gates) && gates.some((g) => typeof g === 'string' && getStepAgents(step).some((a) => g === `after:${a}`));
563
+ if (cur && hasGate(cur)) gateWasTerminal = true;
564
+ } catch {}
565
+ }
566
+ writeStatus({
567
+ awaitingHumanGate: false,
568
+ state: gateWasTerminal ? 'awaiting_approval' : status.state,
569
+ phase: status.phase,
570
+ });
571
+ }
572
+ process.stdout.write(JSON.stringify({ ok: true, action: 'end_human_gate', activeHumanGateEventId: status.activeHumanGateEventId || null }));
573
+ process.exit(0);
574
+ }
575
+
576
+ if (!fs.existsSync(pipelinePath)) {
577
+ legacyFlow();
578
+ }
579
+
580
+ let pipeline;
581
+ let pipelineParseError = null;
582
+ try {
583
+ pipeline = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
584
+ } catch (error) {
585
+ pipelineParseError = error;
586
+ }
587
+ // Modern pipelines carry an `intent` field. If pipeline.json is present but
588
+ // unparseable, surface the error so the orchestrator can stop and report
589
+ // instead of falling back to legacyFlow() — which silently drops modern
590
+ // roles like `openspec-author`, `qa-tester`, and the test-planner trio.
591
+ if (pipelineParseError) {
592
+ if (fs.existsSync(pipelinePath)) {
593
+ const raw = fs.readFileSync(pipelinePath, 'utf8');
594
+ const hasModernIntent = /"intent"\s*:/.test(raw);
595
+ appendMetric({
596
+ eventId: `${status.metricsRunId || slug}:pipeline_parse_error:${Date.now()}`,
597
+ type: 'pipeline_parse_error',
598
+ at: now,
599
+ source: 'hook',
600
+ attributionConfidence: 'high',
601
+ error: String(pipelineParseError.message || pipelineParseError),
602
+ hasModernIntent,
603
+ });
604
+ if (hasModernIntent) {
605
+ writeStatus({ state: 'blocked', phase: 'blocked', currentAgent: 'task-router' });
606
+ process.stdout.write(JSON.stringify({
607
+ followup_message: `pipeline.json is malformed for slug "${slug}" — JSON parse error: ${pipelineParseError.message}. Re-run the task-router to regenerate pipeline.json, then resume via /task-continue ${slug}.`,
608
+ }));
609
+ process.exit(0);
610
+ }
611
+ }
612
+ legacyFlow();
613
+ }
614
+
615
+ const steps = pipeline.steps || [];
616
+ const initialIdx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
617
+ const initialStep = steps[initialIdx];
618
+ const activeAttemptCountBeforeFinish = Object.keys(status.activeAttempts || {}).length;
619
+ const completingAgentHint = (() => {
620
+ const resolved = resolvedAttempt();
621
+ if (resolved?.entry?.agent) return resolved.entry.agent;
622
+ if (status.currentAgent) return status.currentAgent;
623
+ return getStepAgents(initialStep)[0];
624
+ })();
625
+ const reconcilingAttempt = resolvedAttempt();
626
+ reconcileStatusFromManifest(completingAgentHint, initialStep, reconcilingAttempt);
627
+ if (status.state === 'awaiting_artifact_receipt') {
628
+ writeStatus({});
629
+ process.stdout.write('{}');
630
+ process.exit(0);
631
+ }
632
+ const finishedAttempt = finishActiveAttempt();
633
+ if (!finishedAttempt && (hookInput.attemptId || activeAttemptCountBeforeFinish > 0)) {
634
+ process.stdout.write('{}');
635
+ process.exit(0);
636
+ }
637
+ endHumanGateIfResumed();
638
+ if (steps.length === 0 || pipeline.autoChain === false) {
639
+ writeStatus({});
640
+ process.stdout.write('{}');
641
+ process.exit(0);
642
+ }
643
+
644
+ if (status.awaitingHumanGate || status.state === 'awaiting_approval') {
645
+ process.stdout.write('{}');
646
+ process.exit(0);
647
+ }
648
+
649
+ function nearestPrecedingStepIndex(agent, fromIndex, fallback) {
650
+ for (let index = fromIndex - 1; index >= 0; index -= 1) {
651
+ if (getStepAgents(steps[index]).includes(agent)) return index;
652
+ }
653
+ const match = steps.findIndex((step) => getStepAgents(step).includes(agent));
654
+ return match >= 0 ? match : fallback;
655
+ }
656
+ function scheduleRetry(agent, retryAfterFix, predecessor, explicitStepIndex = null) {
657
+ const currentIndex = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
658
+ const stepIndex = Number.isInteger(explicitStepIndex)
659
+ ? explicitStepIndex
660
+ : nearestPrecedingStepIndex(agent, currentIndex, currentIndex);
661
+ const predecessorAttemptId = predecessor?.attemptId || status.lastCompletedAttempt?.attemptId || null;
662
+ appendMetric({ type: 'retry_scheduled', at: now, attemptId: predecessorAttemptId, nextAgent: agent, retryAfterFix: retryAfterFix || null, source: 'hook', attributionConfidence: predecessor ? predecessor.confidence : 'unknown' });
663
+ activateAttempts(steps[stepIndex] || { agent }, stepIndex);
664
+ return stepIndex;
665
+ }
666
+
667
+ const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
668
+ const currentStep = steps[idx];
669
+ const completedAgent = finishedAttempt?.entry?.agent || status.currentAgent || getStepAgents(currentStep)[0];
670
+ const stepAgents = getStepAgents(currentStep);
671
+
672
+ if (currentStep?.parallel && stepAgents.length > 1) {
673
+ const done = [...(status.parallelCompleted || [])];
674
+ const outcomes = { ...(status.parallelOutcomes || {}) };
675
+ if (finishedAttempt && completedAgent) {
676
+ if (!done.includes(completedAgent)) done.push(completedAgent);
677
+ outcomes[completedAgent] = {
678
+ outcome: finishedAttempt.outcome || status.state,
679
+ attemptId: finishedAttempt.attemptId,
680
+ confidence: finishedAttempt.confidence,
681
+ };
682
+ }
683
+ writeStatus({ parallelCompleted: done, parallelOutcomes: outcomes });
684
+ const remaining = stepAgents.filter((agent) => !done.includes(agent));
685
+ if (remaining.length > 0) {
686
+ process.stdout.write('{}');
687
+ process.exit(0);
688
+ }
689
+
690
+ const blockedProducer = stepAgents.find((agent) => outcomes[agent]?.outcome === 'blocked');
691
+ if (blockedProducer) {
692
+ writeStatus({ parallelCompleted: [], parallelOutcomes: {}, state: 'blocked', phase: 'blocked', currentAgent: blockedProducer });
693
+ process.stdout.write('{}');
694
+ process.exit(0);
695
+ }
696
+
697
+ const retryReviewer = stepAgents.find((agent) => outcomes[agent]?.outcome === 'changes_requested');
698
+ if (retryReviewer === 'mockup-reviewer') {
699
+ beginHumanGate();
700
+ writeStatus({
701
+ awaitingHumanGate: true,
702
+ state: 'awaiting_approval',
703
+ currentAgent: 'mockup-reviewer',
704
+ parallelCompleted: [],
705
+ parallelOutcomes: {},
706
+ });
707
+ out(`Mockup review FAIL for slug "${slug}". Read mockup-review.md. Resume only via /task-continue. Do not retry feature-developer.`);
708
+ process.exit(0);
709
+ }
710
+ if (retryReviewer) {
711
+ const retryAfterFixStepIndex = idx;
712
+ const predecessor = outcomes[retryReviewer]
713
+ ? {
714
+ attemptId: outcomes[retryReviewer].attemptId,
715
+ confidence: outcomes[retryReviewer].confidence,
716
+ }
717
+ : finishedAttempt;
718
+ const developerIndex = scheduleRetry('feature-developer', retryReviewer, predecessor);
719
+ writeStatus({
720
+ pipelineIndex: developerIndex,
721
+ currentAgent: 'feature-developer',
722
+ state: 'in_progress',
723
+ phase: 'executing',
724
+ retryAfterFix: retryReviewer,
725
+ retryAfterFixStepIndex,
726
+ parallelCompleted: [],
727
+ parallelOutcomes: {},
728
+ });
729
+ out(handoffForAgent('feature-developer', steps[developerIndex]?.scope, steps[developerIndex]) + ` After fixes, re-run ${retryReviewer}.`);
730
+ process.exit(0);
731
+ }
732
+
733
+ writeStatus({ parallelCompleted: [], parallelOutcomes: {}, state: 'completed' });
734
+ }
735
+
736
+ if (status.state === 'blocked') {
737
+ writeStatus({ state: 'blocked', phase: 'blocked', currentAgent: completedAgent });
738
+ process.stdout.write('{}');
739
+ process.exit(0);
740
+ }
741
+
742
+ if (status.state === 'changes_requested' && (completedAgent === 'mockup-reviewer' || status.currentAgent === 'mockup-reviewer')) {
743
+ beginHumanGate();
744
+ writeStatus({
745
+ awaitingHumanGate: true,
746
+ state: 'awaiting_approval',
747
+ currentAgent: 'mockup-reviewer',
748
+ });
749
+ out(`Mockup review FAIL for slug "${slug}". Read mockup-review.md. Resume only via /task-continue. Do not retry feature-developer.`);
750
+ process.exit(0);
751
+ }
752
+
753
+ if (status.state === 'changes_requested') {
754
+ const reviewer = status.currentAgent || finishedAttempt?.entry?.agent;
755
+ if (!reviewer) {
756
+ process.stdout.write('{}');
757
+ process.exit(0);
758
+ }
759
+ const retryAfterFixStepIndex = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
760
+ const developerIndex = scheduleRetry('feature-developer', reviewer, finishedAttempt);
761
+ writeStatus({
762
+ pipelineIndex: developerIndex,
763
+ currentAgent: 'feature-developer',
764
+ state: 'in_progress',
765
+ phase: 'executing',
766
+ retryAfterFix: reviewer,
767
+ retryAfterFixStepIndex,
768
+ });
769
+ out(handoffForAgent('feature-developer', steps[developerIndex]?.scope, steps[developerIndex]) + ` After fixes, re-run ${reviewer}.`);
770
+ process.exit(0);
771
+ }
772
+
773
+ if (status.state === 'validation_failed' && status.currentAgent === 'build-verifier') {
774
+ const retryAfterFixStepIndex = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
775
+ const developerIndex = scheduleRetry('feature-developer', 'build-verifier', finishedAttempt);
776
+ writeStatus({
777
+ pipelineIndex: developerIndex,
778
+ currentAgent: 'feature-developer',
779
+ state: 'in_progress',
780
+ phase: 'executing',
781
+ retryAfterFix: 'build-verifier',
782
+ retryAfterFixStepIndex,
783
+ });
784
+ out(handoffForAgent('feature-developer', steps[developerIndex]?.scope, steps[developerIndex]) + ' Read validation-report.md. After fixes, re-run build-verifier.');
785
+ process.exit(0);
786
+ }
787
+
788
+ if (status.state === 'completed' && status.retryAfterFix) {
789
+ const retry = status.retryAfterFix;
790
+ const retryIndex = scheduleRetry(retry, null, finishedAttempt, status.retryAfterFixStepIndex);
791
+ writeStatus({ pipelineIndex: retryIndex, currentAgent: retry, state: 'in_progress', retryAfterFix: null, retryAfterFixStepIndex: null });
792
+ out(handoffForAgent(retry, steps[retryIndex]?.scope, steps[retryIndex]));
793
+ process.exit(0);
794
+ }
795
+
796
+ if (!currentStep) {
797
+ writeStatus({ phase: 'done', state: 'completed', currentAgent: null });
798
+ out(`Pipeline complete for "${slug}". Run /technical-retro ${slug}.`);
799
+ process.exit(0);
800
+ }
801
+
802
+ if (hasGateAfterStep(currentStep, pipeline.humanGates)) {
803
+ beginHumanGate();
804
+ writeStatus({
805
+ awaitingHumanGate: true,
806
+ state: 'awaiting_approval',
807
+ currentAgent: completedAgent,
808
+ });
809
+ process.stdout.write('{}');
810
+ process.exit(0);
811
+ }
812
+
813
+ const next = findNextExecutableStep(steps, idx + 1, taskDir);
814
+ if (!next) {
815
+ completeTaskMetrics();
816
+ writeStatus({
817
+ pipelineIndex: steps.length,
818
+ phase: 'done',
819
+ state: 'completed',
820
+ currentAgent: null,
821
+ awaitingHumanGate: false,
822
+ });
823
+ out(`Pipeline complete for "${slug}". Run /technical-retro ${slug}.`);
824
+ process.exit(0);
825
+ }
826
+
827
+ const { step: nextStep, index: nextIdx } = next;
828
+ const nextAgents = getStepAgents(nextStep);
829
+
830
+ activateAttempts(nextStep, nextIdx);
831
+ writeStatus({
832
+ pipelineIndex: nextIdx,
833
+ currentAgent: nextAgents[0],
834
+ phase: 'executing',
835
+ state: 'in_progress',
836
+ awaitingHumanGate: false,
837
+ parallelCompleted: [],
838
+ });
839
+
840
+ if (nextStep.parallel && nextAgents.length > 1) {
841
+ out(handoffForParallelStep(nextStep));
842
+ } else {
843
+ out(handoffForAgent(nextAgents[0], nextStep.scope, nextStep));
844
+ }
845
+ NODE