agentic-orchestrator 0.1.28 → 0.2.0

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 (836) hide show
  1. package/.claude/settings.local.json +46 -1
  2. package/.cortexrc +28 -0
  3. package/.github/agents/copilot-instructions.md +29 -0
  4. package/.github/copilot-instructions.md +93 -0
  5. package/.vscode/settings.json +13 -0
  6. package/.vscode/tms.code-snippets +223 -0
  7. package/AGENTS.md +72 -1
  8. package/Agentic-Orchestrator.iml +12 -11
  9. package/CLAUDE.md +72 -1
  10. package/CONSTITUTION.md +504 -0
  11. package/FUTURE-ENHANCEMENTS.md +85 -0
  12. package/NEXT-TASKS.md +25 -0
  13. package/PROMPTS.md +161 -0
  14. package/README.md +126 -29
  15. package/agentic/orchestrator/agents.yaml +4 -3
  16. package/agentic/orchestrator/defaults/policy.defaults.yaml +39 -3
  17. package/agentic/orchestrator/gates.yaml +15 -3
  18. package/agentic/orchestrator/policy.yaml +47 -3
  19. package/agentic/orchestrator/prompts/builder.system.md +69 -20
  20. package/agentic/orchestrator/prompts/planner-intake.system.md +149 -0
  21. package/agentic/orchestrator/prompts/planner.system.md +113 -40
  22. package/agentic/orchestrator/prompts/qa.system.md +73 -18
  23. package/agentic/orchestrator/prompts/reconciler.system.md +119 -0
  24. package/agentic/orchestrator/schemas/agents.schema.json +89 -1
  25. package/agentic/orchestrator/schemas/execution-control.schema.json +242 -0
  26. package/agentic/orchestrator/schemas/index.schema.json +234 -0
  27. package/agentic/orchestrator/schemas/intake.review.schema.json +82 -0
  28. package/agentic/orchestrator/schemas/organizer-ordering-artifact.schema.json +75 -0
  29. package/agentic/orchestrator/schemas/plan.schema.json +44 -0
  30. package/agentic/orchestrator/schemas/policy.schema.json +238 -9
  31. package/agentic/orchestrator/schemas/policy.user.schema.json +129 -1
  32. package/agentic/orchestrator/schemas/spec.manifest.bootstrap.schema.json +101 -0
  33. package/agentic/orchestrator/schemas/spec.manifest.verified.schema.json +80 -0
  34. package/agentic/orchestrator/schemas/state.schema.json +298 -3
  35. package/agentic/orchestrator/tools/catalog.json +145 -15
  36. package/agentic/orchestrator/tools/schemas/input/doctor.run.input.schema.json +18 -0
  37. package/agentic/orchestrator/tools/schemas/input/evidence.latest.input.schema.json +4 -0
  38. package/agentic/orchestrator/tools/schemas/input/evidence.verify_chain.input.schema.json +13 -0
  39. package/agentic/orchestrator/tools/schemas/input/feature.intake_submit.input.schema.json +11 -0
  40. package/agentic/orchestrator/tools/schemas/input/feature.question_answer.input.schema.json +15 -0
  41. package/agentic/orchestrator/tools/schemas/input/feature.question_create.input.schema.json +21 -0
  42. package/agentic/orchestrator/tools/schemas/input/feature.question_list.input.schema.json +13 -0
  43. package/agentic/orchestrator/tools/schemas/input/feature.ready_to_merge.input.schema.json +5 -0
  44. package/agentic/orchestrator/tools/schemas/input/feature.send_message.input.schema.json +1 -1
  45. package/agentic/orchestrator/tools/schemas/input/replay.timeline_get.input.schema.json +32 -0
  46. package/agentic/orchestrator/tools/schemas/input/repo.conflict_abort.input.schema.json +16 -0
  47. package/agentic/orchestrator/tools/schemas/input/repo.conflict_files.input.schema.json +16 -0
  48. package/agentic/orchestrator/tools/schemas/input/repo.reconcile_mainline.input.schema.json +37 -0
  49. package/agentic/orchestrator/tools/schemas/input/repo.resolve_conflict.input.schema.json +40 -0
  50. package/agentic/orchestrator/tools/schemas/input/runtime.execution_request_list.input.schema.json +7 -0
  51. package/agentic/orchestrator/tools/schemas/input/runtime.execution_request_submit.input.schema.json +25 -0
  52. package/agentic/orchestrator/tools/schemas/output/doctor.run.output.schema.json +34 -0
  53. package/agentic/orchestrator/tools/schemas/output/evidence.verify_chain.output.schema.json +23 -0
  54. package/agentic/orchestrator/tools/schemas/output/feature.get_context.output.schema.json +62 -2
  55. package/agentic/orchestrator/tools/schemas/output/feature.intake_submit.output.schema.json +24 -0
  56. package/agentic/orchestrator/tools/schemas/output/feature.question_answer.output.schema.json +21 -0
  57. package/agentic/orchestrator/tools/schemas/output/feature.question_create.output.schema.json +12 -0
  58. package/agentic/orchestrator/tools/schemas/output/feature.question_list.output.schema.json +14 -0
  59. package/agentic/orchestrator/tools/schemas/output/feature.ready_to_merge.output.schema.json +31 -0
  60. package/agentic/orchestrator/tools/schemas/output/feature.send_message.output.schema.json +8 -18
  61. package/agentic/orchestrator/tools/schemas/output/replay.timeline_get.output.schema.json +64 -0
  62. package/agentic/orchestrator/tools/schemas/output/repo.conflict_abort.output.schema.json +16 -0
  63. package/agentic/orchestrator/tools/schemas/output/repo.conflict_files.output.schema.json +22 -0
  64. package/agentic/orchestrator/tools/schemas/output/repo.reconcile_mainline.output.schema.json +61 -0
  65. package/agentic/orchestrator/tools/schemas/output/repo.resolve_conflict.output.schema.json +19 -0
  66. package/agentic/orchestrator/tools/schemas/output/report.dashboard.output.schema.json +26 -0
  67. package/agentic/orchestrator/tools/schemas/output/runtime.execution_request_list.output.schema.json +17 -0
  68. package/agentic/orchestrator/tools/schemas/output/runtime.execution_request_submit.output.schema.json +24 -0
  69. package/agentic/orchestrator/tools.md +13 -0
  70. package/apps/control-plane/scripts/validate-mcp-contracts.ts +1 -1
  71. package/apps/control-plane/src/application/kernel-tool-wiring.ts +140 -2
  72. package/apps/control-plane/src/application/services/activity-monitor-service.ts +44 -1
  73. package/apps/control-plane/src/application/services/bootstrap-manifest-generator-service.ts +251 -0
  74. package/apps/control-plane/src/application/services/checkpoint-service.ts +87 -27
  75. package/apps/control-plane/src/application/services/collision-override-service.ts +906 -0
  76. package/apps/control-plane/src/application/services/collision-queue-service.ts +129 -38
  77. package/apps/control-plane/src/application/services/cost-tracking-service.ts +94 -0
  78. package/apps/control-plane/src/application/services/execution-control-service.ts +599 -0
  79. package/apps/control-plane/src/application/services/feature-deletion-service.ts +37 -1
  80. package/apps/control-plane/src/application/services/feature-lifecycle-service.ts +182 -4
  81. package/apps/control-plane/src/application/services/feature-send-message-service.ts +17 -8
  82. package/apps/control-plane/src/application/services/feature-state-service.ts +191 -6
  83. package/apps/control-plane/src/application/services/gate-service.ts +121 -2
  84. package/apps/control-plane/src/application/services/git-reconciliation-service.ts +1591 -0
  85. package/apps/control-plane/src/application/services/intake-service.ts +1468 -0
  86. package/apps/control-plane/src/application/services/merge-service.ts +308 -17
  87. package/apps/control-plane/src/application/services/notifier-service.ts +3 -1
  88. package/apps/control-plane/src/application/services/performance-analytics-service.ts +75 -0
  89. package/apps/control-plane/src/application/services/plan-service.ts +336 -20
  90. package/apps/control-plane/src/application/services/question-service.ts +693 -0
  91. package/apps/control-plane/src/application/services/reactions-service.ts +73 -17
  92. package/apps/control-plane/src/application/services/replay-timeline-service.ts +295 -0
  93. package/apps/control-plane/src/application/services/reporting-service.ts +194 -10
  94. package/apps/control-plane/src/application/services/run-lease-service.ts +121 -5
  95. package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +95 -8
  96. package/apps/control-plane/src/application/tools/tool-metadata.ts +7 -0
  97. package/apps/control-plane/src/application/usage-types.ts +138 -0
  98. package/apps/control-plane/src/cli/add-command-handler.ts +162 -0
  99. package/apps/control-plane/src/cli/answer-command-handler.ts +113 -0
  100. package/apps/control-plane/src/cli/attach-command-handler.ts +12 -3
  101. package/apps/control-plane/src/cli/cli-argument-parser.ts +133 -11
  102. package/apps/control-plane/src/cli/collision-command-handler.ts +113 -0
  103. package/apps/control-plane/src/cli/command-catalog.ts +479 -0
  104. package/apps/control-plane/src/cli/complete-command-handler.ts +23 -0
  105. package/apps/control-plane/src/cli/completion-command-handler.ts +25 -0
  106. package/apps/control-plane/src/cli/completion-resolver.ts +319 -0
  107. package/apps/control-plane/src/cli/completion-shell-renderer.ts +58 -0
  108. package/apps/control-plane/src/cli/dashboard-command-handler.ts +110 -1
  109. package/apps/control-plane/src/cli/dashboard-runtime-runner.ts +1036 -0
  110. package/apps/control-plane/src/cli/dashboard-runtime.ts +31 -0
  111. package/apps/control-plane/src/cli/help-command-handler.ts +17 -185
  112. package/apps/control-plane/src/cli/init-command-handler.ts +51 -6
  113. package/apps/control-plane/src/cli/merge-command-handler.ts +200 -0
  114. package/apps/control-plane/src/cli/questions-command-handler.ts +70 -0
  115. package/apps/control-plane/src/cli/replay-command-handler.ts +98 -0
  116. package/apps/control-plane/src/cli/resume-command-handler.ts +231 -16
  117. package/apps/control-plane/src/cli/retry-command-handler.ts +229 -17
  118. package/apps/control-plane/src/cli/retry-resume-decision.ts +75 -0
  119. package/apps/control-plane/src/cli/rollback-command-handler.ts +4 -2
  120. package/apps/control-plane/src/cli/run-command-handler.ts +35 -1
  121. package/apps/control-plane/src/cli/spec-ingestion-service.ts +45 -55
  122. package/apps/control-plane/src/cli/spec-preparation.ts +114 -0
  123. package/apps/control-plane/src/cli/spec-utils.ts +90 -11
  124. package/apps/control-plane/src/cli/status-command-handler.ts +122 -0
  125. package/apps/control-plane/src/cli/types.ts +41 -3
  126. package/apps/control-plane/src/core/collisions.ts +150 -31
  127. package/apps/control-plane/src/core/constants.ts +18 -1
  128. package/apps/control-plane/src/core/error-codes.ts +39 -0
  129. package/apps/control-plane/src/core/execution-control.ts +56 -0
  130. package/apps/control-plane/src/core/feature-resume-phase.ts +118 -0
  131. package/apps/control-plane/src/core/gate-freshness.ts +359 -0
  132. package/apps/control-plane/src/core/gate-log-extractor.ts +97 -0
  133. package/apps/control-plane/src/core/gates.ts +90 -1
  134. package/apps/control-plane/src/core/intake-artifacts.ts +295 -0
  135. package/apps/control-plane/src/core/kernel-types.ts +11 -0
  136. package/apps/control-plane/src/core/kernel.ts +604 -16
  137. package/apps/control-plane/src/core/mainline-conflict.ts +22 -0
  138. package/apps/control-plane/src/core/merge-repair.ts +149 -0
  139. package/apps/control-plane/src/core/path-layout.ts +46 -2
  140. package/apps/control-plane/src/core/path-rules.ts +11 -3
  141. package/apps/control-plane/src/core/plan-submit-recovery.ts +130 -0
  142. package/apps/control-plane/src/core/questions.ts +49 -0
  143. package/apps/control-plane/src/core/runtime-sessions.ts +4 -0
  144. package/apps/control-plane/src/core/schemas.ts +40 -1
  145. package/apps/control-plane/src/core/tool-caller.ts +25 -1
  146. package/apps/control-plane/src/core/utils/index-normalizer.ts +25 -4
  147. package/apps/control-plane/src/core/worktree-diff.ts +66 -0
  148. package/apps/control-plane/src/index.ts +29 -1
  149. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +300 -6
  150. package/apps/control-plane/src/mcp/kernel-tool-executor.ts +17 -0
  151. package/apps/control-plane/src/mcp/tool-runtime.ts +63 -4
  152. package/apps/control-plane/src/providers/api-worker-provider.ts +62 -15
  153. package/apps/control-plane/src/providers/cli-worker-provider.ts +1037 -61
  154. package/apps/control-plane/src/providers/output-parsers/generic-output-parser.ts +99 -1
  155. package/apps/control-plane/src/providers/output-parsers/types.ts +2 -0
  156. package/apps/control-plane/src/providers/provider-defaults.ts +116 -7
  157. package/apps/control-plane/src/providers/providers.ts +225 -21
  158. package/apps/control-plane/src/providers/worker-provider-factory.ts +26 -2
  159. package/apps/control-plane/src/supervisor/artifact-stager.ts +52 -0
  160. package/apps/control-plane/src/supervisor/build-wave-executor.ts +477 -166
  161. package/apps/control-plane/src/supervisor/execution-enrollment-service.ts +408 -0
  162. package/apps/control-plane/src/supervisor/organizer-enrollment-scheduler.ts +117 -0
  163. package/apps/control-plane/src/supervisor/organizer-sidecar-service.ts +394 -0
  164. package/apps/control-plane/src/supervisor/plan-conformance-scorer.ts +2 -5
  165. package/apps/control-plane/src/supervisor/planner-phase.ts +85 -0
  166. package/apps/control-plane/src/supervisor/planning-wave-executor.ts +993 -64
  167. package/apps/control-plane/src/supervisor/prompt-bundle-loader.ts +20 -1
  168. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +384 -177
  169. package/apps/control-plane/src/supervisor/run-coordinator.ts +723 -20
  170. package/apps/control-plane/src/supervisor/runtime.ts +485 -9
  171. package/apps/control-plane/src/supervisor/session-orchestrator.ts +220 -1
  172. package/apps/control-plane/src/supervisor/types.ts +152 -1
  173. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +1030 -92
  174. package/apps/control-plane/test/activity-monitor.spec.ts +76 -0
  175. package/apps/control-plane/test/add-command-handler.spec.ts +189 -0
  176. package/apps/control-plane/test/application/services/feature-state-service.spec.ts +208 -0
  177. package/apps/control-plane/test/artifact-stager.spec.ts +93 -0
  178. package/apps/control-plane/test/batch-operations.spec.ts +58 -0
  179. package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +50 -2
  180. package/apps/control-plane/test/bootstrap-manifest-generator-service.spec.ts +99 -0
  181. package/apps/control-plane/test/bootstrap.spec.ts +177 -4
  182. package/apps/control-plane/test/checkpoint-service.spec.ts +977 -29
  183. package/apps/control-plane/test/cli-argument-parser.spec.ts +119 -0
  184. package/apps/control-plane/test/cli-helpers.spec.ts +1202 -12
  185. package/apps/control-plane/test/cli.unit.spec.ts +797 -16
  186. package/apps/control-plane/test/collision-command-handler.spec.ts +182 -0
  187. package/apps/control-plane/test/collision-override-service.spec.ts +878 -0
  188. package/apps/control-plane/test/collision-queue.spec.ts +430 -2
  189. package/apps/control-plane/test/collisions.spec.ts +209 -1
  190. package/apps/control-plane/test/core-utils.spec.ts +61 -0
  191. package/apps/control-plane/test/cost-tracking.spec.ts +224 -0
  192. package/apps/control-plane/test/dashboard-api.integration.spec.ts +185 -5
  193. package/apps/control-plane/test/dashboard-client.spec.ts +948 -0
  194. package/apps/control-plane/test/dashboard-command.spec.ts +138 -6
  195. package/apps/control-plane/test/dashboard-runtime-runner.spec.ts +1550 -0
  196. package/apps/control-plane/test/dashboard-runtime.spec.ts +138 -0
  197. package/apps/control-plane/test/dashboard-ui-utils.spec.ts +56 -12
  198. package/apps/control-plane/test/dependency-scheduler.spec.ts +7 -1
  199. package/apps/control-plane/test/env-file.spec.ts +76 -0
  200. package/apps/control-plane/test/execution-control-service.spec.ts +535 -0
  201. package/apps/control-plane/test/execution-enrollment-service.spec.ts +648 -0
  202. package/apps/control-plane/test/feature-lifecycle.spec.ts +126 -0
  203. package/apps/control-plane/test/feature-resume-phase.spec.ts +164 -0
  204. package/apps/control-plane/test/feature-send-message-service.spec.ts +161 -0
  205. package/apps/control-plane/test/feature-state-service.spec.ts +295 -0
  206. package/apps/control-plane/test/fs.spec.ts +80 -0
  207. package/apps/control-plane/test/gate-freshness.spec.ts +590 -0
  208. package/apps/control-plane/test/gate-log-extractor.spec.ts +170 -0
  209. package/apps/control-plane/test/gates.spec.ts +108 -0
  210. package/apps/control-plane/test/git-reconciliation-service.spec.ts +2307 -0
  211. package/apps/control-plane/test/helpers.ts +65 -0
  212. package/apps/control-plane/test/incremental-gates.spec.ts +271 -0
  213. package/apps/control-plane/test/index-normalizer.spec.ts +98 -0
  214. package/apps/control-plane/test/init-wizard.spec.ts +17 -0
  215. package/apps/control-plane/test/intake-artifacts.spec.ts +203 -0
  216. package/apps/control-plane/test/intake-service.spec.ts +3176 -0
  217. package/apps/control-plane/test/kernel-collision-replay.spec.ts +3 -2
  218. package/apps/control-plane/test/kernel-tool-executor.spec.ts +77 -0
  219. package/apps/control-plane/test/kernel-tool-wiring.spec.ts +279 -0
  220. package/apps/control-plane/test/kernel.branches.spec.ts +15 -2
  221. package/apps/control-plane/test/kernel.coverage.spec.ts +7 -3
  222. package/apps/control-plane/test/kernel.coverage2.spec.ts +731 -2
  223. package/apps/control-plane/test/kernel.spec.ts +464 -2
  224. package/apps/control-plane/test/mainline-conflict.spec.ts +66 -0
  225. package/apps/control-plane/test/mcp-helpers.spec.ts +79 -0
  226. package/apps/control-plane/test/mcp.spec.ts +177 -13
  227. package/apps/control-plane/test/merge-command-handler.spec.ts +531 -0
  228. package/apps/control-plane/test/merge-service.spec.ts +570 -4
  229. package/apps/control-plane/test/notifier-service.spec.ts +26 -0
  230. package/apps/control-plane/test/organizer-enrollment-scheduler.spec.ts +340 -0
  231. package/apps/control-plane/test/organizer-ordering-artifact.spec.ts +95 -0
  232. package/apps/control-plane/test/organizer-sidecar-service.spec.ts +468 -0
  233. package/apps/control-plane/test/output-loop-detector.spec.ts +6 -0
  234. package/apps/control-plane/test/path-layout.spec.ts +70 -0
  235. package/apps/control-plane/test/performance-analytics.spec.ts +124 -0
  236. package/apps/control-plane/test/plan-conformance-scorer.spec.ts +53 -0
  237. package/apps/control-plane/test/plan-service.spec.ts +686 -4
  238. package/apps/control-plane/test/planning-wave-executor.spec.ts +3272 -86
  239. package/apps/control-plane/test/policy-loader-service.spec.ts +5 -0
  240. package/apps/control-plane/test/prompt-overlay.spec.ts +65 -0
  241. package/apps/control-plane/test/provider-command-runner-epipe.spec.ts +64 -0
  242. package/apps/control-plane/test/providers/api-worker-provider.spec.ts +129 -0
  243. package/apps/control-plane/test/providers/cli-worker-provider.spec.ts +148 -0
  244. package/apps/control-plane/test/providers/usage-types.spec.ts +98 -0
  245. package/apps/control-plane/test/providers.spec.ts +293 -16
  246. package/apps/control-plane/test/question-command-handlers.spec.ts +156 -0
  247. package/apps/control-plane/test/question-service.spec.ts +1119 -0
  248. package/apps/control-plane/test/reactions.spec.ts +114 -0
  249. package/apps/control-plane/test/replay-command-handler.spec.ts +144 -0
  250. package/apps/control-plane/test/replay-timeline-service.spec.ts +459 -0
  251. package/apps/control-plane/test/response.spec.ts +31 -0
  252. package/apps/control-plane/test/resume-command.spec.ts +757 -9
  253. package/apps/control-plane/test/retry-resume-decision.spec.ts +133 -0
  254. package/apps/control-plane/test/rollback-command-handler.spec.ts +334 -0
  255. package/apps/control-plane/test/rollback-command.spec.ts +120 -0
  256. package/apps/control-plane/test/run-coordinator.spec.ts +3062 -404
  257. package/apps/control-plane/test/schemas/state.schema.spec.ts +71 -0
  258. package/apps/control-plane/test/service-retry-paths.spec.ts +112 -0
  259. package/apps/control-plane/test/services.spec.ts +472 -2
  260. package/apps/control-plane/test/session-management.spec.ts +346 -1
  261. package/apps/control-plane/test/spec-ingestion.spec.ts +102 -28
  262. package/apps/control-plane/test/spec-preparation.spec.ts +182 -0
  263. package/apps/control-plane/test/supervisor-collaborators.spec.ts +191 -3
  264. package/apps/control-plane/test/supervisor.calltool.spec.ts +198 -0
  265. package/apps/control-plane/test/supervisor.spec.ts +95 -16
  266. package/apps/control-plane/test/supervisor.unit.spec.ts +385 -18
  267. package/apps/control-plane/test/tool-runtime.spec.ts +122 -0
  268. package/apps/control-plane/test/worker-decision-loop.spec.ts +3479 -476
  269. package/apps/control-plane/test/worker-execution-policy.spec.ts +1416 -6
  270. package/apps/control-plane/test/worker-provider-adapters.spec.ts +1894 -37
  271. package/apps/control-plane/test/worker-provider-factory.spec.ts +81 -0
  272. package/apps/control-plane/test/worktree-watchdog-service.spec.ts +125 -0
  273. package/apps/control-plane/vitest.config.ts +5 -0
  274. package/config/agentic/orchestrator/agents.yaml +22 -1
  275. package/config/agentic/orchestrator/gates.yaml +24 -7
  276. package/config/agentic/orchestrator/policy.yaml +23 -1
  277. package/config/agentic/orchestrator/prompts/builder.system.md +69 -20
  278. package/config/agentic/orchestrator/prompts/organizer.system.md +85 -0
  279. package/config/agentic/orchestrator/prompts/overrides/builder.claude.md +28 -0
  280. package/config/agentic/orchestrator/prompts/overrides/builder.codex.md +28 -0
  281. package/config/agentic/orchestrator/prompts/overrides/planner.claude.md +20 -0
  282. package/config/agentic/orchestrator/prompts/overrides/planner.codex.md +20 -0
  283. package/config/agentic/orchestrator/prompts/planner-intake.system.md +149 -0
  284. package/config/agentic/orchestrator/prompts/planner.system.md +113 -40
  285. package/config/agentic/orchestrator/prompts/qa.system.md +75 -18
  286. package/config/agentic/orchestrator/prompts/reconciler.system.md +119 -0
  287. package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +26 -2
  288. package/dist/apps/control-plane/application/kernel-tool-wiring.js +40 -2
  289. package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -1
  290. package/dist/apps/control-plane/application/services/activity-monitor-service.js +37 -1
  291. package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -1
  292. package/dist/apps/control-plane/application/services/bootstrap-manifest-generator-service.d.ts +4 -0
  293. package/dist/apps/control-plane/application/services/bootstrap-manifest-generator-service.js +188 -0
  294. package/dist/apps/control-plane/application/services/bootstrap-manifest-generator-service.js.map +1 -0
  295. package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +5 -0
  296. package/dist/apps/control-plane/application/services/checkpoint-service.js +69 -24
  297. package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -1
  298. package/dist/apps/control-plane/application/services/collision-override-service.d.ts +139 -0
  299. package/dist/apps/control-plane/application/services/collision-override-service.js +568 -0
  300. package/dist/apps/control-plane/application/services/collision-override-service.js.map +1 -0
  301. package/dist/apps/control-plane/application/services/collision-queue-service.d.ts +15 -0
  302. package/dist/apps/control-plane/application/services/collision-queue-service.js +92 -33
  303. package/dist/apps/control-plane/application/services/collision-queue-service.js.map +1 -1
  304. package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +11 -0
  305. package/dist/apps/control-plane/application/services/cost-tracking-service.js +75 -0
  306. package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -1
  307. package/dist/apps/control-plane/application/services/execution-control-service.d.ts +75 -0
  308. package/dist/apps/control-plane/application/services/execution-control-service.js +421 -0
  309. package/dist/apps/control-plane/application/services/execution-control-service.js.map +1 -0
  310. package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +1 -0
  311. package/dist/apps/control-plane/application/services/feature-deletion-service.js +23 -1
  312. package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
  313. package/dist/apps/control-plane/application/services/feature-lifecycle-service.d.ts +24 -1
  314. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js +132 -3
  315. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js.map +1 -1
  316. package/dist/apps/control-plane/application/services/feature-send-message-service.js +16 -8
  317. package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -1
  318. package/dist/apps/control-plane/application/services/feature-state-service.d.ts +36 -0
  319. package/dist/apps/control-plane/application/services/feature-state-service.js +163 -6
  320. package/dist/apps/control-plane/application/services/feature-state-service.js.map +1 -1
  321. package/dist/apps/control-plane/application/services/gate-service.d.ts +2 -1
  322. package/dist/apps/control-plane/application/services/gate-service.js +95 -5
  323. package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
  324. package/dist/apps/control-plane/application/services/git-reconciliation-service.d.ts +92 -0
  325. package/dist/apps/control-plane/application/services/git-reconciliation-service.js +1097 -0
  326. package/dist/apps/control-plane/application/services/git-reconciliation-service.js.map +1 -0
  327. package/dist/apps/control-plane/application/services/intake-service.d.ts +63 -0
  328. package/dist/apps/control-plane/application/services/intake-service.js +1050 -0
  329. package/dist/apps/control-plane/application/services/intake-service.js.map +1 -0
  330. package/dist/apps/control-plane/application/services/merge-service.d.ts +5 -1
  331. package/dist/apps/control-plane/application/services/merge-service.js +233 -18
  332. package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
  333. package/dist/apps/control-plane/application/services/notifier-service.d.ts +1 -1
  334. package/dist/apps/control-plane/application/services/notifier-service.js +1 -0
  335. package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -1
  336. package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +11 -0
  337. package/dist/apps/control-plane/application/services/performance-analytics-service.js +59 -0
  338. package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -1
  339. package/dist/apps/control-plane/application/services/plan-service.d.ts +5 -0
  340. package/dist/apps/control-plane/application/services/plan-service.js +254 -15
  341. package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
  342. package/dist/apps/control-plane/application/services/question-service.d.ts +72 -0
  343. package/dist/apps/control-plane/application/services/question-service.js +507 -0
  344. package/dist/apps/control-plane/application/services/question-service.js.map +1 -0
  345. package/dist/apps/control-plane/application/services/reactions-service.d.ts +2 -0
  346. package/dist/apps/control-plane/application/services/reactions-service.js +60 -17
  347. package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -1
  348. package/dist/apps/control-plane/application/services/replay-timeline-service.d.ts +39 -0
  349. package/dist/apps/control-plane/application/services/replay-timeline-service.js +205 -0
  350. package/dist/apps/control-plane/application/services/replay-timeline-service.js.map +1 -0
  351. package/dist/apps/control-plane/application/services/reporting-service.d.ts +59 -0
  352. package/dist/apps/control-plane/application/services/reporting-service.js +121 -9
  353. package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
  354. package/dist/apps/control-plane/application/services/run-lease-service.d.ts +20 -0
  355. package/dist/apps/control-plane/application/services/run-lease-service.js +81 -4
  356. package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
  357. package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +10 -0
  358. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +65 -8
  359. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -1
  360. package/dist/apps/control-plane/application/tools/tool-metadata.js +7 -0
  361. package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
  362. package/dist/apps/control-plane/application/usage-types.d.ts +65 -0
  363. package/dist/apps/control-plane/application/usage-types.js +75 -0
  364. package/dist/apps/control-plane/application/usage-types.js.map +1 -0
  365. package/dist/apps/control-plane/cli/add-command-handler.d.ts +18 -0
  366. package/dist/apps/control-plane/cli/add-command-handler.js +110 -0
  367. package/dist/apps/control-plane/cli/add-command-handler.js.map +1 -0
  368. package/dist/apps/control-plane/cli/answer-command-handler.d.ts +8 -0
  369. package/dist/apps/control-plane/cli/answer-command-handler.js +96 -0
  370. package/dist/apps/control-plane/cli/answer-command-handler.js.map +1 -0
  371. package/dist/apps/control-plane/cli/attach-command-handler.js +8 -3
  372. package/dist/apps/control-plane/cli/attach-command-handler.js.map +1 -1
  373. package/dist/apps/control-plane/cli/cli-argument-parser.js +131 -11
  374. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  375. package/dist/apps/control-plane/cli/collision-command-handler.d.ts +8 -0
  376. package/dist/apps/control-plane/cli/collision-command-handler.js +90 -0
  377. package/dist/apps/control-plane/cli/collision-command-handler.js.map +1 -0
  378. package/dist/apps/control-plane/cli/command-catalog.d.ts +21 -0
  379. package/dist/apps/control-plane/cli/command-catalog.js +416 -0
  380. package/dist/apps/control-plane/cli/command-catalog.js.map +1 -0
  381. package/dist/apps/control-plane/cli/complete-command-handler.d.ts +15 -0
  382. package/dist/apps/control-plane/cli/complete-command-handler.js +26 -0
  383. package/dist/apps/control-plane/cli/complete-command-handler.js.map +1 -0
  384. package/dist/apps/control-plane/cli/completion-command-handler.d.ts +8 -0
  385. package/dist/apps/control-plane/cli/completion-command-handler.js +20 -0
  386. package/dist/apps/control-plane/cli/completion-command-handler.js.map +1 -0
  387. package/dist/apps/control-plane/cli/completion-resolver.d.ts +1 -0
  388. package/dist/apps/control-plane/cli/completion-resolver.js +250 -0
  389. package/dist/apps/control-plane/cli/completion-resolver.js.map +1 -0
  390. package/dist/apps/control-plane/cli/completion-shell-renderer.d.ts +3 -0
  391. package/dist/apps/control-plane/cli/completion-shell-renderer.js +53 -0
  392. package/dist/apps/control-plane/cli/completion-shell-renderer.js.map +1 -0
  393. package/dist/apps/control-plane/cli/dashboard-command-handler.d.ts +1 -0
  394. package/dist/apps/control-plane/cli/dashboard-command-handler.js +83 -1
  395. package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -1
  396. package/dist/apps/control-plane/cli/dashboard-runtime-runner.d.ts +81 -0
  397. package/dist/apps/control-plane/cli/dashboard-runtime-runner.js +724 -0
  398. package/dist/apps/control-plane/cli/dashboard-runtime-runner.js.map +1 -0
  399. package/dist/apps/control-plane/cli/dashboard-runtime.d.ts +1 -0
  400. package/dist/apps/control-plane/cli/dashboard-runtime.js +26 -0
  401. package/dist/apps/control-plane/cli/dashboard-runtime.js.map +1 -0
  402. package/dist/apps/control-plane/cli/help-command-handler.js +13 -172
  403. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  404. package/dist/apps/control-plane/cli/init-command-handler.js +51 -6
  405. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  406. package/dist/apps/control-plane/cli/merge-command-handler.d.ts +8 -0
  407. package/dist/apps/control-plane/cli/merge-command-handler.js +139 -0
  408. package/dist/apps/control-plane/cli/merge-command-handler.js.map +1 -0
  409. package/dist/apps/control-plane/cli/questions-command-handler.d.ts +8 -0
  410. package/dist/apps/control-plane/cli/questions-command-handler.js +59 -0
  411. package/dist/apps/control-plane/cli/questions-command-handler.js.map +1 -0
  412. package/dist/apps/control-plane/cli/replay-command-handler.d.ts +15 -0
  413. package/dist/apps/control-plane/cli/replay-command-handler.js +55 -0
  414. package/dist/apps/control-plane/cli/replay-command-handler.js.map +1 -0
  415. package/dist/apps/control-plane/cli/resume-command-handler.d.ts +2 -0
  416. package/dist/apps/control-plane/cli/resume-command-handler.js +180 -17
  417. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  418. package/dist/apps/control-plane/cli/retry-command-handler.js +202 -16
  419. package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -1
  420. package/dist/apps/control-plane/cli/retry-resume-decision.d.ts +26 -0
  421. package/dist/apps/control-plane/cli/retry-resume-decision.js +61 -0
  422. package/dist/apps/control-plane/cli/retry-resume-decision.js.map +1 -0
  423. package/dist/apps/control-plane/cli/rollback-command-handler.js +3 -2
  424. package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -1
  425. package/dist/apps/control-plane/cli/run-command-handler.js +26 -2
  426. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  427. package/dist/apps/control-plane/cli/spec-ingestion-service.d.ts +2 -0
  428. package/dist/apps/control-plane/cli/spec-ingestion-service.js +37 -48
  429. package/dist/apps/control-plane/cli/spec-ingestion-service.js.map +1 -1
  430. package/dist/apps/control-plane/cli/spec-preparation.d.ts +14 -0
  431. package/dist/apps/control-plane/cli/spec-preparation.js +81 -0
  432. package/dist/apps/control-plane/cli/spec-preparation.js.map +1 -0
  433. package/dist/apps/control-plane/cli/spec-utils.d.ts +4 -0
  434. package/dist/apps/control-plane/cli/spec-utils.js +70 -11
  435. package/dist/apps/control-plane/cli/spec-utils.js.map +1 -1
  436. package/dist/apps/control-plane/cli/status-command-handler.js +69 -0
  437. package/dist/apps/control-plane/cli/status-command-handler.js.map +1 -1
  438. package/dist/apps/control-plane/cli/types.d.ts +41 -4
  439. package/dist/apps/control-plane/cli/types.js +9 -1
  440. package/dist/apps/control-plane/cli/types.js.map +1 -1
  441. package/dist/apps/control-plane/core/collisions.d.ts +37 -19
  442. package/dist/apps/control-plane/core/collisions.js +87 -12
  443. package/dist/apps/control-plane/core/collisions.js.map +1 -1
  444. package/dist/apps/control-plane/core/constants.d.ts +17 -1
  445. package/dist/apps/control-plane/core/constants.js +18 -1
  446. package/dist/apps/control-plane/core/constants.js.map +1 -1
  447. package/dist/apps/control-plane/core/error-codes.d.ts +39 -0
  448. package/dist/apps/control-plane/core/error-codes.js +39 -0
  449. package/dist/apps/control-plane/core/error-codes.js.map +1 -1
  450. package/dist/apps/control-plane/core/execution-control.d.ts +45 -0
  451. package/dist/apps/control-plane/core/execution-control.js +2 -0
  452. package/dist/apps/control-plane/core/execution-control.js.map +1 -0
  453. package/dist/apps/control-plane/core/feature-resume-phase.d.ts +3 -0
  454. package/dist/apps/control-plane/core/feature-resume-phase.js +88 -0
  455. package/dist/apps/control-plane/core/feature-resume-phase.js.map +1 -0
  456. package/dist/apps/control-plane/core/gate-freshness.d.ts +48 -0
  457. package/dist/apps/control-plane/core/gate-freshness.js +267 -0
  458. package/dist/apps/control-plane/core/gate-freshness.js.map +1 -0
  459. package/dist/apps/control-plane/core/gate-log-extractor.d.ts +22 -0
  460. package/dist/apps/control-plane/core/gate-log-extractor.js +66 -0
  461. package/dist/apps/control-plane/core/gate-log-extractor.js.map +1 -0
  462. package/dist/apps/control-plane/core/gates.d.ts +11 -2
  463. package/dist/apps/control-plane/core/gates.js +67 -3
  464. package/dist/apps/control-plane/core/gates.js.map +1 -1
  465. package/dist/apps/control-plane/core/intake-artifacts.d.ts +109 -0
  466. package/dist/apps/control-plane/core/intake-artifacts.js +143 -0
  467. package/dist/apps/control-plane/core/intake-artifacts.js.map +1 -0
  468. package/dist/apps/control-plane/core/kernel-types.d.ts +8 -0
  469. package/dist/apps/control-plane/core/kernel.d.ts +256 -8
  470. package/dist/apps/control-plane/core/kernel.js +400 -14
  471. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  472. package/dist/apps/control-plane/core/mainline-conflict.d.ts +7 -0
  473. package/dist/apps/control-plane/core/mainline-conflict.js +20 -0
  474. package/dist/apps/control-plane/core/mainline-conflict.js.map +1 -0
  475. package/dist/apps/control-plane/core/merge-repair.d.ts +35 -0
  476. package/dist/apps/control-plane/core/merge-repair.js +99 -0
  477. package/dist/apps/control-plane/core/merge-repair.js.map +1 -0
  478. package/dist/apps/control-plane/core/path-layout.d.ts +10 -0
  479. package/dist/apps/control-plane/core/path-layout.js +32 -2
  480. package/dist/apps/control-plane/core/path-layout.js.map +1 -1
  481. package/dist/apps/control-plane/core/path-rules.js +9 -3
  482. package/dist/apps/control-plane/core/path-rules.js.map +1 -1
  483. package/dist/apps/control-plane/core/plan-submit-recovery.d.ts +22 -0
  484. package/dist/apps/control-plane/core/plan-submit-recovery.js +78 -0
  485. package/dist/apps/control-plane/core/plan-submit-recovery.js.map +1 -0
  486. package/dist/apps/control-plane/core/questions.d.ts +40 -0
  487. package/dist/apps/control-plane/core/questions.js +2 -0
  488. package/dist/apps/control-plane/core/questions.js.map +1 -0
  489. package/dist/apps/control-plane/core/runtime-sessions.d.ts +4 -0
  490. package/dist/apps/control-plane/core/schemas.d.ts +2 -0
  491. package/dist/apps/control-plane/core/schemas.js +31 -1
  492. package/dist/apps/control-plane/core/schemas.js.map +1 -1
  493. package/dist/apps/control-plane/core/tool-caller.d.ts +18 -1
  494. package/dist/apps/control-plane/core/utils/index-normalizer.js +17 -4
  495. package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -1
  496. package/dist/apps/control-plane/core/worktree-diff.d.ts +4 -0
  497. package/dist/apps/control-plane/core/worktree-diff.js +52 -0
  498. package/dist/apps/control-plane/core/worktree-diff.js.map +1 -0
  499. package/dist/apps/control-plane/index.d.ts +10 -2
  500. package/dist/apps/control-plane/index.js +9 -2
  501. package/dist/apps/control-plane/index.js.map +1 -1
  502. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +236 -6
  503. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  504. package/dist/apps/control-plane/mcp/kernel-tool-executor.js +16 -0
  505. package/dist/apps/control-plane/mcp/kernel-tool-executor.js.map +1 -1
  506. package/dist/apps/control-plane/mcp/tool-runtime.d.ts +5 -0
  507. package/dist/apps/control-plane/mcp/tool-runtime.js +40 -5
  508. package/dist/apps/control-plane/mcp/tool-runtime.js.map +1 -1
  509. package/dist/apps/control-plane/providers/api-worker-provider.d.ts +2 -2
  510. package/dist/apps/control-plane/providers/api-worker-provider.js +40 -9
  511. package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
  512. package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +59 -3
  513. package/dist/apps/control-plane/providers/cli-worker-provider.js +758 -46
  514. package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
  515. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js +91 -1
  516. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js.map +1 -1
  517. package/dist/apps/control-plane/providers/output-parsers/types.d.ts +2 -0
  518. package/dist/apps/control-plane/providers/provider-defaults.d.ts +12 -0
  519. package/dist/apps/control-plane/providers/provider-defaults.js +103 -7
  520. package/dist/apps/control-plane/providers/provider-defaults.js.map +1 -1
  521. package/dist/apps/control-plane/providers/providers.d.ts +50 -4
  522. package/dist/apps/control-plane/providers/providers.js +145 -14
  523. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  524. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +2 -0
  525. package/dist/apps/control-plane/providers/worker-provider-factory.js +8 -1
  526. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  527. package/dist/apps/control-plane/supervisor/artifact-stager.d.ts +5 -0
  528. package/dist/apps/control-plane/supervisor/artifact-stager.js +45 -0
  529. package/dist/apps/control-plane/supervisor/artifact-stager.js.map +1 -0
  530. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +24 -1
  531. package/dist/apps/control-plane/supervisor/build-wave-executor.js +362 -150
  532. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  533. package/dist/apps/control-plane/supervisor/execution-enrollment-service.d.ts +41 -0
  534. package/dist/apps/control-plane/supervisor/execution-enrollment-service.js +311 -0
  535. package/dist/apps/control-plane/supervisor/execution-enrollment-service.js.map +1 -0
  536. package/dist/apps/control-plane/supervisor/organizer-enrollment-scheduler.d.ts +15 -0
  537. package/dist/apps/control-plane/supervisor/organizer-enrollment-scheduler.js +93 -0
  538. package/dist/apps/control-plane/supervisor/organizer-enrollment-scheduler.js.map +1 -0
  539. package/dist/apps/control-plane/supervisor/organizer-sidecar-service.d.ts +44 -0
  540. package/dist/apps/control-plane/supervisor/organizer-sidecar-service.js +311 -0
  541. package/dist/apps/control-plane/supervisor/organizer-sidecar-service.js.map +1 -0
  542. package/dist/apps/control-plane/supervisor/plan-conformance-scorer.js +2 -5
  543. package/dist/apps/control-plane/supervisor/plan-conformance-scorer.js.map +1 -1
  544. package/dist/apps/control-plane/supervisor/planner-phase.d.ts +3 -0
  545. package/dist/apps/control-plane/supervisor/planner-phase.js +70 -0
  546. package/dist/apps/control-plane/supervisor/planner-phase.js.map +1 -0
  547. package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +42 -0
  548. package/dist/apps/control-plane/supervisor/planning-wave-executor.js +753 -55
  549. package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
  550. package/dist/apps/control-plane/supervisor/prompt-bundle-loader.js +19 -1
  551. package/dist/apps/control-plane/supervisor/prompt-bundle-loader.js.map +1 -1
  552. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +21 -0
  553. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +287 -156
  554. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  555. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +30 -1
  556. package/dist/apps/control-plane/supervisor/run-coordinator.js +561 -17
  557. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  558. package/dist/apps/control-plane/supervisor/runtime.d.ts +84 -0
  559. package/dist/apps/control-plane/supervisor/runtime.js +393 -3
  560. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  561. package/dist/apps/control-plane/supervisor/session-orchestrator.d.ts +54 -0
  562. package/dist/apps/control-plane/supervisor/session-orchestrator.js +176 -1
  563. package/dist/apps/control-plane/supervisor/session-orchestrator.js.map +1 -1
  564. package/dist/apps/control-plane/supervisor/types.d.ts +142 -1
  565. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  566. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +68 -2
  567. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +723 -89
  568. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  569. package/docs/core/ARCHITECTURE.md +227 -0
  570. package/docs/core/DECISIONS.md +94 -0
  571. package/docs/core/DOMAIN-LOGIC.md +60 -0
  572. package/docs/core/PATTERNS.md +201 -0
  573. package/docs/core/TROUBLESHOOTING.md +347 -0
  574. package/docs/core/intentgraph-dependencies.json +39860 -0
  575. package/docs/core/intentgraph.index.json +46580 -0
  576. package/docs/plans/2026-03-10-gate-failure-targeted-repair-design.md +224 -0
  577. package/docs/plans/2026-03-10-gate-failure-targeted-repair.md +1032 -0
  578. package/docs/superpowers/plans/2026-03-16-provider-cli-config.md +743 -0
  579. package/docs/superpowers/plans/2026-03-23-reconcile-divergence-fix.md +777 -0
  580. package/docs/superpowers/plans/2026-03-28-ordering-agent-implementation.md +1754 -0
  581. package/docs/superpowers/plans/2026-03-29-drop-zone-and-provider-optimization.md +1108 -0
  582. package/docs/superpowers/plans/2026-03-29-merge-target-feature-branch.md +685 -0
  583. package/docs/superpowers/plans/2026-03-29-organizer-sidecar-runtime-loop.md +1289 -0
  584. package/docs/superpowers/specs/2026-03-23-reconcile-divergence-fix-design.md +118 -0
  585. package/docs/superpowers/specs/2026-03-28-ordering-agent-spec-audit-design.md +50 -0
  586. package/docs/superpowers/specs/2026-03-29-drop-zone-and-provider-optimization-design.md +254 -0
  587. package/docs/superpowers/specs/2026-03-29-merge-target-feature-branch-design.md +152 -0
  588. package/docs/superpowers/specs/2026-03-29-organizer-sidecar-runtime-loop-design.md +225 -0
  589. package/package.json +3 -2
  590. package/packages/web-dashboard/package.json +2 -1
  591. package/packages/web-dashboard/src/app/analytics/page.tsx +36 -2
  592. package/packages/web-dashboard/src/app/api/actions/route.ts +274 -63
  593. package/packages/web-dashboard/src/app/api/actions/status/route.ts +35 -0
  594. package/packages/web-dashboard/src/app/api/analytics/provider/route.ts +18 -0
  595. package/packages/web-dashboard/src/app/api/collisions/approve/route.ts +58 -0
  596. package/packages/web-dashboard/src/app/api/features/[id]/checkpoint-diff/route.ts +36 -0
  597. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/route.ts +29 -0
  598. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/abort/route.ts +29 -0
  599. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/files/route.ts +30 -0
  600. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/resolve/route.ts +51 -0
  601. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/route.ts +75 -0
  602. package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +16 -2
  603. package/packages/web-dashboard/src/app/api/features/[id]/files/route.ts +26 -0
  604. package/packages/web-dashboard/src/app/api/features/[id]/gate-history/route.ts +27 -0
  605. package/packages/web-dashboard/src/app/api/features/[id]/genealogy/route.ts +26 -0
  606. package/packages/web-dashboard/src/app/api/features/[id]/history/run/[runId]/route.ts +20 -0
  607. package/packages/web-dashboard/src/app/api/features/[id]/history/runs/route.ts +34 -0
  608. package/packages/web-dashboard/src/app/api/features/[id]/intake-workspace/route.ts +20 -0
  609. package/packages/web-dashboard/src/app/api/features/[id]/live-output/route.ts +74 -0
  610. package/packages/web-dashboard/src/app/api/features/[id]/plan/amend/route.ts +21 -0
  611. package/packages/web-dashboard/src/app/api/features/[id]/plan-progress/route.ts +20 -0
  612. package/packages/web-dashboard/src/app/api/features/[id]/planner-artifacts/[artifact]/route.ts +78 -0
  613. package/packages/web-dashboard/src/app/api/features/[id]/planner-lifecycle/route.ts +20 -0
  614. package/packages/web-dashboard/src/app/api/features/[id]/planning-workspace/route.ts +20 -0
  615. package/packages/web-dashboard/src/app/api/features/[id]/questions/[questionId]/answer/route.ts +27 -0
  616. package/packages/web-dashboard/src/app/api/features/[id]/questions/route.ts +18 -0
  617. package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +14 -7
  618. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +57 -2
  619. package/packages/web-dashboard/src/app/api/features/[id]/spec/route.ts +30 -0
  620. package/packages/web-dashboard/src/app/api/features/[id]/triage/route.ts +83 -0
  621. package/packages/web-dashboard/src/app/api/features/[id]/worker-events/route.ts +40 -0
  622. package/packages/web-dashboard/src/app/api/launch/preview/route.ts +86 -0
  623. package/packages/web-dashboard/src/app/api/launch/submit/route.ts +180 -0
  624. package/packages/web-dashboard/src/app/api/mainline/status/route.ts +74 -0
  625. package/packages/web-dashboard/src/app/api/merge-queue/route.ts +13 -0
  626. package/packages/web-dashboard/src/app/api/policy/budget/route.ts +14 -0
  627. package/packages/web-dashboard/src/app/api/projects/route.ts +11 -7
  628. package/packages/web-dashboard/src/app/api/reconciler/queue/route.ts +47 -0
  629. package/packages/web-dashboard/src/app/api/run/route.ts +26 -2
  630. package/packages/web-dashboard/src/app/api/runtime/events/route.ts +227 -0
  631. package/packages/web-dashboard/src/app/api/runtime/operations/route.ts +269 -0
  632. package/packages/web-dashboard/src/app/api/runtime/questions/route.ts +11 -0
  633. package/packages/web-dashboard/src/app/api/runtime/runs/route.ts +80 -0
  634. package/packages/web-dashboard/src/app/api/status/route.ts +4 -2
  635. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -42
  636. package/packages/web-dashboard/src/app/globals.css +34 -3
  637. package/packages/web-dashboard/src/app/launch/page.tsx +357 -0
  638. package/packages/web-dashboard/src/app/layout.tsx +23 -1
  639. package/packages/web-dashboard/src/app/page.tsx +263 -272
  640. package/packages/web-dashboard/src/components/dashboard/attention-strip.tsx +52 -0
  641. package/packages/web-dashboard/src/components/dashboard/collision-approval-drawer.tsx +185 -0
  642. package/packages/web-dashboard/src/components/dashboard/command-center-header.tsx +102 -0
  643. package/packages/web-dashboard/src/components/dashboard/mainline-status-banner.tsx +84 -0
  644. package/packages/web-dashboard/src/components/dashboard/merged-archive.tsx +36 -0
  645. package/packages/web-dashboard/src/components/dashboard/prioritized-queues.tsx +98 -0
  646. package/packages/web-dashboard/src/components/dashboard/reconciler-queue-card.tsx +115 -0
  647. package/packages/web-dashboard/src/components/dashboard/secondary-diagnostics-rail.tsx +48 -0
  648. package/packages/web-dashboard/src/components/dashboard/task-filter-bar.tsx +74 -0
  649. package/packages/web-dashboard/src/components/dashboard/triage-drawer.tsx +455 -0
  650. package/packages/web-dashboard/src/components/diff-viewer.tsx +19 -3
  651. package/packages/web-dashboard/src/components/evidence-viewer.tsx +65 -51
  652. package/packages/web-dashboard/src/components/feature-card.tsx +90 -7
  653. package/packages/web-dashboard/src/components/feature-cost-panel.tsx +112 -11
  654. package/packages/web-dashboard/src/components/feature-list-view.tsx +25 -4
  655. package/packages/web-dashboard/src/components/features/runtime-inspector/EventsTimelineView.tsx +260 -0
  656. package/packages/web-dashboard/src/components/features/runtime-inspector/OperationsListView.tsx +172 -0
  657. package/packages/web-dashboard/src/components/features/runtime-inspector/RuntimeInspectorPanel.tsx +896 -0
  658. package/packages/web-dashboard/src/components/filter-bar.tsx +7 -39
  659. package/packages/web-dashboard/src/components/focus/ActionableRiskList.tsx +46 -0
  660. package/packages/web-dashboard/src/components/focus/AgentRolePerformanceCard.tsx +200 -0
  661. package/packages/web-dashboard/src/components/focus/BlockedGuidanceBanner.tsx +149 -0
  662. package/packages/web-dashboard/src/components/focus/CheckpointInspector.tsx +123 -0
  663. package/packages/web-dashboard/src/components/focus/CheckpointRail.tsx +118 -0
  664. package/packages/web-dashboard/src/components/focus/CheckpointScrubber.tsx +249 -0
  665. package/packages/web-dashboard/src/components/focus/CollisionApprovalBanner.tsx +192 -0
  666. package/packages/web-dashboard/src/components/focus/CollisionRadar.tsx +136 -0
  667. package/packages/web-dashboard/src/components/focus/ConflictStatusCard.tsx +52 -0
  668. package/packages/web-dashboard/src/components/focus/ContextSidebar.tsx +108 -0
  669. package/packages/web-dashboard/src/components/focus/DiagnosisPanel.tsx +68 -0
  670. package/packages/web-dashboard/src/components/focus/FeatureDecisionBanner.tsx +68 -0
  671. package/packages/web-dashboard/src/components/focus/FeatureQuestionAnswerPanel.tsx +167 -0
  672. package/packages/web-dashboard/src/components/focus/FocusHeader.tsx +54 -0
  673. package/packages/web-dashboard/src/components/focus/FocusLayout.tsx +283 -0
  674. package/packages/web-dashboard/src/components/focus/GateFlakinessSummary.tsx +144 -0
  675. package/packages/web-dashboard/src/components/focus/GenealogyTree.tsx +34 -0
  676. package/packages/web-dashboard/src/components/focus/HeroBlock.tsx +67 -0
  677. package/packages/web-dashboard/src/components/focus/LiveAgentConsole.tsx +277 -0
  678. package/packages/web-dashboard/src/components/focus/MergeQueueCard.tsx +78 -0
  679. package/packages/web-dashboard/src/components/focus/OperationalSummaryCard.tsx +227 -0
  680. package/packages/web-dashboard/src/components/focus/PinnedActions.tsx +96 -0
  681. package/packages/web-dashboard/src/components/focus/PlanAmendmentPanel.tsx +250 -0
  682. package/packages/web-dashboard/src/components/focus/PlanProgressPanel.tsx +133 -0
  683. package/packages/web-dashboard/src/components/focus/PlannerArtifactViewer.tsx +158 -0
  684. package/packages/web-dashboard/src/components/focus/PlannerLifecycleHeader.tsx +141 -0
  685. package/packages/web-dashboard/src/components/focus/ProgressSnapshotCard.tsx +113 -0
  686. package/packages/web-dashboard/src/components/focus/RecentMaterialChanges.tsx +69 -0
  687. package/packages/web-dashboard/src/components/focus/RoleLogViewer.tsx +436 -0
  688. package/packages/web-dashboard/src/components/focus/RunHistoryBrowser.tsx +62 -0
  689. package/packages/web-dashboard/src/components/focus/SpecViewer.tsx +172 -0
  690. package/packages/web-dashboard/src/components/focus/TabBar.tsx +33 -0
  691. package/packages/web-dashboard/src/components/focus/UsageBurnChart.tsx +212 -0
  692. package/packages/web-dashboard/src/components/focus/VerificationSummaryCard.tsx +122 -0
  693. package/packages/web-dashboard/src/components/focus/tabs/ChangesTab.tsx +325 -0
  694. package/packages/web-dashboard/src/components/focus/tabs/ConflictsTab.tsx +395 -0
  695. package/packages/web-dashboard/src/components/focus/tabs/GatesQaTab.tsx +38 -0
  696. package/packages/web-dashboard/src/components/focus/tabs/HistoryTab.tsx +213 -0
  697. package/packages/web-dashboard/src/components/focus/tabs/IntakeTab.tsx +429 -0
  698. package/packages/web-dashboard/src/components/focus/tabs/OverviewTab.tsx +217 -0
  699. package/packages/web-dashboard/src/components/focus/tabs/PlanningTab.tsx +390 -0
  700. package/packages/web-dashboard/src/components/focus/tabs/ReviewTab.tsx +497 -0
  701. package/packages/web-dashboard/src/components/focus/tabs/RuntimeTab.tsx +213 -0
  702. package/packages/web-dashboard/src/components/focus/tabs/TranscriptTab.tsx +315 -0
  703. package/packages/web-dashboard/src/components/gate-results.tsx +2 -2
  704. package/packages/web-dashboard/src/components/human-input-panel.tsx +33 -57
  705. package/packages/web-dashboard/src/components/kanban-board.tsx +4 -0
  706. package/packages/web-dashboard/src/components/launch/launch-draft-card.tsx +131 -0
  707. package/packages/web-dashboard/src/components/plan-viewer.tsx +147 -69
  708. package/packages/web-dashboard/src/components/quick-launch-panel.tsx +20 -47
  709. package/packages/web-dashboard/src/components/summary-bar.tsx +30 -76
  710. package/packages/web-dashboard/src/lib/aop-client.ts +2484 -36
  711. package/packages/web-dashboard/src/lib/blocked-state-guidance.ts +475 -0
  712. package/packages/web-dashboard/src/lib/collision-radar.ts +136 -0
  713. package/packages/web-dashboard/src/lib/dashboard-action-states.ts +204 -0
  714. package/packages/web-dashboard/src/lib/dashboard-runtime-client.ts +439 -0
  715. package/packages/web-dashboard/src/lib/dashboard-utils.ts +179 -18
  716. package/packages/web-dashboard/src/lib/drop-zone-utils.ts +92 -0
  717. package/packages/web-dashboard/src/lib/focus-detail-derivations.ts +958 -0
  718. package/packages/web-dashboard/src/lib/focus-view.ts +300 -0
  719. package/packages/web-dashboard/src/lib/health-diagnosis.ts +356 -0
  720. package/packages/web-dashboard/src/lib/launch-contracts.ts +77 -0
  721. package/packages/web-dashboard/src/lib/launch-markdown.ts +107 -0
  722. package/packages/web-dashboard/src/lib/launch-page-preview.ts +89 -0
  723. package/packages/web-dashboard/src/lib/live-feed.ts +1 -1
  724. package/packages/web-dashboard/src/lib/multi-project-config.ts +33 -0
  725. package/packages/web-dashboard/src/lib/orchestrator-tools.ts +845 -59
  726. package/packages/web-dashboard/src/lib/planner-workspace.ts +1285 -0
  727. package/packages/web-dashboard/src/lib/review-contracts.ts +5 -3
  728. package/packages/web-dashboard/src/lib/runtime-files.ts +285 -0
  729. package/packages/web-dashboard/src/lib/tool-catalog.ts +51 -0
  730. package/packages/web-dashboard/src/lib/types.ts +731 -3
  731. package/packages/web-dashboard/src/lib/usage-burn.ts +175 -0
  732. package/packages/web-dashboard/src/lib/worktree-diff.ts +128 -0
  733. package/packages/web-dashboard/src/styles/dashboard.module.css +1742 -459
  734. package/packages/web-dashboard/test/api/actions/route.spec.ts +675 -0
  735. package/packages/web-dashboard/test/api/features/diff.route.spec.ts +57 -0
  736. package/packages/web-dashboard/test/api/features/feature.route.spec.ts +99 -0
  737. package/packages/web-dashboard/test/api/features/live-output.route.spec.ts +123 -0
  738. package/packages/web-dashboard/test/api/features/plan-amend.route.spec.ts +95 -0
  739. package/packages/web-dashboard/test/api/features/planner-workspaces.route.spec.ts +162 -0
  740. package/packages/web-dashboard/test/api/features/question-answer.route.spec.ts +99 -0
  741. package/packages/web-dashboard/test/api/features/triage.route.spec.ts +195 -0
  742. package/packages/web-dashboard/test/api/launch/preview.route.spec.ts +149 -0
  743. package/packages/web-dashboard/test/api/launch/submit.route.spec.ts +382 -0
  744. package/packages/web-dashboard/test/api/runtime/events/route.spec.ts +164 -0
  745. package/packages/web-dashboard/test/api/runtime/operations/route.spec.ts +156 -0
  746. package/packages/web-dashboard/test/api/runtime/runs/route.spec.ts +112 -0
  747. package/packages/web-dashboard/test/components/changes-tab.spec.tsx +76 -0
  748. package/packages/web-dashboard/test/components/command-center-root.spec.tsx +87 -0
  749. package/packages/web-dashboard/test/components/diagnosis-panel.spec.tsx +59 -0
  750. package/packages/web-dashboard/test/components/feature-card.spec.tsx +45 -0
  751. package/packages/web-dashboard/test/components/focus-layout.spec.tsx +299 -0
  752. package/packages/web-dashboard/test/components/gate-results.spec.tsx +39 -0
  753. package/packages/web-dashboard/test/components/gates-qa-tab.spec.tsx +118 -0
  754. package/packages/web-dashboard/test/components/human-input-panel.spec.tsx +54 -0
  755. package/packages/web-dashboard/test/components/intake-tab.spec.tsx +210 -0
  756. package/packages/web-dashboard/test/components/kanban-board.spec.tsx +35 -0
  757. package/packages/web-dashboard/test/components/launch-draft-card.spec.tsx +54 -0
  758. package/packages/web-dashboard/test/components/launch-page.spec.tsx +79 -0
  759. package/packages/web-dashboard/test/components/overview-tab.spec.tsx +236 -0
  760. package/packages/web-dashboard/test/components/planning-tab.spec.tsx +202 -0
  761. package/packages/web-dashboard/test/components/review-tab.spec.tsx +169 -0
  762. package/packages/web-dashboard/test/components/role-log-viewer.spec.ts +42 -0
  763. package/packages/web-dashboard/test/components/runtime-inspector.spec.tsx +22 -0
  764. package/packages/web-dashboard/test/components/runtime-tab.spec.tsx +133 -0
  765. package/packages/web-dashboard/test/components/transcript-tab.spec.tsx +46 -0
  766. package/packages/web-dashboard/test/components/triage-drawer.spec.tsx +159 -0
  767. package/packages/web-dashboard/test/lib/aop-client.spec.ts +235 -0
  768. package/packages/web-dashboard/test/lib/dashboard-runtime-client.spec.ts +144 -0
  769. package/packages/web-dashboard/test/lib/focus-detail-derivations.spec.ts +314 -0
  770. package/packages/web-dashboard/test/lib/focus-view.spec.ts +248 -0
  771. package/packages/web-dashboard/test/lib/health-diagnosis.spec.ts +277 -0
  772. package/packages/web-dashboard/test/lib/launch-markdown.spec.ts +36 -0
  773. package/packages/web-dashboard/test/lib/multi-project-config.spec.ts +54 -0
  774. package/packages/web-dashboard/test/lib/orchestrator-tools.spec.ts +352 -0
  775. package/packages/web-dashboard/test/lib/planner-workspace.spec.ts +289 -0
  776. package/packages/web-dashboard/test/lib/worktree-diff.spec.ts +119 -0
  777. package/packages/web-dashboard/vitest.config.ts +2 -0
  778. package/spec-files/completed/agentic_orchestrator_add_feature_to_active_execution_spec.md +557 -0
  779. package/spec-files/completed/agentic_orchestrator_dashboard_command_center_redesign_spec.md +1147 -0
  780. package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +18 -16
  781. package/spec-files/completed/agentic_orchestrator_feature_focus_view_track_a_spec.md +672 -0
  782. package/spec-files/completed/agentic_orchestrator_feature_focus_view_track_b_spec.md +794 -0
  783. package/spec-files/completed/agentic_orchestrator_feature_focus_view_track_c_decision_centric_remediation_spec.md +1037 -0
  784. package/spec-files/completed/agentic_orchestrator_feature_focus_view_ux_redesign_spec.md +1432 -0
  785. package/spec-files/completed/agentic_orchestrator_focus_plan_tab_intake_planning_workspace_spec.md +921 -0
  786. package/spec-files/completed/agentic_orchestrator_intentional_collision_override_spec.md +584 -0
  787. package/spec-files/completed/agentic_orchestrator_interactive_planning_intake_and_requirements_verification_spec.md +1185 -0
  788. package/spec-files/completed/agentic_orchestrator_reactive_execution_enrollment_spec.md +864 -0
  789. package/spec-files/{outstanding → completed}/agentic_orchestrator_runtime_inspection_spec.md +92 -19
  790. package/spec-files/completed/agentic_orchestrator_scope_aware_run_lease_spec.md +408 -0
  791. package/spec-files/completed/git-reconciliation-engine.md +827 -0
  792. package/spec-files/outstanding/agentic_orchestrator_dashboard_quick_launch_and_control_surface_spec.md +331 -0
  793. package/spec-files/outstanding/agentic_orchestrator_enterprise_governance_dashboard_spec.md +16 -6
  794. package/spec-files/outstanding/agentic_orchestrator_evidence_integrity_doctor_spec.md +60 -9
  795. package/spec-files/outstanding/agentic_orchestrator_focus_plan_tab_execution_contract_workspace_spec.md +616 -0
  796. package/spec-files/outstanding/agentic_orchestrator_headless_standby_dashboard_runtime_spec.md +310 -0
  797. package/spec-files/outstanding/agentic_orchestrator_human_input_interaction_protocol_spec.md +175 -72
  798. package/spec-files/outstanding/agentic_orchestrator_interactive_rename_cleanup_spec.md +197 -0
  799. package/spec-files/outstanding/agentic_orchestrator_interactive_resume_and_reconciliation_disposition_spec.md +412 -0
  800. package/spec-files/outstanding/agentic_orchestrator_knowledge_canary_spec.md +166 -137
  801. package/spec-files/outstanding/agentic_orchestrator_observability_replay_spec.md +3 -3
  802. package/spec-files/outstanding/agentic_orchestrator_phase_specific_agent_profiles_and_token_telemetry_spec.md +303 -0
  803. package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +18 -5
  804. package/spec-files/outstanding/agentic_orchestrator_policy_stratification_spec.md +225 -0
  805. package/spec-files/outstanding/agentic_orchestrator_quality_adoption_execution_spec.md +77 -50
  806. package/spec-files/outstanding/agentic_orchestrator_ready_to_merge_branch_handoff_spec.md +724 -0
  807. package/spec-files/outstanding/agentic_orchestrator_remove_deterministic_mode_spec.md +263 -0
  808. package/spec-files/outstanding/agentic_orchestrator_request_more_context_and_dashboard_human_input_spec.md +456 -0
  809. package/spec-files/outstanding/agentic_orchestrator_spec_coverage_and_reconciliation_enforcement_spec.md +1411 -0
  810. package/spec-files/outstanding/agentic_orchestrator_spec_ordering_agent_spec.md +370 -0
  811. package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1 -1
  812. package/spec-files/progress.md +2026 -120
  813. package/specs/001-runtime-inspection/checklists/requirements.md +35 -0
  814. package/specs/001-runtime-inspection/design.md +338 -0
  815. package/specs/001-runtime-inspection/spec.md +95 -0
  816. package/specs/002-scope-aware-lease/checklists/requirements.md +35 -0
  817. package/specs/002-scope-aware-lease/contracts/lease-registry.schema.json +101 -0
  818. package/specs/002-scope-aware-lease/data-model.md +236 -0
  819. package/specs/002-scope-aware-lease/plan.md +766 -0
  820. package/specs/002-scope-aware-lease/quickstart.md +150 -0
  821. package/specs/002-scope-aware-lease/research.md +135 -0
  822. package/specs/002-scope-aware-lease/spec.md +128 -0
  823. package/specs/002-scope-aware-lease/tasks.md +767 -0
  824. package/tsconfig.json +1 -1
  825. package/vitest.config.ts +28 -0
  826. package/ARCHITECTURE_ADHERENCE_ANALYSIS.md +0 -871
  827. package/packages/web-dashboard/next-env.d.ts +0 -6
  828. package/packages/web-dashboard/src/components/detail-panel.tsx +0 -1124
  829. package/packages/web-dashboard/src/components/review-workspace.tsx +0 -1162
  830. /package/spec-files/{outstanding → completed}/agentic_orchestrator_artifact_database_publishing_spec.md +0 -0
  831. /package/spec-files/{outstanding → completed}/agentic_orchestrator_cli_shell_tab_completion_spec.md +0 -0
  832. /package/spec-files/{outstanding → completed}/agentic_orchestrator_dashboard_diff_and_agent_console_spec.md +0 -0
  833. /package/spec-files/{outstanding → completed}/agentic_orchestrator_performance_improvements_spec.md +0 -0
  834. /package/spec-files/{outstanding → completed}/agentic_orchestrator_persistent_worker_runtime_spec.md +0 -0
  835. /package/spec-files/{outstanding → completed}/agentic_orchestrator_provider_auth_bootstrap_spec.md +0 -0
  836. /package/spec-files/{outstanding → completed}/agentic_orchestrator_real_worker_provider_execution_spec.md +0 -0
@@ -0,0 +1,2307 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { FailResponse } from '../src/core/response.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Hoisted mocks
6
+ // ---------------------------------------------------------------------------
7
+ const ensureDirMock = vi.hoisted(() => vi.fn(async () => {}));
8
+ const nowIsoMock = vi.hoisted(() => vi.fn(() => '2026-01-01T00:00:00.000Z'));
9
+ const runGitMock = vi.hoisted(() => vi.fn());
10
+ const fsWriteFileMock = vi.hoisted(() => vi.fn(async () => {}));
11
+ const fsAppendFileMock = vi.hoisted(() => vi.fn(async () => {}));
12
+
13
+ vi.mock('../src/core/fs.js', () => ({
14
+ ensureDir: ensureDirMock,
15
+ nowIso: nowIsoMock,
16
+ }));
17
+
18
+ vi.mock('../src/core/git.js', () => ({
19
+ runGit: runGitMock,
20
+ }));
21
+
22
+ vi.mock('node:fs/promises', () => ({
23
+ default: {
24
+ writeFile: fsWriteFileMock,
25
+ appendFile: fsAppendFileMock,
26
+ },
27
+ }));
28
+
29
+ import {
30
+ GitReconciliationService,
31
+ type GitReconciliationServicePort,
32
+ } from '../src/application/services/git-reconciliation-service.js';
33
+ import type { RuntimeSessionsSnapshot } from '../src/core/runtime-sessions.js';
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function cmdResult(
40
+ overrides: Partial<{
41
+ code: number;
42
+ signal: NodeJS.Signals | null;
43
+ stdout: string;
44
+ stderr: string;
45
+ timeout: boolean;
46
+ }> = {},
47
+ ) {
48
+ return { code: 0, signal: null, stdout: '', stderr: '', timeout: false, ...overrides };
49
+ }
50
+
51
+ function makePort(
52
+ overrides: Partial<GitReconciliationServicePort> = {},
53
+ ): GitReconciliationServicePort {
54
+ return {
55
+ getRepoRoot: vi.fn(() => '/repo'),
56
+ worktreePath: vi.fn((_id: string) => '/repo/.worktrees/feat1'),
57
+ readState: vi.fn(async (_id: string) => ({
58
+ frontMatter: { feature_id: 'feat1', version: 1, status: 'building' } as Record<
59
+ string,
60
+ unknown
61
+ >,
62
+ body: '# Feature\n',
63
+ })),
64
+ writeState: vi.fn(async () => {}),
65
+ withFeatureLock: (async <T>(_featureId: string, operation: () => Promise<T>) =>
66
+ await operation()) as GitReconciliationServicePort['withFeatureLock'],
67
+ readIndex: vi.fn(async () => ({
68
+ collision_overrides: [],
69
+ collision_records: [],
70
+ })),
71
+ evidencePath: vi.fn((_id: string) => '/repo/.aop/features/feat1/evidence'),
72
+ logsPath: vi.fn((_id: string) => '/repo/.aop/features/feat1/logs'),
73
+ withIndexLock: (async <T>(operation: () => Promise<T>) =>
74
+ await operation()) as GitReconciliationServicePort['withIndexLock'],
75
+ readRunLease: vi.fn(
76
+ async () =>
77
+ ({
78
+ run_id: 'run:test',
79
+ orchestrator_session_id: 'orchestrator:1',
80
+ reconciler_session_id: 'reconciler:1',
81
+ reconciler_active_feature_id: null,
82
+ reconciler_queue: [],
83
+ provider: 'codex',
84
+ model: 'codex-default',
85
+ provider_config_ref_hash: 'hash',
86
+ owner_instance_id: 'owner:test',
87
+ lease_id: 'lease:test',
88
+ started_at: '2026-01-01T00:00:00.000Z',
89
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
90
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
91
+ feature_sessions: {},
92
+ }) satisfies RuntimeSessionsSnapshot,
93
+ ),
94
+ writeRunLease: vi.fn(async () => {}),
95
+ normalizeRuntimeSessions: vi.fn((value: RuntimeSessionsSnapshot) => value),
96
+ getProvider: vi.fn(() => null),
97
+ ...overrides,
98
+ };
99
+ }
100
+
101
+ function makeSvc(portOverrides: Partial<GitReconciliationServicePort> = {}) {
102
+ return new GitReconciliationService(makePort(portOverrides));
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // reconcileWithMainline — up_to_date
107
+ // ---------------------------------------------------------------------------
108
+
109
+ describe('GitReconciliationService.reconcileWithMainline', () => {
110
+ beforeEach(() => {
111
+ vi.resetAllMocks();
112
+ nowIsoMock.mockReturnValue('2026-01-01T00:00:00.000Z');
113
+ });
114
+
115
+ it('GIVEN_worktree_up_to_date_WHEN_reconcile_THEN_returns_up_to_date', async () => {
116
+ const mergeBase = 'abc123';
117
+ runGitMock
118
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
119
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${mergeBase}\n` })) // merge-base main HEAD (local)
120
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${mergeBase}\n` })) // rev-parse main (repoRoot) → mainlineHead === mergeBase → up_to_date
121
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin123\n' })) // rev-parse origin/main (fallback, not used)
122
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'worktree456\n' })); // rev-parse HEAD
123
+
124
+ const svc = makeSvc();
125
+ const result = await svc.reconcileWithMainline('feat1', 'main');
126
+
127
+ expect(result.ok).toBe(true);
128
+ if (!result.ok) {
129
+ return;
130
+ }
131
+ expect(result.data.status).toBe('up_to_date');
132
+ expect(result.data.divergence_detected).toBe(false);
133
+ expect(result.data.conflicting_files).toHaveLength(0);
134
+ });
135
+
136
+ it('GIVEN_fast_forward_possible_WHEN_reconcile_THEN_returns_merged_with_ff_strategy', async () => {
137
+ runGitMock
138
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
139
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base main HEAD (local)
140
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main (repoRoot) — mainlineHead
141
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main (fallback, not used)
142
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD (worktree)
143
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'fileA.ts\n' })) // mainline diff
144
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'fileB.ts\n' })) // worktree diff
145
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // write evidence — mainline diff
146
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // write evidence — worktree diff
147
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // status --porcelain
148
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // ff merge succeeds
149
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // any remaining
150
+
151
+ const svc = makeSvc();
152
+ const result = await svc.reconcileWithMainline('feat1', 'main');
153
+
154
+ expect(result.ok).toBe(true);
155
+ if (!result.ok) {
156
+ return;
157
+ }
158
+ expect(result.data.status).toBe('merged');
159
+ expect(result.data.divergence_detected).toBe(true);
160
+ expect(result.data.conflicting_files).toHaveLength(0);
161
+ });
162
+
163
+ it('GIVEN_ff_fails_but_rebase_succeeds_WHEN_reconcile_THEN_returns_merged', async () => {
164
+ runGitMock
165
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
166
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base main HEAD (local)
167
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main (repoRoot)
168
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main (fallback, not used)
169
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD (worktree)
170
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // mainline diff — no files
171
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // worktree diff — no files
172
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: mainline patch diff
173
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: worktree patch diff
174
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // status --porcelain
175
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'not-ff' })) // ff merge fails
176
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // rebase succeeds
177
+
178
+ const svc = makeSvc();
179
+ const result = await svc.reconcileWithMainline('feat1', 'main');
180
+
181
+ expect(result.ok).toBe(true);
182
+ if (!result.ok) {
183
+ return;
184
+ }
185
+ expect(result.data.status).toBe('merged');
186
+ });
187
+
188
+ it('GIVEN_ff_and_rebase_both_fail_WHEN_reconcile_THEN_returns_reconciliation_failed', async () => {
189
+ runGitMock
190
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
191
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base main HEAD (local)
192
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main (repoRoot)
193
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main (fallback, not used)
194
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD (worktree)
195
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // mainline diff
196
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // worktree diff
197
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: mainline patch diff
198
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: worktree patch diff
199
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // status --porcelain
200
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'not-ff' })) // ff fails
201
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'conflicts' })) // rebase fails
202
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // rebase --abort
203
+
204
+ const svc = makeSvc();
205
+ const result = await svc.reconcileWithMainline('feat1', 'main');
206
+
207
+ expect(result.ok).toBe(false);
208
+ if (result.ok) {
209
+ return;
210
+ }
211
+ expect((result as FailResponse).error.code).toBe('reconciliation_failed');
212
+ });
213
+
214
+ it('GIVEN_dirty_feature_worktree_WHEN_reconcile_requires_auto_merge_THEN_returns_uncommitted_changes', async () => {
215
+ runGitMock
216
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
217
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base main HEAD (local)
218
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main (repoRoot)
219
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main
220
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD (worktree)
221
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'fileA.ts\n' })) // mainline diff
222
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'fileB.ts\n' })) // worktree diff
223
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: mainline patch diff
224
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: worktree patch diff
225
+ .mockResolvedValueOnce(
226
+ cmdResult({
227
+ code: 0,
228
+ stdout:
229
+ ' M packages/web-dashboard/src/lib/planner-workspace.ts\n?? packages/web-dashboard/src/components/focus/plan/CriticalPathPanel.tsx\n',
230
+ }),
231
+ ); // status --porcelain
232
+
233
+ const svc = makeSvc();
234
+ const result = await svc.reconcileWithMainline('feat1', 'main');
235
+
236
+ expect(result.ok).toBe(false);
237
+ if (result.ok) {
238
+ return;
239
+ }
240
+ expect((result as FailResponse).error.code).toBe('uncommitted_changes');
241
+ expect((result as FailResponse).error.details).toMatchObject({
242
+ worktree_path: '/repo/.worktrees/feat1',
243
+ files: [
244
+ 'packages/web-dashboard/src/components/focus/plan/CriticalPathPanel.tsx',
245
+ 'packages/web-dashboard/src/lib/planner-workspace.ts',
246
+ ],
247
+ });
248
+ });
249
+
250
+ it('GIVEN_dirty_feature_worktree_and_checkpoint_message_WHEN_reconcile_called_THEN_commits_changes_before_auto_merge', async () => {
251
+ runGitMock
252
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
253
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base main HEAD (local)
254
+ .mockResolvedValueOnce(
255
+ cmdResult({
256
+ code: 0,
257
+ stdout:
258
+ ' M packages/web-dashboard/src/lib/planner-workspace.ts\n?? packages/web-dashboard/src/components/focus/plan/CriticalPathPanel.tsx\n',
259
+ }),
260
+ ) // status --porcelain (checkpoint)
261
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // add -A
262
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '[feature 123] checkpoint\n' })) // commit
263
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main (repoRoot)
264
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main
265
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat222\n' })) // rev-parse HEAD (worktree)
266
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'fileA.ts\n' })) // mainline diff
267
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'fileB.ts\n' })) // worktree diff
268
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: mainline patch diff
269
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: worktree patch diff
270
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // status --porcelain (auto-merge)
271
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // ff merge succeeds
272
+
273
+ const svc = makeSvc();
274
+ const result = await svc.reconcileWithMainline('feat1', 'main', 'Checkpoint before merge');
275
+
276
+ expect(result.ok).toBe(true);
277
+ if (!result.ok) {
278
+ return;
279
+ }
280
+ expect(result.data.status).toBe('merged');
281
+ expect(runGitMock).toHaveBeenCalledWith('/repo/.worktrees/feat1', ['add', '-A']);
282
+ expect(runGitMock).toHaveBeenCalledWith('/repo/.worktrees/feat1', [
283
+ 'commit',
284
+ '-m',
285
+ 'Checkpoint before merge',
286
+ ]);
287
+ });
288
+
289
+ it('GIVEN_conflicting_files_WHEN_reconcile_THEN_returns_conflict_detected', async () => {
290
+ runGitMock
291
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
292
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base main HEAD (local)
293
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main (repoRoot)
294
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main (fallback, not used)
295
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD (worktree)
296
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/core/kernel.ts\nsrc/utils.ts\n' })) // mainline diff
297
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/core/kernel.ts\nsrc/feature.ts\n' })) // worktree diff
298
+ // evidence writes
299
+ .mockResolvedValueOnce(cmdResult({ code: 0 }))
300
+ .mockResolvedValueOnce(cmdResult({ code: 0 }))
301
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: diff --name-only --diff-filter=U (no stale merge)
302
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge --no-commit --no-ff
303
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/core/kernel.ts\n' })); // diff --name-only --diff-filter=U
304
+
305
+ const svc = makeSvc();
306
+ const result = await svc.reconcileWithMainline('feat1', 'main');
307
+
308
+ expect(result.ok).toBe(true);
309
+ if (!result.ok) {
310
+ return;
311
+ }
312
+ expect(result.data.status).toBe('conflict_detected');
313
+ expect(result.data.divergence_detected).toBe(true);
314
+ expect(result.data.conflicting_files).toEqual(['src/core/kernel.ts']);
315
+ expect(result.data.files_changed_mainline).toContain('src/utils.ts');
316
+ expect(result.data.files_changed_worktree).toContain('src/feature.ts');
317
+ });
318
+
319
+ it('GIVEN_idle_reconciler_lane_WHEN_conflict_detected_THEN_dispatches_head_of_line_feature', async () => {
320
+ const sendMessageMock = vi.fn(async () => undefined);
321
+ const writeRunLeaseMock = vi.fn(async () => undefined);
322
+ runGitMock
323
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
324
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base
325
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main
326
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main
327
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD
328
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })) // mainline diff
329
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })) // worktree diff
330
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // evidence mainline
331
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // evidence worktree
332
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: diff --name-only --diff-filter=U (no stale merge)
333
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge --no-commit --no-ff
334
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })); // diff --name-only --diff-filter=U
335
+
336
+ const svc = makeSvc({
337
+ writeRunLease: writeRunLeaseMock,
338
+ getProvider: vi.fn(() => ({
339
+ sendMessage: sendMessageMock,
340
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'codex' })),
341
+ })),
342
+ });
343
+ const result = await svc.reconcileWithMainline('feat1', 'main');
344
+
345
+ expect(result.ok).toBe(true);
346
+ if (!result.ok) {
347
+ return;
348
+ }
349
+ expect(result.data.status).toBe('conflict_detected');
350
+ expect(result.data.reconciler_active_feature_id).toBe('feat1');
351
+ expect(result.data.reconciler_queue_position).toBe(0);
352
+ expect(result.data.dispatched_to_reconciler).toBe(true);
353
+ expect(writeRunLeaseMock).toHaveBeenCalledWith(
354
+ expect.objectContaining({
355
+ reconciler_active_feature_id: 'feat1',
356
+ reconciler_queue: [],
357
+ }),
358
+ );
359
+ expect(sendMessageMock).toHaveBeenCalledWith(
360
+ 'reconciler:1',
361
+ expect.stringContaining('feature_id=feat1'),
362
+ );
363
+ });
364
+
365
+ it('GIVEN_reconciler_send_failure_WHEN_conflict_detected_THEN_returns_conflict_without_bubbling_internal_error', async () => {
366
+ const sendMessageMock = vi.fn(async () => {
367
+ throw new Error('claude provider send failed');
368
+ });
369
+ runGitMock
370
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
371
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base
372
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main
373
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main
374
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD
375
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })) // mainline diff
376
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })) // worktree diff
377
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // evidence mainline
378
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // evidence worktree
379
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: diff --name-only --diff-filter=U (no stale merge)
380
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge --no-commit --no-ff
381
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })); // diff --name-only --diff-filter=U
382
+
383
+ const svc = makeSvc({
384
+ getProvider: vi.fn(() => ({
385
+ sendMessage: sendMessageMock,
386
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'claude' })),
387
+ })),
388
+ });
389
+ const result = await svc.reconcileWithMainline('feat1', 'main');
390
+
391
+ expect(result.ok).toBe(true);
392
+ if (!result.ok) {
393
+ return;
394
+ }
395
+ expect(result.data.status).toBe('conflict_detected');
396
+ expect(result.data.dispatched_to_reconciler).toBe(false);
397
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
398
+ expect.stringContaining('reconciliation.log'),
399
+ expect.stringContaining('dispatch_failed: claude provider send failed'),
400
+ 'utf8',
401
+ );
402
+ });
403
+
404
+ it('GIVEN_approved_collision_context_WHEN_conflict_detected_THEN_includes_override_metadata_in_dispatch', async () => {
405
+ const sendMessageMock = vi.fn(async () => undefined);
406
+ runGitMock
407
+ .mockResolvedValueOnce(cmdResult({ code: 0 }))
408
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' }))
409
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' }))
410
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' }))
411
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' }))
412
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' }))
413
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' }))
414
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' }))
415
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' }))
416
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: no unmerged files
417
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge --no-commit --no-ff
418
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })); // diff --name-only --diff-filter=U
419
+
420
+ const svc = makeSvc({
421
+ readIndex: vi.fn(async () => ({
422
+ collision_overrides: [
423
+ {
424
+ override_id: 'colovr_1',
425
+ feature_ids: ['feat1', 'feat2'],
426
+ rationale: 'preserve both dashboard behaviors',
427
+ approved_paths: ['shared.ts'],
428
+ approved_collision_fingerprint: 'fp_1',
429
+ status: 'applied',
430
+ source_collision_id: 'colrec_1',
431
+ applied_at: '2026-03-15T00:00:00.000Z',
432
+ },
433
+ ],
434
+ collision_records: [
435
+ {
436
+ collision_id: 'colrec_1',
437
+ detected_at: '2026-03-15T00:00:00.000Z',
438
+ },
439
+ ],
440
+ })),
441
+ getProvider: vi.fn(() => ({
442
+ sendMessage: sendMessageMock,
443
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'codex' })),
444
+ })),
445
+ });
446
+
447
+ const result = await svc.reconcileWithMainline('feat1', 'main');
448
+
449
+ expect(result.ok).toBe(true);
450
+ expect(sendMessageMock).toHaveBeenCalledWith(
451
+ 'reconciler:1',
452
+ expect.stringContaining('"override_id":"colovr_1"'),
453
+ );
454
+ expect(sendMessageMock).toHaveBeenCalledWith(
455
+ 'reconciler:1',
456
+ expect.stringContaining('"counterpart_feature_id":"feat2"'),
457
+ );
458
+ expect(sendMessageMock).toHaveBeenCalledWith(
459
+ 'reconciler:1',
460
+ expect.stringContaining('"source_collision_record":{"collision_id":"colrec_1"'),
461
+ );
462
+ });
463
+
464
+ it('GIVEN_busy_reconciler_lane_WHEN_conflict_detected_THEN_enqueues_feature_without_dispatch', async () => {
465
+ const writeRunLeaseMock = vi.fn(async () => undefined);
466
+ runGitMock
467
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
468
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base
469
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main
470
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main
471
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD
472
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })) // mainline diff
473
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })) // worktree diff
474
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // evidence mainline
475
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // evidence worktree
476
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: diff --name-only --diff-filter=U (no stale merge)
477
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge --no-commit --no-ff
478
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\n' })); // diff --name-only --diff-filter=U
479
+
480
+ const svc = makeSvc({
481
+ readRunLease: vi.fn(
482
+ async () =>
483
+ ({
484
+ run_id: 'run:test',
485
+ orchestrator_session_id: 'orchestrator:1',
486
+ reconciler_session_id: 'reconciler:1',
487
+ reconciler_active_feature_id: 'feature_busy',
488
+ reconciler_queue: ['feature_waiting'],
489
+ provider: 'codex',
490
+ model: 'codex-default',
491
+ provider_config_ref_hash: 'hash',
492
+ owner_instance_id: 'owner:test',
493
+ lease_id: 'lease:test',
494
+ started_at: '2026-01-01T00:00:00.000Z',
495
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
496
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
497
+ feature_sessions: {},
498
+ }) satisfies RuntimeSessionsSnapshot,
499
+ ),
500
+ writeRunLease: writeRunLeaseMock,
501
+ });
502
+ const result = await svc.reconcileWithMainline('feat1', 'main');
503
+
504
+ expect(result.ok).toBe(true);
505
+ if (!result.ok) {
506
+ return;
507
+ }
508
+ expect(result.data.status).toBe('conflict_detected');
509
+ expect(result.data.reconciler_active_feature_id).toBe('feature_busy');
510
+ expect(result.data.reconciler_queue_position).toBe(2);
511
+ expect(result.data.dispatched_to_reconciler).toBe(false);
512
+ expect(writeRunLeaseMock).toHaveBeenCalledWith(
513
+ expect.objectContaining({
514
+ reconciler_active_feature_id: 'feature_busy',
515
+ reconciler_queue: ['feature_waiting', 'feat1'],
516
+ }),
517
+ );
518
+ });
519
+
520
+ it('GIVEN_merge_base_not_found_WHEN_reconcile_THEN_returns_merge_base_not_found_error', async () => {
521
+ runGitMock
522
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
523
+ .mockResolvedValueOnce(cmdResult({ code: 128, stderr: 'fatal: no merge base' })); // merge-base local fails
524
+
525
+ const svc = makeSvc();
526
+ const result = await svc.reconcileWithMainline('feat1', 'main');
527
+
528
+ expect(result.ok).toBe(false);
529
+ if (result.ok) {
530
+ return;
531
+ }
532
+ expect((result as FailResponse).error.code).toBe('merge_base_not_found');
533
+ });
534
+
535
+ it('GIVEN_empty_merge_base_sha_WHEN_reconcile_THEN_returns_merge_base_not_found_error', async () => {
536
+ runGitMock
537
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
538
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: ' \n' })); // local merge-base → whitespace → empty
539
+
540
+ const svc = makeSvc();
541
+ const result = await svc.reconcileWithMainline('feat1', 'main');
542
+
543
+ // Local fallback returns empty mergeBase → fail
544
+ expect(result.ok).toBe(false);
545
+ if (result.ok) {
546
+ return;
547
+ }
548
+ expect((result as FailResponse).error.code).toBe('merge_base_not_found');
549
+ });
550
+
551
+ it('GIVEN_state_read_throws_WHEN_reconcile_THEN_up_to_date_result_still_returned', async () => {
552
+ const mergeBase = 'sha_base';
553
+ runGitMock
554
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
555
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${mergeBase}\n` })) // merge-base main HEAD (local)
556
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${mergeBase}\n` })) // rev-parse main (repoRoot) → mainlineHead == mergeBase
557
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin_base\n' })) // rev-parse origin/main (fallback, not used)
558
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat999\n' })); // rev-parse HEAD
559
+
560
+ const port = makePort({
561
+ readState: vi.fn(async () => {
562
+ throw new Error('disk error');
563
+ }),
564
+ });
565
+ const svc = new GitReconciliationService(port);
566
+ const result = await svc.reconcileWithMainline('feat1', 'main');
567
+
568
+ // Non-fatal: state write failure should not break reconciliation
569
+ expect(result.ok).toBe(true);
570
+ if (!result.ok) {
571
+ return;
572
+ }
573
+ expect(result.data.status).toBe('up_to_date');
574
+ });
575
+
576
+ it('GIVEN_conflict_detected_WHEN_reconcile_THEN_state_is_updated_with_conflict_metadata', async () => {
577
+ const writeStateMock = vi.fn(async () => {});
578
+ runGitMock
579
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
580
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base main HEAD (local)
581
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main999\n' })) // rev-parse main (repoRoot)
582
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin999\n' })) // rev-parse origin/main (fallback, not used)
583
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse HEAD (worktree)
584
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'conflict.ts\n' })) // mainline diff
585
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'conflict.ts\n' })) // worktree diff
586
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // evidence
587
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // evidence
588
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: diff --name-only --diff-filter=U (no stale merge)
589
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge --no-commit --no-ff
590
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'conflict.ts\n' })); // diff --name-only --diff-filter=U
591
+
592
+ const svc = makeSvc({ writeState: writeStateMock });
593
+ await svc.reconcileWithMainline('feat1', 'main');
594
+
595
+ expect(writeStateMock).toHaveBeenCalled();
596
+ const callArgs = writeStateMock.mock.calls[0] as unknown as [string, Record<string, unknown>];
597
+ const patchedFrontMatter = callArgs[1];
598
+ expect(Array.isArray(patchedFrontMatter.conflicts)).toBe(true);
599
+ const conflict = (patchedFrontMatter.conflicts as Record<string, unknown>[])[0];
600
+ expect(conflict.type).toBe('mainline_divergence');
601
+ expect(conflict.conflicting_files).toEqual(['conflict.ts']);
602
+ expect(conflict.resolution_status).toBe('in_progress');
603
+ expect(patchedFrontMatter.status).toBe('blocked');
604
+ });
605
+
606
+ it('GIVEN_fetch_fails_WHEN_reconcile_THEN_continues_with_local_refs', async () => {
607
+ const mergeBase = 'localbase';
608
+ runGitMock
609
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'no remote' })) // fetch fails (non-fatal)
610
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${mergeBase}\n` })) // merge-base main HEAD (local)
611
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${mergeBase}\n` })) // rev-parse main (repoRoot) → mainlineHead == mergeBase
612
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin_base\n' })) // rev-parse origin/main (fallback, not used)
613
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'wt123\n' })); // rev-parse HEAD
614
+
615
+ const svc = makeSvc();
616
+ const result = await svc.reconcileWithMainline('feat1', 'main');
617
+
618
+ expect(result.ok).toBe(true);
619
+ if (!result.ok) {
620
+ return;
621
+ }
622
+ expect(result.data.status).toBe('up_to_date');
623
+ });
624
+
625
+ it('GIVEN_local_mainline_ref_missing_WHEN_reconcile_THEN_falls_back_to_origin_and_treats_matching_head_as_up_to_date', async () => {
626
+ runGitMock
627
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
628
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base
629
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'missing main' })) // rev-parse main
630
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })) // rev-parse origin/main
631
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })); // rev-parse HEAD
632
+
633
+ const svc = makeSvc();
634
+ const result = await svc.reconcileWithMainline('feat1', 'main');
635
+
636
+ expect(result.ok).toBe(true);
637
+ if (!result.ok) {
638
+ return;
639
+ }
640
+ expect(result.data.status).toBe('up_to_date');
641
+ expect(result.data.mainline_head).toBe('feat111');
642
+ });
643
+ });
644
+
645
+ // ---------------------------------------------------------------------------
646
+ // detectDivergence
647
+ // ---------------------------------------------------------------------------
648
+
649
+ describe('GitReconciliationService.detectDivergence', () => {
650
+ beforeEach(() => {
651
+ vi.resetAllMocks();
652
+ nowIsoMock.mockReturnValue('2026-01-01T00:00:00.000Z');
653
+ });
654
+
655
+ it('GIVEN_no_divergence_WHEN_detect_THEN_returns_up_to_date', async () => {
656
+ const sha = 'samesha';
657
+ runGitMock
658
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
659
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // merge-base main HEAD (local)
660
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // rev-parse main (repoRoot) → mainlineHead == mergeBase
661
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin_sha\n' })) // rev-parse origin/main (fallback, not used)
662
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat555\n' })); // rev-parse HEAD
663
+
664
+ const svc = makeSvc();
665
+ const result = await svc.detectDivergence('feat1', 'main');
666
+
667
+ expect(result.ok).toBe(true);
668
+ if (!result.ok) {
669
+ return;
670
+ }
671
+ expect(result.data.status).toBe('up_to_date');
672
+ expect(result.data.divergence_detected).toBe(false);
673
+ });
674
+
675
+ it('GIVEN_non_conflicting_divergence_WHEN_detect_THEN_returns_merged_status_without_mutating', async () => {
676
+ runGitMock
677
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
678
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'mergebase\n' })) // merge-base main HEAD (local)
679
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'mainhead\n' })) // rev-parse main (repoRoot)
680
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin_head\n' })) // rev-parse origin/main (fallback, not used)
681
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'worktreehead\n' })) // rev-parse HEAD
682
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'readme.md\n' })) // mainline diff files
683
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/feature.ts\n' })); // worktree diff files
684
+
685
+ const svc = makeSvc();
686
+ const result = await svc.detectDivergence('feat1', 'main');
687
+
688
+ expect(result.ok).toBe(true);
689
+ if (!result.ok) {
690
+ return;
691
+ }
692
+ // detectDivergence returns 'merged' to indicate "would be mergeable"
693
+ expect(result.data.status).toBe('merged');
694
+ expect(result.data.conflicting_files).toHaveLength(0);
695
+ // Should NOT have called writeState (no mutation)
696
+ });
697
+
698
+ it('GIVEN_conflicting_divergence_WHEN_detect_THEN_returns_conflict_detected', async () => {
699
+ runGitMock
700
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
701
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'mergebase\n' })) // merge-base main HEAD (local)
702
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'mainhead\n' })) // rev-parse main (repoRoot)
703
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin_head\n' })) // rev-parse origin/main (fallback, not used)
704
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'worktreehead\n' })) // rev-parse HEAD
705
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\nonly_main.ts\n' })) // mainline diff
706
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'shared.ts\nonly_feat.ts\n' })); // worktree diff
707
+
708
+ const svc = makeSvc();
709
+ const result = await svc.detectDivergence('feat1', 'main');
710
+
711
+ expect(result.ok).toBe(true);
712
+ if (!result.ok) {
713
+ return;
714
+ }
715
+ expect(result.data.status).toBe('conflict_detected');
716
+ expect(result.data.conflicting_files).toEqual(['shared.ts']);
717
+ });
718
+
719
+ it('GIVEN_merge_base_fails_for_both_remote_and_local_WHEN_detect_THEN_returns_error', async () => {
720
+ runGitMock
721
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
722
+ .mockResolvedValueOnce(cmdResult({ code: 128, stderr: 'fatal' })); // merge-base local fails
723
+
724
+ const svc = makeSvc();
725
+ const result = await svc.detectDivergence('feat1', 'main');
726
+
727
+ expect(result.ok).toBe(false);
728
+ if (result.ok) {
729
+ return;
730
+ }
731
+ expect((result as FailResponse).error.code).toBe('merge_base_not_found');
732
+ });
733
+ });
734
+
735
+ // ---------------------------------------------------------------------------
736
+ // Evidence artifacts
737
+ // ---------------------------------------------------------------------------
738
+
739
+ describe('GitReconciliationService — evidence artifacts', () => {
740
+ beforeEach(() => {
741
+ vi.resetAllMocks();
742
+ nowIsoMock.mockReturnValue('2026-01-01T00:00:00.000Z');
743
+ });
744
+
745
+ it('GIVEN_conflict_detected_WHEN_reconcile_THEN_evidence_dir_is_created', async () => {
746
+ runGitMock
747
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
748
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base\n' })) // merge-base main HEAD (local)
749
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main\n' })) // rev-parse main (repoRoot)
750
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin\n' })) // rev-parse origin/main (fallback, not used)
751
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat\n' })) // rev-parse HEAD (worktree)
752
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'conflict.ts\n' })) // mainline diff
753
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'conflict.ts\n' })) // worktree diff
754
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // mainline diff patch
755
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'diff content' })) // worktree diff patch
756
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: diff --name-only --diff-filter=U (no stale merge)
757
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge --no-commit --no-ff
758
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'conflict.ts\n' })); // diff --name-only --diff-filter=U
759
+
760
+ const svc = makeSvc();
761
+ await svc.reconcileWithMainline('feat1', 'main');
762
+
763
+ expect(ensureDirMock).toHaveBeenCalledWith(expect.stringContaining('reconciliation'));
764
+ expect(fsWriteFileMock).toHaveBeenCalledWith(
765
+ expect.stringContaining('mainline-diff.patch'),
766
+ expect.any(String),
767
+ 'utf8',
768
+ );
769
+ });
770
+
771
+ it('GIVEN_merged_successfully_WHEN_reconcile_THEN_log_is_appended', async () => {
772
+ runGitMock
773
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
774
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base\n' })) // merge-base main HEAD (local)
775
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'main\n' })) // rev-parse main (repoRoot)
776
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin\n' })) // rev-parse origin/main (fallback, not used)
777
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat\n' })) // rev-parse HEAD (worktree)
778
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // mainline diff (no files)
779
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // worktree diff (no files)
780
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: mainline patch
781
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // evidence: worktree patch
782
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // status --porcelain
783
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // ff merge succeeds
784
+
785
+ const svc = makeSvc();
786
+ await svc.reconcileWithMainline('feat1', 'main');
787
+
788
+ expect(ensureDirMock).toHaveBeenCalledWith(expect.stringContaining('logs'));
789
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
790
+ expect.stringContaining('reconciliation.log'),
791
+ expect.stringContaining('merged'),
792
+ 'utf8',
793
+ );
794
+ });
795
+ });
796
+
797
+ // ---------------------------------------------------------------------------
798
+ // Additional branch coverage
799
+ // ---------------------------------------------------------------------------
800
+
801
+ describe('GitReconciliationService — additional branches', () => {
802
+ beforeEach(() => {
803
+ vi.resetAllMocks();
804
+ nowIsoMock.mockReturnValue('2026-01-01T00:00:00.000Z');
805
+ });
806
+
807
+ it('GIVEN_origin_rev_parse_fails_WHEN_reconcile_THEN_uses_local_ref_for_mainline_head', async () => {
808
+ const sha = 'localsha';
809
+ runGitMock
810
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
811
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // merge-base main HEAD (local)
812
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // rev-parse main (repoRoot) → mainlineHead == mergeBase
813
+ .mockResolvedValueOnce(cmdResult({ code: 128, stderr: 'no origin' })) // rev-parse origin/main FAILS (fallback not used since local succeeded)
814
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'wt333\n' })); // rev-parse HEAD
815
+
816
+ const svc = makeSvc();
817
+ const result = await svc.reconcileWithMainline('feat1', 'main');
818
+
819
+ // mainlineHead (localsha) === mergeBase (localsha) → up_to_date
820
+ expect(result.ok).toBe(true);
821
+ if (!result.ok) {
822
+ return;
823
+ }
824
+ expect(result.data.status).toBe('up_to_date');
825
+ });
826
+
827
+ it('GIVEN_origin_rev_parse_fails_in_detect_WHEN_detect_THEN_uses_local_ref', async () => {
828
+ const sha = 'localbase';
829
+ runGitMock
830
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
831
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // merge-base main HEAD (local)
832
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // rev-parse main (repoRoot) → mainlineHead == mergeBase
833
+ .mockResolvedValueOnce(cmdResult({ code: 128, stderr: 'no origin' })) // rev-parse origin/main FAILS (fallback not used)
834
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'wt_head\n' })); // rev-parse HEAD
835
+
836
+ const svc = makeSvc();
837
+ const result = await svc.detectDivergence('feat1', 'main');
838
+
839
+ expect(result.ok).toBe(true);
840
+ if (!result.ok) {
841
+ return;
842
+ }
843
+ // mainlineHead (localbase) === mergeBase (localbase) → up_to_date
844
+ expect(result.data.status).toBe('up_to_date');
845
+ });
846
+
847
+ it('GIVEN_local_merge_base_fallback_in_detect_WHEN_local_also_fails_THEN_returns_error', async () => {
848
+ runGitMock
849
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
850
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'no local' })); // merge-base local fails
851
+
852
+ const svc = makeSvc();
853
+ const result = await svc.detectDivergence('feat1', 'main');
854
+
855
+ expect(result.ok).toBe(false);
856
+ if (result.ok) {
857
+ return;
858
+ }
859
+ expect((result as FailResponse).error.code).toBe('merge_base_not_found');
860
+ });
861
+
862
+ it('GIVEN_local_merge_base_fallback_in_detect_WHEN_local_succeeds_THEN_continues_with_local_base', async () => {
863
+ const sha = 'localbase';
864
+ runGitMock
865
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
866
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // merge-base local succeeds
867
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: `${sha}\n` })) // rev-parse main (repoRoot) → mainlineHead == mergeBase
868
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin_sha\n' })) // rev-parse origin/main (fallback, not used)
869
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'wt_h\n' })); // rev-parse HEAD
870
+
871
+ const svc = makeSvc();
872
+ const result = await svc.detectDivergence('feat1', 'main');
873
+
874
+ expect(result.ok).toBe(true);
875
+ if (!result.ok) {
876
+ return;
877
+ }
878
+ expect(result.data.status).toBe('up_to_date');
879
+ });
880
+
881
+ it('GIVEN_local_mainline_ref_missing_WHEN_detect_THEN_falls_back_to_origin_ref', async () => {
882
+ runGitMock
883
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
884
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'mergebase\n' })) // merge-base
885
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'missing main' })) // rev-parse main
886
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin-main\n' })) // rev-parse origin/main
887
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'worktreehead\n' })) // rev-parse HEAD
888
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/main.ts\n' })) // mainline diff
889
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/feature.ts\n' })); // worktree diff
890
+
891
+ const svc = makeSvc();
892
+ const result = await svc.detectDivergence('feat1', 'main');
893
+
894
+ expect(result.ok).toBe(true);
895
+ if (!result.ok) {
896
+ return;
897
+ }
898
+ expect(result.data.status).toBe('merged');
899
+ expect(result.data.mainline_head).toBe('origin-main');
900
+ });
901
+ });
902
+
903
+ // ---------------------------------------------------------------------------
904
+ // checkRemoteLocalDivergence
905
+ // ---------------------------------------------------------------------------
906
+ describe('GitReconciliationService.checkRemoteLocalDivergence', () => {
907
+ beforeEach(() => {
908
+ runGitMock.mockReset();
909
+ });
910
+
911
+ it('GIVEN_local_and_remote_same_sha_WHEN_check_divergence_THEN_returns_not_diverged', async () => {
912
+ runGitMock
913
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
914
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'abc123\n' })) // rev-parse main (local)
915
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'abc123\n' })); // rev-parse origin/main
916
+
917
+ const svc = makeSvc();
918
+ const result = await svc.checkRemoteLocalDivergence('main');
919
+
920
+ expect(result.ok).toBe(true);
921
+ if (!result.ok) {
922
+ return;
923
+ }
924
+ expect(result.data.diverged).toBe(false);
925
+ expect(result.data.local_ahead).toBe(false);
926
+ expect(result.data.local_head).toBe('abc123');
927
+ expect(result.data.remote_head).toBe('abc123');
928
+ });
929
+
930
+ it('GIVEN_local_and_remote_different_sha_WHEN_check_divergence_THEN_returns_diverged', async () => {
931
+ runGitMock
932
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
933
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'localsha\n' })) // rev-parse main (local)
934
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'remotesha\n' })) // rev-parse origin/main
935
+ .mockResolvedValueOnce(cmdResult({ code: 1 })); // merge-base --is-ancestor exits 1
936
+
937
+ const svc = makeSvc();
938
+ const result = await svc.checkRemoteLocalDivergence('main');
939
+
940
+ expect(result.ok).toBe(true);
941
+ if (!result.ok) {
942
+ return;
943
+ }
944
+ expect(result.data.diverged).toBe(true);
945
+ expect(result.data.local_ahead).toBe(false);
946
+ expect(result.data.local_head).toBe('localsha');
947
+ expect(result.data.remote_head).toBe('remotesha');
948
+ });
949
+
950
+ it('GIVEN_no_remote_WHEN_check_divergence_THEN_returns_not_diverged_using_local_sha', async () => {
951
+ runGitMock
952
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch (non-fatal)
953
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'localsha\n' })) // rev-parse main (local)
954
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'unknown revision' })); // rev-parse origin/main fails
955
+
956
+ const svc = makeSvc();
957
+ const result = await svc.checkRemoteLocalDivergence('main');
958
+
959
+ expect(result.ok).toBe(true);
960
+ if (!result.ok) {
961
+ return;
962
+ }
963
+ expect(result.data.diverged).toBe(false);
964
+ expect(result.data.local_ahead).toBe(false);
965
+ expect(result.data.local_head).toBe('localsha');
966
+ expect(result.data.remote_head).toBe('localsha');
967
+ });
968
+
969
+ it('GIVEN_identical_shas_WHEN_check_divergence_THEN_skips_merge_base_call', async () => {
970
+ runGitMock
971
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
972
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'abc123\n' })) // rev-parse main
973
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'abc123\n' })); // rev-parse origin/main
974
+
975
+ const svc = makeSvc();
976
+ const result = await svc.checkRemoteLocalDivergence('main');
977
+
978
+ expect(result.ok).toBe(true);
979
+ if (!result.ok) {
980
+ return;
981
+ }
982
+ expect(result.data.diverged).toBe(false);
983
+ expect(result.data.local_ahead).toBe(false);
984
+ // Should NOT call merge-base when SHAs are identical
985
+ expect(runGitMock).toHaveBeenCalledTimes(3);
986
+ });
987
+
988
+ it('GIVEN_local_ahead_of_remote_WHEN_check_divergence_THEN_returns_not_diverged_and_local_ahead', async () => {
989
+ runGitMock
990
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
991
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'localsha\n' })) // rev-parse main
992
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'remotesha\n' })) // rev-parse origin/main
993
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // merge-base --is-ancestor exits 0
994
+
995
+ const svc = makeSvc();
996
+ const result = await svc.checkRemoteLocalDivergence('main');
997
+
998
+ expect(result.ok).toBe(true);
999
+ if (!result.ok) {
1000
+ return;
1001
+ }
1002
+ expect(result.data.diverged).toBe(false);
1003
+ expect(result.data.local_ahead).toBe(true);
1004
+ expect(runGitMock).toHaveBeenCalledWith(expect.any(String), [
1005
+ 'merge-base',
1006
+ '--is-ancestor',
1007
+ 'origin/main',
1008
+ 'main',
1009
+ ]);
1010
+ });
1011
+
1012
+ it('GIVEN_true_divergence_WHEN_check_divergence_THEN_returns_diverged', async () => {
1013
+ runGitMock
1014
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
1015
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'localsha\n' })) // rev-parse main
1016
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'remotesha\n' })) // rev-parse origin/main
1017
+ .mockResolvedValueOnce(cmdResult({ code: 1 })); // merge-base --is-ancestor exits 1
1018
+
1019
+ const svc = makeSvc();
1020
+ const result = await svc.checkRemoteLocalDivergence('main');
1021
+
1022
+ expect(result.ok).toBe(true);
1023
+ if (!result.ok) {
1024
+ return;
1025
+ }
1026
+ expect(result.data.diverged).toBe(true);
1027
+ expect(result.data.local_ahead).toBe(false);
1028
+ });
1029
+
1030
+ it('GIVEN_remote_ref_unresolvable_WHEN_check_divergence_THEN_returns_not_diverged', async () => {
1031
+ runGitMock
1032
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
1033
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'localsha\n' })) // rev-parse main
1034
+ .mockResolvedValueOnce(cmdResult({ code: 128, stderr: 'fatal: bad ref' })); // rev-parse origin/main fails
1035
+
1036
+ const svc = makeSvc();
1037
+ const result = await svc.checkRemoteLocalDivergence('main');
1038
+
1039
+ expect(result.ok).toBe(true);
1040
+ if (!result.ok) {
1041
+ return;
1042
+ }
1043
+ expect(result.data.diverged).toBe(false);
1044
+ expect(result.data.local_ahead).toBe(false);
1045
+ expect(result.data.remote_head).toBe('localsha');
1046
+ // Should NOT call merge-base when remote ref fails
1047
+ expect(runGitMock).toHaveBeenCalledTimes(3);
1048
+ });
1049
+
1050
+ it('GIVEN_local_rev_parse_fails_WHEN_check_divergence_THEN_returns_git_failure', async () => {
1051
+ runGitMock
1052
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
1053
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'unknown revision main' })); // rev-parse main fails
1054
+
1055
+ const svc = makeSvc();
1056
+ const result = await svc.checkRemoteLocalDivergence('main');
1057
+
1058
+ expect(result.ok).toBe(false);
1059
+ if (result.ok) {
1060
+ return;
1061
+ }
1062
+ expect((result as unknown as FailResponse).error.code).toBe('git_failure');
1063
+ });
1064
+ });
1065
+
1066
+ // ---------------------------------------------------------------------------
1067
+ // syncMainlineFromRemote
1068
+ // ---------------------------------------------------------------------------
1069
+ describe('GitReconciliationService.syncMainlineFromRemote', () => {
1070
+ beforeEach(() => {
1071
+ runGitMock.mockReset();
1072
+ });
1073
+
1074
+ it('GIVEN_no_conflicts_WHEN_sync_THEN_merges_and_returns_sha', async () => {
1075
+ runGitMock
1076
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
1077
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // checkout main
1078
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // merge -X ours origin/main
1079
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'merged123\n' })); // rev-parse HEAD
1080
+
1081
+ const svc = makeSvc();
1082
+ const result = await svc.syncMainlineFromRemote('main');
1083
+
1084
+ expect(result.ok).toBe(true);
1085
+ if (!result.ok) {
1086
+ return;
1087
+ }
1088
+ expect(result.data.merged).toBe(true);
1089
+ expect(result.data.sha).toBe('merged123');
1090
+ });
1091
+
1092
+ it('GIVEN_fetch_fails_WHEN_sync_THEN_returns_git_failure', async () => {
1093
+ runGitMock.mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'network error' })); // fetch fails
1094
+
1095
+ const svc = makeSvc();
1096
+ const result = await svc.syncMainlineFromRemote('main');
1097
+
1098
+ expect(result.ok).toBe(false);
1099
+ if (result.ok) {
1100
+ return;
1101
+ }
1102
+ expect((result as unknown as FailResponse).error.code).toBe('git_failure');
1103
+ });
1104
+
1105
+ it('GIVEN_checkout_fails_WHEN_sync_THEN_returns_git_failure', async () => {
1106
+ runGitMock
1107
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
1108
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'checkout failed' })); // checkout main fails
1109
+
1110
+ const svc = makeSvc();
1111
+ const result = await svc.syncMainlineFromRemote('main');
1112
+
1113
+ expect(result.ok).toBe(false);
1114
+ if (result.ok) {
1115
+ return;
1116
+ }
1117
+ expect((result as unknown as FailResponse).error.code).toBe('git_failure');
1118
+ });
1119
+
1120
+ it('GIVEN_merge_fails_WHEN_sync_THEN_aborts_and_returns_reconciliation_failed', async () => {
1121
+ runGitMock
1122
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
1123
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // checkout main
1124
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge conflict' })) // merge -X ours fails
1125
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // merge --abort
1126
+
1127
+ const svc = makeSvc();
1128
+ const result = await svc.syncMainlineFromRemote('main');
1129
+
1130
+ expect(result.ok).toBe(false);
1131
+ if (result.ok) {
1132
+ return;
1133
+ }
1134
+ expect((result as unknown as FailResponse).error.code).toBe('reconciliation_failed');
1135
+ // Verify --abort was called
1136
+ expect(runGitMock).toHaveBeenCalledWith('/repo', ['merge', '--abort']);
1137
+ });
1138
+ });
1139
+
1140
+ // ---------------------------------------------------------------------------
1141
+ // Internal branch coverage
1142
+ // ---------------------------------------------------------------------------
1143
+ describe('GitReconciliationService — internal queue and state branches', () => {
1144
+ beforeEach(() => {
1145
+ vi.resetAllMocks();
1146
+ nowIsoMock.mockReturnValue('2026-01-01T00:00:00.000Z');
1147
+ });
1148
+
1149
+ it('GIVEN_existing_conflict_without_unmerged_files_WHEN_reuse_checked_THEN_returns_null', async () => {
1150
+ runGitMock.mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' }));
1151
+
1152
+ const svc = makeSvc({
1153
+ readState: vi.fn(async () => ({
1154
+ frontMatter: {
1155
+ feature_id: 'feat1',
1156
+ version: 1,
1157
+ status: 'blocked',
1158
+ conflicts: [{ type: 'mainline_divergence', resolution_status: 'pending' }],
1159
+ } as Record<string, unknown>,
1160
+ body: '# Feature\n',
1161
+ })),
1162
+ }) as unknown as {
1163
+ reuseExistingConflictIfPresent: (
1164
+ featureId: string,
1165
+ worktreePath: string,
1166
+ baseRef: string,
1167
+ ) => Promise<unknown>;
1168
+ };
1169
+
1170
+ const result = await svc.reuseExistingConflictIfPresent(
1171
+ 'feat1',
1172
+ '/repo/.worktrees/feat1',
1173
+ 'main',
1174
+ );
1175
+ expect(result).toBeNull();
1176
+ });
1177
+
1178
+ it('GIVEN_no_unresolved_mainline_conflict_WHEN_reuse_checked_THEN_returns_null', async () => {
1179
+ const svc = makeSvc({
1180
+ readState: vi.fn(async () => ({
1181
+ frontMatter: {
1182
+ feature_id: 'feat1',
1183
+ version: 1,
1184
+ status: 'blocked',
1185
+ conflicts: [{ type: 'mainline_divergence', resolution_status: 'resolved' }],
1186
+ } as Record<string, unknown>,
1187
+ body: '# Feature\n',
1188
+ })),
1189
+ }) as unknown as {
1190
+ reuseExistingConflictIfPresent: (
1191
+ featureId: string,
1192
+ worktreePath: string,
1193
+ baseRef: string,
1194
+ ) => Promise<unknown>;
1195
+ };
1196
+
1197
+ const result = await svc.reuseExistingConflictIfPresent(
1198
+ 'feat1',
1199
+ '/repo/.worktrees/feat1',
1200
+ 'main',
1201
+ );
1202
+ expect(result).toBeNull();
1203
+ });
1204
+
1205
+ it('GIVEN_existing_conflict_with_missing_metadata_WHEN_reuse_checked_THEN_defaults_to_unknown_values', async () => {
1206
+ const writeStateMock = vi.fn(async () => {});
1207
+ runGitMock.mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/shared.ts\n' }));
1208
+
1209
+ const svc = makeSvc({
1210
+ readState: vi.fn(async () => ({
1211
+ frontMatter: {
1212
+ feature_id: 'feat1',
1213
+ version: 1,
1214
+ status: 'building',
1215
+ conflicts: [{ type: 'mainline_divergence', resolution_status: 'pending' }],
1216
+ } as Record<string, unknown>,
1217
+ body: '# Feature\n',
1218
+ })),
1219
+ readRunLease: vi.fn(
1220
+ async () =>
1221
+ ({
1222
+ run_id: 'run:test',
1223
+ orchestrator_session_id: 'orchestrator:1',
1224
+ reconciler_session_id: 'unassigned',
1225
+ reconciler_active_feature_id: null,
1226
+ reconciler_queue: [],
1227
+ provider: 'codex',
1228
+ model: 'codex-default',
1229
+ provider_config_ref_hash: 'hash',
1230
+ owner_instance_id: 'owner:test',
1231
+ lease_id: 'lease:test',
1232
+ started_at: '2026-01-01T00:00:00.000Z',
1233
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
1234
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
1235
+ feature_sessions: {},
1236
+ }) satisfies RuntimeSessionsSnapshot,
1237
+ ),
1238
+ writeState: writeStateMock,
1239
+ }) as unknown as {
1240
+ reuseExistingConflictIfPresent: (
1241
+ featureId: string,
1242
+ worktreePath: string,
1243
+ baseRef: string,
1244
+ ) => Promise<{ ok: true; data: Record<string, unknown> } | { ok: false } | null>;
1245
+ };
1246
+
1247
+ const result = await svc.reuseExistingConflictIfPresent(
1248
+ 'feat1',
1249
+ '/repo/.worktrees/feat1',
1250
+ 'main',
1251
+ );
1252
+ expect(result).not.toBeNull();
1253
+ expect(result?.ok).toBe(true);
1254
+ if (!result || !result.ok) {
1255
+ return;
1256
+ }
1257
+ expect(result.data.merge_base).toBe('unknown');
1258
+ expect(result.data.mainline_head).toBe('unknown');
1259
+ expect(result.data.worktree_head).toBe('unknown');
1260
+ expect(result.data.dispatched_to_reconciler).toBe(false);
1261
+ expect(writeStateMock).toHaveBeenCalledWith(
1262
+ 'feat1',
1263
+ expect.objectContaining({
1264
+ status: 'blocked',
1265
+ status_reason: 'Mainline divergence assigned to the shared reconciler lane',
1266
+ }),
1267
+ '# Feature\n',
1268
+ );
1269
+ });
1270
+
1271
+ it('GIVEN_materialized_merge_commit_succeeds_WHEN_materializing_conflict_worktree_THEN_returns_merged', async () => {
1272
+ runGitMock
1273
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: no unmerged files
1274
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // merge --no-commit --no-ff succeeds
1275
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // commit
1276
+
1277
+ const svc = makeSvc() as unknown as {
1278
+ materializeConflictWorktree: (
1279
+ featureId: string,
1280
+ worktreePath: string,
1281
+ baseRef: string,
1282
+ mainlineHead: string,
1283
+ ) => Promise<{ kind: string; strategy?: string }>;
1284
+ };
1285
+
1286
+ const result = await svc.materializeConflictWorktree(
1287
+ 'feat1',
1288
+ '/repo/.worktrees/feat1',
1289
+ 'main',
1290
+ 'main123',
1291
+ );
1292
+
1293
+ expect(result).toEqual({ kind: 'merged', strategy: 'merge_commit' });
1294
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1295
+ expect.stringContaining('reconciliation.log'),
1296
+ expect.stringContaining('materialized_merge_commit: main123'),
1297
+ 'utf8',
1298
+ );
1299
+ });
1300
+
1301
+ it('GIVEN_materialized_merge_commit_fails_WHEN_materializing_conflict_worktree_THEN_returns_error_and_aborts', async () => {
1302
+ runGitMock
1303
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' })) // stale-merge check: no unmerged files
1304
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // merge --no-commit --no-ff succeeds
1305
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'commit failed' })) // commit fails
1306
+ .mockResolvedValueOnce(cmdResult({ code: 0 })); // merge --abort
1307
+
1308
+ const svc = makeSvc() as unknown as {
1309
+ materializeConflictWorktree: (
1310
+ featureId: string,
1311
+ worktreePath: string,
1312
+ baseRef: string,
1313
+ mainlineHead: string,
1314
+ ) => Promise<{ kind: string; response?: FailResponse }>;
1315
+ };
1316
+
1317
+ const result = await svc.materializeConflictWorktree(
1318
+ 'feat1',
1319
+ '/repo/.worktrees/feat1',
1320
+ 'main',
1321
+ 'main123',
1322
+ );
1323
+
1324
+ expect(result.kind).toBe('error');
1325
+ expect(result.response?.error.code).toBe('reconciliation_failed');
1326
+ expect(runGitMock).toHaveBeenCalledWith('/repo/.worktrees/feat1', ['merge', '--abort']);
1327
+ });
1328
+
1329
+ it('GIVEN_merge_failure_without_unmerged_files_WHEN_materializing_conflict_worktree_THEN_returns_error', async () => {
1330
+ runGitMock
1331
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'merge failed' }))
1332
+ .mockResolvedValueOnce(cmdResult({ code: 1, stderr: 'diff failed' }))
1333
+ .mockResolvedValueOnce(cmdResult({ code: 0 }));
1334
+
1335
+ const svc = makeSvc() as unknown as {
1336
+ materializeConflictWorktree: (
1337
+ featureId: string,
1338
+ worktreePath: string,
1339
+ baseRef: string,
1340
+ mainlineHead: string,
1341
+ ) => Promise<{ kind: string; response?: FailResponse }>;
1342
+ listUnmergedFiles: (worktreePath: string) => Promise<string[]>;
1343
+ };
1344
+
1345
+ const result = await svc.materializeConflictWorktree(
1346
+ 'feat1',
1347
+ '/repo/.worktrees/feat1',
1348
+ 'main',
1349
+ 'main123',
1350
+ );
1351
+
1352
+ expect(result.kind).toBe('error');
1353
+ expect(result.response?.error.code).toBe('reconciliation_failed');
1354
+ expect(runGitMock).toHaveBeenCalledWith('/repo/.worktrees/feat1', [
1355
+ 'diff',
1356
+ '--name-only',
1357
+ '--diff-filter=U',
1358
+ ]);
1359
+ });
1360
+
1361
+ it('GIVEN_blocked_feature_with_resume_status_WHEN_resolving_mainline_conflict_THEN_restores_prior_status', async () => {
1362
+ const writeStateMock = vi.fn(async () => {});
1363
+ const svc = makeSvc({
1364
+ readState: vi.fn(async () => ({
1365
+ frontMatter: {
1366
+ feature_id: 'feat1',
1367
+ version: 1,
1368
+ status: 'blocked',
1369
+ recovery: {
1370
+ state: 'retrying',
1371
+ cause: 'provider_output_invalid',
1372
+ role: 'planner',
1373
+ },
1374
+ conflicts: [
1375
+ {
1376
+ type: 'mainline_divergence',
1377
+ resolution_status: 'pending',
1378
+ resume_status: 'ready_to_merge',
1379
+ },
1380
+ ],
1381
+ } as Record<string, unknown>,
1382
+ body: '# Feature\n',
1383
+ })),
1384
+ writeState: writeStateMock,
1385
+ }) as unknown as {
1386
+ resolveMainlineConflictState: (featureId: string, mainlineHead: string) => Promise<void>;
1387
+ };
1388
+
1389
+ await svc.resolveMainlineConflictState('feat1', 'main456');
1390
+
1391
+ expect(writeStateMock).toHaveBeenCalledWith(
1392
+ 'feat1',
1393
+ expect.objectContaining({
1394
+ status: 'ready_to_merge',
1395
+ status_reason: 'Reconciled against main456',
1396
+ last_reconciliation_sha: 'main456',
1397
+ recovery: null,
1398
+ }),
1399
+ '# Feature\n',
1400
+ );
1401
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1402
+ expect.stringContaining('reconciliation.log'),
1403
+ expect.stringContaining('"approved_collision_context_used":false'),
1404
+ 'utf8',
1405
+ );
1406
+ });
1407
+
1408
+ it('GIVEN_approved_collision_context_WHEN_resolving_mainline_conflict_THEN_appends_structured_completion_log', async () => {
1409
+ const writeStateMock = vi.fn(async () => {});
1410
+ const svc = makeSvc({
1411
+ readState: vi.fn(async () => ({
1412
+ frontMatter: {
1413
+ feature_id: 'feat1',
1414
+ version: 1,
1415
+ status: 'blocked',
1416
+ conflicts: [
1417
+ {
1418
+ type: 'mainline_divergence',
1419
+ resolution_status: 'in_progress',
1420
+ resume_status: 'building',
1421
+ },
1422
+ ],
1423
+ } as Record<string, unknown>,
1424
+ body: '# Feature\n',
1425
+ })),
1426
+ writeState: writeStateMock,
1427
+ readIndex: vi.fn(async () => ({
1428
+ collision_overrides: [
1429
+ {
1430
+ override_id: 'colovr_1',
1431
+ feature_ids: ['feat1', 'feat2'],
1432
+ rationale: 'preserve both dashboard behaviors',
1433
+ approved_paths: ['shared.ts'],
1434
+ approved_collision_fingerprint: 'fp_1',
1435
+ status: 'applied',
1436
+ source_collision_id: 'colrec_1',
1437
+ applied_at: '2026-03-15T00:00:00.000Z',
1438
+ },
1439
+ ],
1440
+ collision_records: [
1441
+ {
1442
+ collision_id: 'colrec_1',
1443
+ detected_at: '2026-03-15T00:00:00.000Z',
1444
+ collisions: {
1445
+ files: [{ with_feature_id: 'feat2', paths: ['shared.ts'] }],
1446
+ areas: [],
1447
+ contracts: [],
1448
+ },
1449
+ },
1450
+ ],
1451
+ })),
1452
+ }) as unknown as {
1453
+ resolveMainlineConflictState: (featureId: string, mainlineHead: string) => Promise<void>;
1454
+ };
1455
+
1456
+ await svc.resolveMainlineConflictState('feat1', 'main999');
1457
+
1458
+ expect(writeStateMock).toHaveBeenCalled();
1459
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1460
+ expect.stringContaining('reconciliation.log'),
1461
+ expect.stringContaining('"approved_collision_context_used":true'),
1462
+ 'utf8',
1463
+ );
1464
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1465
+ expect.stringContaining('reconciliation.log'),
1466
+ expect.stringContaining('"override_ids":["colovr_1"]'),
1467
+ 'utf8',
1468
+ );
1469
+ });
1470
+
1471
+ it('GIVEN_resolved_and_unresolved_mainline_conflicts_WHEN_resolving_state_THEN_skips_resolved_entry_and_updates_latest_unresolved', async () => {
1472
+ const writeStateMock = vi.fn(async () => {});
1473
+ const svc = makeSvc({
1474
+ readState: vi.fn(async () => ({
1475
+ frontMatter: {
1476
+ feature_id: 'feat1',
1477
+ version: 1,
1478
+ status: 'blocked',
1479
+ conflicts: [
1480
+ {
1481
+ type: 'mainline_divergence',
1482
+ resolution_status: 'resolved',
1483
+ resume_status: 'building',
1484
+ },
1485
+ {
1486
+ type: 'mainline_divergence',
1487
+ resolution_status: 'pending',
1488
+ resume_status: 'ready_to_merge',
1489
+ },
1490
+ ],
1491
+ } as Record<string, unknown>,
1492
+ body: '# Feature\n',
1493
+ })),
1494
+ writeState: writeStateMock,
1495
+ }) as unknown as {
1496
+ resolveMainlineConflictState: (featureId: string, mainlineHead: string) => Promise<void>;
1497
+ };
1498
+
1499
+ await svc.resolveMainlineConflictState('feat1', 'main789');
1500
+
1501
+ const [, nextFrontMatter] = writeStateMock.mock.calls[0] as unknown as [
1502
+ string,
1503
+ Record<string, unknown>,
1504
+ string,
1505
+ ];
1506
+ const conflicts = nextFrontMatter.conflicts as Array<Record<string, unknown>>;
1507
+ expect(conflicts[0].resolution_status).toBe('resolved');
1508
+ expect(conflicts[1].resolution_status).toBe('resolved');
1509
+ expect(conflicts[1].resolved_mainline_head).toBe('main789');
1510
+ expect(nextFrontMatter.status).toBe('ready_to_merge');
1511
+ });
1512
+
1513
+ it('GIVEN_no_unresolved_mainline_conflict_WHEN_resolving_state_THEN_skips_write', async () => {
1514
+ const writeStateMock = vi.fn(async () => {});
1515
+ const svc = makeSvc({
1516
+ readState: vi.fn(async () => ({
1517
+ frontMatter: {
1518
+ feature_id: 'feat1',
1519
+ version: 1,
1520
+ status: 'building',
1521
+ conflicts: [{ type: 'qa_blocker', resolution_status: 'pending' }],
1522
+ } as Record<string, unknown>,
1523
+ body: '# Feature\n',
1524
+ })),
1525
+ writeState: writeStateMock,
1526
+ }) as unknown as {
1527
+ resolveMainlineConflictState: (featureId: string, mainlineHead: string) => Promise<void>;
1528
+ };
1529
+
1530
+ await svc.resolveMainlineConflictState('feat1', 'main456');
1531
+ expect(writeStateMock).not.toHaveBeenCalled();
1532
+ });
1533
+
1534
+ it('GIVEN_existing_blocked_conflict_WHEN_upserting_mainline_conflict_THEN_reuses_entry_and_restores_building_resume_status', async () => {
1535
+ const writeStateMock = vi.fn(async () => {});
1536
+ const svc = makeSvc({
1537
+ readState: vi.fn(async () => ({
1538
+ frontMatter: {
1539
+ feature_id: 'feat1',
1540
+ version: 1,
1541
+ status: 'blocked',
1542
+ recovery: {
1543
+ state: 'retrying',
1544
+ cause: 'provider_output_invalid',
1545
+ role: 'planner',
1546
+ },
1547
+ conflicts: [
1548
+ {
1549
+ type: 'mainline_divergence',
1550
+ resolution_status: 'pending',
1551
+ detected_at: '2025-12-31T00:00:00.000Z',
1552
+ },
1553
+ ],
1554
+ } as Record<string, unknown>,
1555
+ body: '# Feature\n',
1556
+ })),
1557
+ writeState: writeStateMock,
1558
+ }) as unknown as {
1559
+ upsertMainlineConflictState: (
1560
+ featureId: string,
1561
+ info: {
1562
+ mergeBase: string;
1563
+ mainlineHead: string;
1564
+ worktreeHead: string;
1565
+ conflictingFiles: string[];
1566
+ },
1567
+ options: {
1568
+ baseRef: string;
1569
+ resolutionStatus: 'pending' | 'in_progress';
1570
+ queuePosition: number;
1571
+ statusReason: string;
1572
+ },
1573
+ ) => Promise<void>;
1574
+ };
1575
+
1576
+ await svc.upsertMainlineConflictState(
1577
+ 'feat1',
1578
+ {
1579
+ mergeBase: 'base123',
1580
+ mainlineHead: 'main123',
1581
+ worktreeHead: 'feat123',
1582
+ conflictingFiles: ['src/shared.ts'],
1583
+ },
1584
+ {
1585
+ baseRef: 'main',
1586
+ resolutionStatus: 'pending',
1587
+ queuePosition: 2,
1588
+ statusReason: 'Queued for shared reconciler lane at position 3 behind feat0',
1589
+ },
1590
+ );
1591
+
1592
+ expect(writeStateMock).toHaveBeenCalledTimes(1);
1593
+ const [, nextFrontMatter] = writeStateMock.mock.calls[0] as unknown as [
1594
+ string,
1595
+ Record<string, unknown>,
1596
+ string,
1597
+ ];
1598
+ const nextConflict = (nextFrontMatter.conflicts as Array<Record<string, unknown>>)[0];
1599
+ expect(nextConflict.detected_at).toBe('2025-12-31T00:00:00.000Z');
1600
+ expect(nextConflict.resume_status).toBe('building');
1601
+ expect(nextConflict.queue_position).toBe(2);
1602
+ expect(nextConflict.queued_at).toBe('2026-01-01T00:00:00.000Z');
1603
+ // When reconciler is assigned, recovery state is set to 'retrying'
1604
+ expect(nextFrontMatter.recovery).toEqual(
1605
+ expect.objectContaining({
1606
+ state: 'retrying',
1607
+ cause: 'mainline_divergence',
1608
+ role: 'reconciler',
1609
+ resume_phase: 'building',
1610
+ }),
1611
+ );
1612
+ });
1613
+
1614
+ it('GIVEN_missing_session_or_provider_WHEN_dispatching_reconciler_turn_THEN_returns_false', async () => {
1615
+ const svcNoSession = makeSvc() as unknown as {
1616
+ dispatchReconcilerTurn: (
1617
+ featureId: string,
1618
+ baseRef: string,
1619
+ sessionId: string | null,
1620
+ info: {
1621
+ mergeBase: string;
1622
+ mainlineHead: string;
1623
+ worktreeHead: string;
1624
+ conflictingFiles: string[];
1625
+ },
1626
+ ) => Promise<boolean>;
1627
+ waitForSessionToBecomeActive: (
1628
+ provider: Record<string, unknown>,
1629
+ sessionId: string,
1630
+ ) => Promise<void>;
1631
+ };
1632
+
1633
+ await expect(
1634
+ svcNoSession.dispatchReconcilerTurn('feat1', 'main', 'unknown', {
1635
+ mergeBase: 'base',
1636
+ mainlineHead: 'main',
1637
+ worktreeHead: 'feat',
1638
+ conflictingFiles: ['src/shared.ts'],
1639
+ }),
1640
+ ).resolves.toBe(false);
1641
+
1642
+ await expect(
1643
+ svcNoSession.waitForSessionToBecomeActive({}, 'reconciler:1'),
1644
+ ).resolves.toBeUndefined();
1645
+
1646
+ const svcNoSender = makeSvc({
1647
+ getProvider: vi.fn(() => ({
1648
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'codex' })),
1649
+ })),
1650
+ }) as unknown as {
1651
+ dispatchReconcilerTurn: (
1652
+ featureId: string,
1653
+ baseRef: string,
1654
+ sessionId: string | null,
1655
+ info: {
1656
+ mergeBase: string;
1657
+ mainlineHead: string;
1658
+ worktreeHead: string;
1659
+ conflictingFiles: string[];
1660
+ },
1661
+ ) => Promise<boolean>;
1662
+ };
1663
+
1664
+ await expect(
1665
+ svcNoSender.dispatchReconcilerTurn('feat1', 'main', 'reconciler:1', {
1666
+ mergeBase: 'base',
1667
+ mainlineHead: 'main',
1668
+ worktreeHead: 'feat',
1669
+ conflictingFiles: ['src/shared.ts'],
1670
+ }),
1671
+ ).resolves.toBe(false);
1672
+ });
1673
+
1674
+ it('GIVEN_send_message_throws_WHEN_dispatching_reconciler_turn_THEN_returns_false_and_logs_the_failure', async () => {
1675
+ const sendMessageMock = vi.fn(async () => {
1676
+ throw new Error('claude provider send failed');
1677
+ });
1678
+ const svc = makeSvc({
1679
+ getProvider: vi.fn(() => ({
1680
+ sendMessage: sendMessageMock,
1681
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'claude' })),
1682
+ })),
1683
+ }) as unknown as {
1684
+ dispatchReconcilerTurn: (
1685
+ featureId: string,
1686
+ baseRef: string,
1687
+ sessionId: string | null,
1688
+ info: {
1689
+ mergeBase: string;
1690
+ mainlineHead: string;
1691
+ worktreeHead: string;
1692
+ conflictingFiles: string[];
1693
+ },
1694
+ ) => Promise<boolean>;
1695
+ };
1696
+
1697
+ await expect(
1698
+ svc.dispatchReconcilerTurn('feat1', 'main', 'reconciler:1', {
1699
+ mergeBase: 'base',
1700
+ mainlineHead: 'main',
1701
+ worktreeHead: 'feat',
1702
+ conflictingFiles: ['src/shared.ts'],
1703
+ }),
1704
+ ).resolves.toBe(false);
1705
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1706
+ expect.stringContaining('reconciliation.log'),
1707
+ expect.stringContaining('dispatch_failed: claude provider send failed'),
1708
+ 'utf8',
1709
+ );
1710
+ });
1711
+
1712
+ it('GIVEN_missing_claude_reconciler_conversation_WHEN_dispatching_reconciler_turn_THEN_refreshes_the_session_and_retries_once', async () => {
1713
+ const sendMessageMock = vi
1714
+ .fn<(sessionId: string, _message: string) => Promise<void>>()
1715
+ .mockRejectedValueOnce(
1716
+ Object.assign(new Error('claude provider send failed'), {
1717
+ details: { stderr: 'No conversation found with session ID: reconciler:1' },
1718
+ }),
1719
+ )
1720
+ .mockResolvedValueOnce(undefined);
1721
+ const refreshReconcilerSessionMock = vi.fn(async () => 'reconciler:2');
1722
+ const svc = makeSvc({
1723
+ refreshReconcilerSession: refreshReconcilerSessionMock,
1724
+ getProvider: vi.fn(() => ({
1725
+ sendMessage: sendMessageMock,
1726
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'claude' })),
1727
+ })),
1728
+ }) as unknown as {
1729
+ dispatchReconcilerTurn: (
1730
+ featureId: string,
1731
+ baseRef: string,
1732
+ sessionId: string | null,
1733
+ info: {
1734
+ mergeBase: string;
1735
+ mainlineHead: string;
1736
+ worktreeHead: string;
1737
+ conflictingFiles: string[];
1738
+ },
1739
+ ) => Promise<boolean>;
1740
+ };
1741
+
1742
+ await expect(
1743
+ svc.dispatchReconcilerTurn('feat1', 'main', 'reconciler:1', {
1744
+ mergeBase: 'base',
1745
+ mainlineHead: 'main',
1746
+ worktreeHead: 'feat',
1747
+ conflictingFiles: ['src/shared.ts'],
1748
+ }),
1749
+ ).resolves.toBe(true);
1750
+ expect(refreshReconcilerSessionMock).toHaveBeenCalledTimes(1);
1751
+ expect(sendMessageMock).toHaveBeenNthCalledWith(
1752
+ 1,
1753
+ 'reconciler:1',
1754
+ expect.stringContaining('feature_id=feat1'),
1755
+ );
1756
+ expect(sendMessageMock).toHaveBeenNthCalledWith(
1757
+ 2,
1758
+ 'reconciler:2',
1759
+ expect.stringContaining('feature_id=feat1'),
1760
+ );
1761
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1762
+ expect.stringContaining('reconciliation.log'),
1763
+ expect.stringContaining(
1764
+ 'reconciler_session_refreshed old_session_id=reconciler:1 new_session_id=reconciler:2',
1765
+ ),
1766
+ 'utf8',
1767
+ );
1768
+ });
1769
+
1770
+ it('GIVEN_missing_claude_reconciler_conversation_WHEN_refresh_does_not_change_the_session_THEN_logs_the_original_failure', async () => {
1771
+ const sendMessageMock = vi
1772
+ .fn<(sessionId: string, _message: string) => Promise<void>>()
1773
+ .mockRejectedValueOnce(
1774
+ Object.assign(new Error('claude provider send failed'), {
1775
+ details: { stderr: 'No conversation found with session ID: reconciler:1' },
1776
+ }),
1777
+ );
1778
+ const refreshReconcilerSessionMock = vi.fn(async () => 'reconciler:1');
1779
+ const svc = makeSvc({
1780
+ refreshReconcilerSession: refreshReconcilerSessionMock,
1781
+ getProvider: vi.fn(() => ({
1782
+ sendMessage: sendMessageMock,
1783
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'claude' })),
1784
+ })),
1785
+ }) as unknown as {
1786
+ dispatchReconcilerTurn: (
1787
+ featureId: string,
1788
+ baseRef: string,
1789
+ sessionId: string | null,
1790
+ info: {
1791
+ mergeBase: string;
1792
+ mainlineHead: string;
1793
+ worktreeHead: string;
1794
+ conflictingFiles: string[];
1795
+ },
1796
+ ) => Promise<boolean>;
1797
+ };
1798
+
1799
+ await expect(
1800
+ svc.dispatchReconcilerTurn('feat1', 'main', 'reconciler:1', {
1801
+ mergeBase: 'base',
1802
+ mainlineHead: 'main',
1803
+ worktreeHead: 'feat',
1804
+ conflictingFiles: ['src/shared.ts'],
1805
+ }),
1806
+ ).resolves.toBe(false);
1807
+
1808
+ expect(refreshReconcilerSessionMock).toHaveBeenCalledTimes(1);
1809
+ expect(sendMessageMock).toHaveBeenCalledTimes(1);
1810
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1811
+ expect.stringContaining('reconciliation.log'),
1812
+ expect.stringContaining(
1813
+ 'dispatch_failed: claude provider send failed: No conversation found with session ID: reconciler:1 session_id=reconciler:1',
1814
+ ),
1815
+ 'utf8',
1816
+ );
1817
+ });
1818
+
1819
+ it('GIVEN_missing_claude_reconciler_conversation_WHEN_retry_dispatch_fails_THEN_logs_the_retry_failure', async () => {
1820
+ const sendMessageMock = vi
1821
+ .fn<(sessionId: string, _message: string) => Promise<void>>()
1822
+ .mockRejectedValueOnce(
1823
+ Object.assign(new Error('claude provider send failed'), {
1824
+ details: { stderr: 'No conversation found with session ID: reconciler:1' },
1825
+ }),
1826
+ )
1827
+ .mockRejectedValueOnce(new Error('retry dispatch failed'));
1828
+ const refreshReconcilerSessionMock = vi.fn(async () => 'reconciler:2');
1829
+ const svc = makeSvc({
1830
+ refreshReconcilerSession: refreshReconcilerSessionMock,
1831
+ getProvider: vi.fn(() => ({
1832
+ sendMessage: sendMessageMock,
1833
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'claude' })),
1834
+ })),
1835
+ }) as unknown as {
1836
+ dispatchReconcilerTurn: (
1837
+ featureId: string,
1838
+ baseRef: string,
1839
+ sessionId: string | null,
1840
+ info: {
1841
+ mergeBase: string;
1842
+ mainlineHead: string;
1843
+ worktreeHead: string;
1844
+ conflictingFiles: string[];
1845
+ },
1846
+ ) => Promise<boolean>;
1847
+ };
1848
+
1849
+ await expect(
1850
+ svc.dispatchReconcilerTurn('feat1', 'main', 'reconciler:1', {
1851
+ mergeBase: 'base',
1852
+ mainlineHead: 'main',
1853
+ worktreeHead: 'feat',
1854
+ conflictingFiles: ['src/shared.ts'],
1855
+ }),
1856
+ ).resolves.toBe(false);
1857
+
1858
+ expect(refreshReconcilerSessionMock).toHaveBeenCalledTimes(1);
1859
+ expect(sendMessageMock).toHaveBeenNthCalledWith(
1860
+ 2,
1861
+ 'reconciler:2',
1862
+ expect.stringContaining('feature_id=feat1'),
1863
+ );
1864
+ expect(fsAppendFileMock).toHaveBeenCalledWith(
1865
+ expect.stringContaining('reconciliation.log'),
1866
+ expect.stringContaining('dispatch_failed: retry dispatch failed session_id=reconciler:2'),
1867
+ 'utf8',
1868
+ );
1869
+ });
1870
+
1871
+ it('GIVEN_active_prepared_conflict_WHEN_redriving_reconciler_assignment_THEN_dispatches_the_pending_turn', async () => {
1872
+ runGitMock.mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/shared.ts\n' }));
1873
+ const sendMessageMock = vi.fn(async () => undefined);
1874
+ const writeStateMock = vi.fn(async () => {});
1875
+ const svc = makeSvc({
1876
+ readState: vi.fn(async () => ({
1877
+ frontMatter: {
1878
+ feature_id: 'feat1',
1879
+ version: 1,
1880
+ status: 'blocked',
1881
+ conflicts: [
1882
+ {
1883
+ type: 'mainline_divergence',
1884
+ base_ref: 'main',
1885
+ merge_base: 'base123',
1886
+ mainline_head: 'main456',
1887
+ worktree_head: 'feat789',
1888
+ conflicting_files: ['src/shared.ts'],
1889
+ resolution_status: 'in_progress',
1890
+ resume_status: 'ready_to_merge',
1891
+ },
1892
+ ],
1893
+ },
1894
+ body: '# Feature\n',
1895
+ })),
1896
+ writeState: writeStateMock,
1897
+ readRunLease: vi.fn(
1898
+ async () =>
1899
+ ({
1900
+ run_id: 'run:test',
1901
+ orchestrator_session_id: 'orchestrator:1',
1902
+ reconciler_session_id: 'reconciler:1',
1903
+ reconciler_active_feature_id: 'feat1',
1904
+ reconciler_queue: [],
1905
+ provider: 'claude',
1906
+ model: 'claude-sonnet-4-6',
1907
+ provider_config_ref_hash: 'hash',
1908
+ owner_instance_id: 'owner:test',
1909
+ lease_id: 'lease:test',
1910
+ started_at: '2026-01-01T00:00:00.000Z',
1911
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
1912
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
1913
+ feature_sessions: {},
1914
+ }) satisfies RuntimeSessionsSnapshot,
1915
+ ),
1916
+ getProvider: vi.fn(() => ({
1917
+ sendMessage: sendMessageMock,
1918
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'claude' })),
1919
+ })),
1920
+ });
1921
+
1922
+ await expect(svc.redrivePreparedReconcilerConflict('feat1')).resolves.toBe(true);
1923
+
1924
+ expect(sendMessageMock).toHaveBeenCalledTimes(1);
1925
+ expect(writeStateMock).toHaveBeenCalledWith(
1926
+ 'feat1',
1927
+ expect.objectContaining({
1928
+ status: 'blocked',
1929
+ status_reason: 'Mainline divergence assigned to the shared reconciler lane',
1930
+ }),
1931
+ '# Feature\n',
1932
+ );
1933
+ });
1934
+
1935
+ it('GIVEN_session_never_becomes_active_WHEN_waiting_for_reconciler_session_THEN_times_out_cleanly', async () => {
1936
+ vi.useFakeTimers();
1937
+ const getSessionInfoMock = vi.fn(async () => ({ active: false, provider: 'codex' }));
1938
+ const svc = makeSvc() as unknown as {
1939
+ waitForSessionToBecomeActive: (
1940
+ provider: {
1941
+ getSessionInfo: (sessionId: string) => Promise<{ active: boolean; provider: string }>;
1942
+ },
1943
+ sessionId: string,
1944
+ ) => Promise<void>;
1945
+ };
1946
+
1947
+ const waitPromise = svc.waitForSessionToBecomeActive(
1948
+ { getSessionInfo: getSessionInfoMock },
1949
+ 'reconciler:1',
1950
+ );
1951
+ await vi.advanceTimersByTimeAsync(6000);
1952
+ await waitPromise;
1953
+
1954
+ expect(getSessionInfoMock).toHaveBeenCalled();
1955
+ vi.useRealTimers();
1956
+ });
1957
+
1958
+ it('GIVEN_released_lane_with_stale_first_queue_entry_WHEN_dispatching_next_THEN_promotes_next_dispatchable_feature', async () => {
1959
+ let runtimeSessions: RuntimeSessionsSnapshot = {
1960
+ run_id: 'run:test',
1961
+ orchestrator_session_id: 'orchestrator:1',
1962
+ reconciler_session_id: 'reconciler:1',
1963
+ reconciler_active_feature_id: 'feat1',
1964
+ reconciler_queue: ['feat2', 'feat3'],
1965
+ provider: 'codex',
1966
+ model: 'codex-default',
1967
+ provider_config_ref_hash: 'hash',
1968
+ owner_instance_id: 'owner:test',
1969
+ lease_id: 'lease:test',
1970
+ started_at: '2026-01-01T00:00:00.000Z',
1971
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
1972
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
1973
+ feature_sessions: {},
1974
+ };
1975
+ const sendMessageMock = vi.fn(async () => undefined);
1976
+
1977
+ runGitMock
1978
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' }))
1979
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' }));
1980
+
1981
+ const svc = makeSvc({
1982
+ readRunLease: vi.fn(async () => runtimeSessions),
1983
+ writeRunLease: vi.fn(async (nextState: RuntimeSessionsSnapshot) => {
1984
+ runtimeSessions = nextState;
1985
+ }),
1986
+ readState: vi.fn(async (featureId: string) => {
1987
+ if (featureId === 'feat2') {
1988
+ return {
1989
+ frontMatter: {
1990
+ feature_id: 'feat2',
1991
+ version: 1,
1992
+ status: 'blocked',
1993
+ conflicts: [{ type: 'mainline_divergence', resolution_status: 'pending' }],
1994
+ } as Record<string, unknown>,
1995
+ body: '# Feature 2\n',
1996
+ };
1997
+ }
1998
+ return {
1999
+ frontMatter: {
2000
+ feature_id: 'feat3',
2001
+ version: 1,
2002
+ status: 'blocked',
2003
+ conflicts: [
2004
+ {
2005
+ type: 'mainline_divergence',
2006
+ resolution_status: 'pending',
2007
+ merge_base: 'base3',
2008
+ mainline_head: 'main3',
2009
+ worktree_head: 'feat3head',
2010
+ conflicting_files: ['src/shared.ts'],
2011
+ },
2012
+ ],
2013
+ } as Record<string, unknown>,
2014
+ body: '# Feature 3\n',
2015
+ };
2016
+ }),
2017
+ getProvider: vi.fn(() => ({
2018
+ sendMessage: sendMessageMock,
2019
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'codex' })),
2020
+ })),
2021
+ }) as unknown as {
2022
+ releaseAndDispatchNext: (featureId: string, baseRef: string) => Promise<void>;
2023
+ };
2024
+
2025
+ await svc.releaseAndDispatchNext('feat1', 'main');
2026
+
2027
+ expect(runtimeSessions.reconciler_active_feature_id).toBe('feat3');
2028
+ expect(runtimeSessions.reconciler_queue).toEqual([]);
2029
+ expect(sendMessageMock).toHaveBeenCalledWith(
2030
+ 'reconciler:1',
2031
+ expect.stringContaining('feature_id=feat3'),
2032
+ );
2033
+ });
2034
+
2035
+ it('GIVEN_idle_lane_with_feature_already_in_queue_WHEN_claiming_THEN_promotes_feature_and_removes_duplicate_queue_entry', async () => {
2036
+ let runtimeSessions: RuntimeSessionsSnapshot = {
2037
+ run_id: 'run:test',
2038
+ orchestrator_session_id: 'orchestrator:1',
2039
+ reconciler_session_id: 'reconciler:1',
2040
+ reconciler_active_feature_id: null,
2041
+ reconciler_queue: ['feat1', 'feat2'],
2042
+ provider: 'codex',
2043
+ model: 'codex-default',
2044
+ provider_config_ref_hash: 'hash',
2045
+ owner_instance_id: 'owner:test',
2046
+ lease_id: 'lease:test',
2047
+ started_at: '2026-01-01T00:00:00.000Z',
2048
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
2049
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
2050
+ feature_sessions: {},
2051
+ };
2052
+
2053
+ const svc = makeSvc({
2054
+ readRunLease: vi.fn(async () => runtimeSessions),
2055
+ writeRunLease: vi.fn(async (nextState: RuntimeSessionsSnapshot) => {
2056
+ runtimeSessions = nextState;
2057
+ }),
2058
+ }) as unknown as {
2059
+ claimReconcilerLane: (featureId: string) => Promise<{
2060
+ activeFeatureId: string | null;
2061
+ queuePosition: number | null;
2062
+ dispatchNow: boolean;
2063
+ }>;
2064
+ };
2065
+
2066
+ const claim = await svc.claimReconcilerLane('feat1');
2067
+
2068
+ expect(claim.activeFeatureId).toBe('feat1');
2069
+ expect(claim.dispatchNow).toBe(true);
2070
+ expect(runtimeSessions.reconciler_queue).toEqual(['feat2']);
2071
+ });
2072
+
2073
+ it('GIVEN_feature_already_owns_reconciler_lane_WHEN_claiming_THEN_deduplicates_queue_without_requeueing', async () => {
2074
+ let runtimeSessions: RuntimeSessionsSnapshot = {
2075
+ run_id: 'run:test',
2076
+ orchestrator_session_id: 'orchestrator:1',
2077
+ reconciler_session_id: 'reconciler:1',
2078
+ reconciler_active_feature_id: 'feat1',
2079
+ reconciler_queue: ['feat1', 'feat2', 'feat2', 'feat3'],
2080
+ provider: 'codex',
2081
+ model: 'codex-default',
2082
+ provider_config_ref_hash: 'hash',
2083
+ owner_instance_id: 'owner:test',
2084
+ lease_id: 'lease:test',
2085
+ started_at: '2026-01-01T00:00:00.000Z',
2086
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
2087
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
2088
+ feature_sessions: {},
2089
+ };
2090
+
2091
+ const svc = makeSvc({
2092
+ readRunLease: vi.fn(async () => runtimeSessions),
2093
+ writeRunLease: vi.fn(async (nextState: RuntimeSessionsSnapshot) => {
2094
+ runtimeSessions = nextState;
2095
+ }),
2096
+ }) as unknown as {
2097
+ claimReconcilerLane: (featureId: string) => Promise<{
2098
+ activeFeatureId: string | null;
2099
+ queuePosition: number | null;
2100
+ dispatchNow: boolean;
2101
+ }>;
2102
+ };
2103
+
2104
+ const claim = await svc.claimReconcilerLane('feat1');
2105
+
2106
+ expect(claim.activeFeatureId).toBe('feat1');
2107
+ expect(claim.queuePosition).toBe(0);
2108
+ expect(claim.dispatchNow).toBe(false);
2109
+ expect(runtimeSessions.reconciler_queue).toEqual(['feat2', 'feat3']);
2110
+ });
2111
+
2112
+ it('GIVEN_releasing_non_active_queued_feature_WHEN_releasing_lane_THEN_removes_it_without_changing_active_owner', async () => {
2113
+ let runtimeSessions: RuntimeSessionsSnapshot = {
2114
+ run_id: 'run:test',
2115
+ orchestrator_session_id: 'orchestrator:1',
2116
+ reconciler_session_id: 'reconciler:1',
2117
+ reconciler_active_feature_id: 'feat1',
2118
+ reconciler_queue: ['feat2', 'feat3'],
2119
+ provider: 'codex',
2120
+ model: 'codex-default',
2121
+ provider_config_ref_hash: 'hash',
2122
+ owner_instance_id: 'owner:test',
2123
+ lease_id: 'lease:test',
2124
+ started_at: '2026-01-01T00:00:00.000Z',
2125
+ last_heartbeat_at: '2026-01-01T00:00:00.000Z',
2126
+ lease_expires_at: '2026-01-01T00:05:00.000Z',
2127
+ feature_sessions: {},
2128
+ };
2129
+
2130
+ const svc = makeSvc({
2131
+ readRunLease: vi.fn(async () => runtimeSessions),
2132
+ writeRunLease: vi.fn(async (nextState: RuntimeSessionsSnapshot) => {
2133
+ runtimeSessions = nextState;
2134
+ }),
2135
+ }) as unknown as {
2136
+ releaseReconcilerLane: (featureId: string) => Promise<{
2137
+ nextFeatureId: string | null;
2138
+ sessionId: string | null;
2139
+ }>;
2140
+ };
2141
+
2142
+ const release = await svc.releaseReconcilerLane('feat2');
2143
+
2144
+ expect(release.nextFeatureId).toBe('feat1');
2145
+ expect(runtimeSessions.reconciler_active_feature_id).toBe('feat1');
2146
+ expect(runtimeSessions.reconciler_queue).toEqual(['feat3']);
2147
+ });
2148
+
2149
+ it('GIVEN_queued_feature_without_readable_state_WHEN_activating_THEN_returns_false', async () => {
2150
+ const svc = makeSvc({
2151
+ readState: vi.fn(async () => {
2152
+ throw new Error('missing state');
2153
+ }),
2154
+ }) as unknown as {
2155
+ activateQueuedFeature: (
2156
+ featureId: string,
2157
+ baseRef: string,
2158
+ sessionId: string | null,
2159
+ ) => Promise<boolean>;
2160
+ };
2161
+
2162
+ await expect(svc.activateQueuedFeature('feat2', 'main', 'reconciler:1')).resolves.toBe(false);
2163
+ });
2164
+
2165
+ it('GIVEN_queued_feature_without_unresolved_mainline_conflict_WHEN_activating_THEN_returns_false', async () => {
2166
+ const svc = makeSvc({
2167
+ readState: vi.fn(async () => ({
2168
+ frontMatter: {
2169
+ feature_id: 'feat2',
2170
+ version: 1,
2171
+ status: 'blocked',
2172
+ conflicts: [{ type: 'qa_blocker', resolution_status: 'pending' }],
2173
+ } as Record<string, unknown>,
2174
+ body: '# Feature 2\n',
2175
+ })),
2176
+ }) as unknown as {
2177
+ activateQueuedFeature: (
2178
+ featureId: string,
2179
+ baseRef: string,
2180
+ sessionId: string | null,
2181
+ ) => Promise<boolean>;
2182
+ };
2183
+
2184
+ await expect(svc.activateQueuedFeature('feat2', 'main', 'reconciler:1')).resolves.toBe(false);
2185
+ });
2186
+
2187
+ it('GIVEN_queued_feature_without_conflict_files_WHEN_activating_THEN_returns_false', async () => {
2188
+ runGitMock.mockResolvedValueOnce(cmdResult({ code: 0, stdout: '' }));
2189
+
2190
+ const svc = makeSvc({
2191
+ readState: vi.fn(async () => ({
2192
+ frontMatter: {
2193
+ feature_id: 'feat2',
2194
+ version: 1,
2195
+ status: 'blocked',
2196
+ conflicts: [{ type: 'mainline_divergence', resolution_status: 'pending' }],
2197
+ } as Record<string, unknown>,
2198
+ body: '# Feature 2\n',
2199
+ })),
2200
+ }) as unknown as {
2201
+ activateQueuedFeature: (
2202
+ featureId: string,
2203
+ baseRef: string,
2204
+ sessionId: string | null,
2205
+ ) => Promise<boolean>;
2206
+ };
2207
+
2208
+ await expect(svc.activateQueuedFeature('feat2', 'main', 'reconciler:1')).resolves.toBe(false);
2209
+ });
2210
+
2211
+ it('GIVEN_queued_feature_with_unmerged_files_WHEN_activating_THEN_dispatches_reconciler_turn', async () => {
2212
+ const sendMessageMock = vi.fn(async () => undefined);
2213
+ runGitMock.mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'src/shared.ts\n' }));
2214
+
2215
+ const svc = makeSvc({
2216
+ readState: vi.fn(async () => ({
2217
+ frontMatter: {
2218
+ feature_id: 'feat2',
2219
+ version: 1,
2220
+ status: 'blocked',
2221
+ conflicts: [
2222
+ {
2223
+ type: 'mainline_divergence',
2224
+ resolution_status: 'pending',
2225
+ merge_base: 'base2',
2226
+ mainline_head: 'main2',
2227
+ worktree_head: 'feat2head',
2228
+ },
2229
+ ],
2230
+ } as Record<string, unknown>,
2231
+ body: '# Feature 2\n',
2232
+ })),
2233
+ getProvider: vi.fn(() => ({
2234
+ sendMessage: sendMessageMock,
2235
+ getSessionInfo: vi.fn(async () => ({ active: true, provider: 'codex' })),
2236
+ })),
2237
+ }) as unknown as {
2238
+ activateQueuedFeature: (
2239
+ featureId: string,
2240
+ baseRef: string,
2241
+ sessionId: string | null,
2242
+ ) => Promise<boolean>;
2243
+ };
2244
+
2245
+ await expect(svc.activateQueuedFeature('feat2', 'main', 'reconciler:1')).resolves.toBe(true);
2246
+ expect(sendMessageMock).toHaveBeenCalledWith(
2247
+ 'reconciler:1',
2248
+ expect.stringContaining('feature_id=feat2'),
2249
+ );
2250
+ });
2251
+
2252
+ it('GIVEN_state_write_helpers_throw_WHEN_reconciliation_state_updates_run_THEN_errors_are_swallowed', async () => {
2253
+ runGitMock
2254
+ .mockResolvedValueOnce(cmdResult({ code: 0 })) // fetch
2255
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // merge-base
2256
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'base001\n' })) // rev-parse main
2257
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'origin001\n' })) // rev-parse origin/main
2258
+ .mockResolvedValueOnce(cmdResult({ code: 0, stdout: 'feat111\n' })); // rev-parse HEAD
2259
+
2260
+ const svc = makeSvc({
2261
+ writeState: vi.fn(async () => {
2262
+ throw new Error('write failed');
2263
+ }),
2264
+ });
2265
+
2266
+ const result = await svc.reconcileWithMainline('feat1', 'main');
2267
+ expect(result.ok).toBe(true);
2268
+
2269
+ const privateSvc = svc as unknown as {
2270
+ upsertMainlineConflictState: (
2271
+ featureId: string,
2272
+ info: {
2273
+ mergeBase: string;
2274
+ mainlineHead: string;
2275
+ worktreeHead: string;
2276
+ conflictingFiles: string[];
2277
+ },
2278
+ options: {
2279
+ baseRef: string;
2280
+ resolutionStatus: 'pending' | 'in_progress';
2281
+ queuePosition: number;
2282
+ statusReason: string;
2283
+ },
2284
+ ) => Promise<void>;
2285
+ resolveMainlineConflictState: (featureId: string, mainlineHead: string) => Promise<void>;
2286
+ };
2287
+
2288
+ await expect(
2289
+ privateSvc.upsertMainlineConflictState(
2290
+ 'feat1',
2291
+ {
2292
+ mergeBase: 'base',
2293
+ mainlineHead: 'main',
2294
+ worktreeHead: 'feat',
2295
+ conflictingFiles: ['src/shared.ts'],
2296
+ },
2297
+ {
2298
+ baseRef: 'main',
2299
+ resolutionStatus: 'pending',
2300
+ queuePosition: 1,
2301
+ statusReason: 'queued',
2302
+ },
2303
+ ),
2304
+ ).resolves.toBeUndefined();
2305
+ await expect(privateSvc.resolveMainlineConflictState('feat1', 'main')).resolves.toBeUndefined();
2306
+ });
2307
+ });