agentic-orchestrator 0.1.28 → 0.2.1

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 +111 -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 +84 -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 +362 -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 +154 -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 +103 -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 +881 -60
  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 +114 -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
@@ -2,7 +2,8 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { describe, expect, it, vi } from 'vitest';
5
- import { TOOLS } from '../src/core/constants.js';
5
+ import { STATUS, TOOLS } from '../src/core/constants.js';
6
+ import { ERROR_CODES } from '../src/core/error-codes.js';
6
7
  import {
7
8
  NOOP_WORKER_DECISION_RUNNER,
8
9
  WorkerDecisionLoop,
@@ -20,6 +21,18 @@ describe('WorkerDecisionLoop', () => {
20
21
  };
21
22
  }
22
23
 
24
+ function makeSequentialProvider(results: Record<string, unknown>[]) {
25
+ const queue = [...results];
26
+ return {
27
+ selection: {
28
+ provider: 'custom',
29
+ model: 'model-test',
30
+ provider_config_ref: null,
31
+ },
32
+ runWorker: vi.fn(async () => queue.shift() ?? { outputs: [] }),
33
+ };
34
+ }
35
+
23
36
  function makeToolCaller() {
24
37
  return {
25
38
  callTool: vi.fn(async (_role: string, toolName: string, _input?: unknown) => {
@@ -41,15 +54,25 @@ describe('WorkerDecisionLoop', () => {
41
54
 
42
55
  expect(result).toEqual({
43
56
  planSubmission: false,
57
+ intakeSubmission: false,
44
58
  patchApplied: false,
45
59
  noteLogged: false,
46
60
  requestHandled: false,
61
+ questionRequested: false,
62
+ contextStall: false,
63
+ contextRequestCount: 0,
64
+ lastContextRequestAt: null,
65
+ lastContextRequestRole: null,
47
66
  invalidOutput: false,
67
+ errorCode: null,
48
68
  noProgress: false,
49
69
  outputTypes: [],
50
70
  rawOutputs: [],
51
71
  priorityOrder: [],
52
72
  toolResults: [],
73
+ interactiveOutcome: null,
74
+ checkpoint: null,
75
+ usageRecords: [],
53
76
  });
54
77
  });
55
78
 
@@ -105,11 +128,11 @@ describe('WorkerDecisionLoop', () => {
105
128
  expect(provider.runWorker).toHaveBeenCalledWith(
106
129
  expect.objectContaining({
107
130
  last_tool_results: [],
108
- runtime_selection: {
131
+ runtime_selection: expect.objectContaining({
109
132
  provider: 'custom',
110
133
  model: 'model-test',
111
134
  provider_config_ref: null,
112
- },
135
+ }),
113
136
  }),
114
137
  );
115
138
  expect(toolCaller.callTool).toHaveBeenCalledWith(
@@ -121,6 +144,47 @@ describe('WorkerDecisionLoop', () => {
121
144
  );
122
145
  });
123
146
 
147
+ it('GIVEN_runtime_selection_override_WHEN_executed_THEN_passes_override_to_provider', async () => {
148
+ const provider = makeProvider({ type: 'NOTE', content: 'ok' });
149
+ const toolCaller = makeToolCaller();
150
+ const loop = new WorkerDecisionLoop({
151
+ provider: provider as never,
152
+ toolCaller: toolCaller as never,
153
+ resolveRuntimeSelection: async () => ({
154
+ provider: 'custom',
155
+ model: 'tiny-model',
156
+ provider_config_env: 'ROLE_ENV',
157
+ provider_config_ref: 'role-token',
158
+ agent_config: {
159
+ command: 'custom',
160
+ args: ['run'],
161
+ },
162
+ }),
163
+ });
164
+
165
+ await loop.execute({
166
+ role: 'builder',
167
+ featureId: 'feature_a',
168
+ contextBundle: {},
169
+ instructions: 'build',
170
+ });
171
+
172
+ expect(provider.runWorker).toHaveBeenCalledWith(
173
+ expect.objectContaining({
174
+ runtime_selection: {
175
+ provider: 'custom',
176
+ model: 'tiny-model',
177
+ provider_config_env: 'ROLE_ENV',
178
+ provider_config_ref: 'role-token',
179
+ agent_config: {
180
+ command: 'custom',
181
+ args: ['run'],
182
+ },
183
+ },
184
+ }),
185
+ );
186
+ });
187
+
124
188
  it('GIVEN_planner_submission_with_unknown_plan_fields_WHEN_executed_THEN_drops_unrecognized_fields', async () => {
125
189
  const provider = makeProvider({
126
190
  type: 'PLAN_SUBMISSION',
@@ -192,51 +256,93 @@ describe('WorkerDecisionLoop', () => {
192
256
  );
193
257
  });
194
258
 
195
- it('GIVEN_non_planner_or_empty_plan_submission_WHEN_executed_THEN_ignores_submission', async () => {
259
+ it('GIVEN_planner_submission_during_unverified_intake_WHEN_executed_THEN_routes_to_question_create_instead_of_plan_submit', async () => {
196
260
  const provider = makeProvider({
197
- outputs: [
198
- {
199
- type: 'PLAN_SUBMISSION',
200
- plan_json: {},
201
- },
202
- {
203
- type: 'PLAN_SUBMISSION',
204
- plan_json: {
205
- feature_id: 'feature_a',
206
- plan_version: 1,
207
- },
208
- },
209
- ],
261
+ type: 'PLAN_SUBMISSION',
262
+ plan_json: {
263
+ feature_id: 'feature_a',
264
+ plan_version: 1,
265
+ summary: 'Should not submit while intake is unresolved',
266
+ },
210
267
  });
211
268
  const toolCaller = makeToolCaller();
212
269
  const loop = new WorkerDecisionLoop({
213
270
  provider: provider as never,
214
271
  toolCaller: toolCaller as never,
272
+ resolveRoleSessionId: () => 'planner-session-intake',
215
273
  });
216
274
 
217
275
  const result = await loop.execute({
218
- role: 'builder',
276
+ role: 'planner',
219
277
  featureId: 'feature_a',
220
- contextBundle: {},
221
- instructions: 'build',
278
+ contextBundle: {
279
+ plan: null,
280
+ state: {
281
+ front_matter: {
282
+ status: STATUS.BLOCKED,
283
+ intake: {
284
+ status: 'in_progress',
285
+ verified_manifest_version: null,
286
+ },
287
+ },
288
+ },
289
+ human_input: {
290
+ open_questions: [],
291
+ },
292
+ intake: {
293
+ verified_manifest: null,
294
+ summary: {
295
+ status: 'in_progress',
296
+ verified_manifest_version: null,
297
+ },
298
+ review: {
299
+ ambiguities: [
300
+ {
301
+ id: 'AMB-100',
302
+ status: 'open',
303
+ summary: 'Clarify the role-specific token telemetry contract.',
304
+ obligation_ids: ['OBL-100'],
305
+ },
306
+ ],
307
+ },
308
+ },
309
+ },
310
+ instructions: 'intake',
222
311
  });
223
312
 
224
313
  expect(result.planSubmission).toBe(false);
314
+ expect(result.questionRequested).toBe(true);
315
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
316
+ 'orchestrator',
317
+ TOOLS.FEATURE_QUESTION_CREATE,
318
+ expect.objectContaining({
319
+ feature_id: 'feature_a',
320
+ session_id: 'planner-session-intake',
321
+ phase: STATUS.INTAKE,
322
+ resume_status: STATUS.INTAKE,
323
+ resume_phase: STATUS.INTAKE,
324
+ details: expect.objectContaining({
325
+ ambiguity_ids: ['AMB-100'],
326
+ obligation_ids: ['OBL-100'],
327
+ recovery_reason: 'verified_manifest_required',
328
+ }),
329
+ }),
330
+ );
225
331
  expect(toolCaller.callTool).not.toHaveBeenCalledWith(
226
332
  'planner',
227
333
  TOOLS.PLAN_SUBMIT,
228
- expect.any(Object),
334
+ expect.anything(),
229
335
  );
230
336
  });
231
337
 
232
- it('GIVEN_builder_patch_output_WHEN_executed_THEN_routes_to_repo_apply_patch', async () => {
338
+ it('GIVEN_planner_submission_during_intake_with_open_question_WHEN_executed_THEN_suppresses_plan_submission', async () => {
233
339
  const provider = makeProvider({
234
- outputs: [
235
- {
236
- type: 'PATCH',
237
- diff: 'diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1 @@\n-a\n+b\n',
238
- },
239
- ],
340
+ type: 'PLAN_SUBMISSION',
341
+ plan_json: {
342
+ feature_id: 'feature_a',
343
+ plan_version: 1,
344
+ summary: 'Should be suppressed while a clarification question is open',
345
+ },
240
346
  });
241
347
  const toolCaller = makeToolCaller();
242
348
  const loop = new WorkerDecisionLoop({
@@ -245,30 +351,66 @@ describe('WorkerDecisionLoop', () => {
245
351
  });
246
352
 
247
353
  const result = await loop.execute({
248
- role: 'qa',
354
+ role: 'planner',
249
355
  featureId: 'feature_a',
250
- contextBundle: {},
251
- instructions: 'qa',
356
+ contextBundle: {
357
+ plan: null,
358
+ state: {
359
+ front_matter: {
360
+ status: STATUS.BLOCKED,
361
+ intake: {
362
+ status: 'awaiting_input',
363
+ verified_manifest_version: null,
364
+ },
365
+ },
366
+ },
367
+ human_input: {
368
+ open_questions: [{ question_id: 'q_existing' }],
369
+ },
370
+ intake: {
371
+ verified_manifest: null,
372
+ summary: {
373
+ status: 'awaiting_input',
374
+ verified_manifest_version: null,
375
+ },
376
+ review: {
377
+ ambiguities: [
378
+ {
379
+ id: 'AMB-102',
380
+ status: 'open',
381
+ summary: 'Clarify the provider budget label.',
382
+ obligation_ids: ['OBL-102'],
383
+ },
384
+ ],
385
+ },
386
+ },
387
+ },
388
+ instructions: 'intake',
252
389
  });
253
390
 
254
- expect(result.patchApplied).toBe(true);
255
- expect(toolCaller.callTool).toHaveBeenCalledWith(
256
- 'qa',
257
- TOOLS.REPO_APPLY_PATCH,
258
- expect.objectContaining({
259
- feature_id: 'feature_a',
260
- }),
391
+ expect(result.planSubmission).toBe(false);
392
+ expect(result.requestHandled).toBe(true);
393
+ expect(result.questionRequested).toBe(false);
394
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
395
+ 'planner',
396
+ TOOLS.PLAN_SUBMIT,
397
+ expect.anything(),
398
+ );
399
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
400
+ 'orchestrator',
401
+ TOOLS.FEATURE_QUESTION_CREATE,
402
+ expect.anything(),
261
403
  );
262
404
  });
263
405
 
264
- it('GIVEN_patch_without_diff_or_unsupported_role_WHEN_executed_THEN_skips_patch_routing', async () => {
406
+ it('GIVEN_planner_submission_during_intake_without_open_ambiguities_WHEN_executed_THEN_suppresses_plan_submission', async () => {
265
407
  const provider = makeProvider({
266
- outputs: [
267
- {
268
- type: 'PATCH',
269
- unified_diff: ' ',
270
- },
271
- ],
408
+ type: 'PLAN_SUBMISSION',
409
+ plan_json: {
410
+ feature_id: 'feature_a',
411
+ plan_version: 1,
412
+ summary: 'Should be suppressed until intake is verified',
413
+ },
272
414
  });
273
415
  const toolCaller = makeToolCaller();
274
416
  const loop = new WorkerDecisionLoop({
@@ -279,60 +421,107 @@ describe('WorkerDecisionLoop', () => {
279
421
  const result = await loop.execute({
280
422
  role: 'planner',
281
423
  featureId: 'feature_a',
282
- contextBundle: {},
283
- instructions: 'plan',
424
+ contextBundle: {
425
+ plan: null,
426
+ state: {
427
+ front_matter: {
428
+ status: STATUS.INTAKE,
429
+ intake: {
430
+ status: 'in_progress',
431
+ verified_manifest_version: null,
432
+ },
433
+ },
434
+ },
435
+ human_input: {
436
+ open_questions: [],
437
+ },
438
+ intake: {
439
+ verified_manifest: null,
440
+ summary: {
441
+ status: 'in_progress',
442
+ verified_manifest_version: null,
443
+ },
444
+ review: {
445
+ ambiguities: [],
446
+ },
447
+ },
448
+ },
449
+ instructions: 'intake',
284
450
  });
285
451
 
286
- expect(result.patchApplied).toBe(false);
452
+ expect(result.planSubmission).toBe(false);
453
+ expect(result.requestHandled).toBe(true);
454
+ expect(result.questionRequested).toBe(false);
287
455
  expect(toolCaller.callTool).not.toHaveBeenCalledWith(
288
456
  'planner',
289
- TOOLS.REPO_APPLY_PATCH,
290
- expect.any(Object),
457
+ TOOLS.PLAN_SUBMIT,
458
+ expect.anything(),
291
459
  );
292
460
  });
293
461
 
294
- it('GIVEN_note_output_WHEN_log_append_succeeds_THEN_marks_note_logged', async () => {
295
- const provider = makeProvider({
296
- type: 'NOTE',
297
- content: 'worker note',
298
- });
299
- const toolCaller = makeToolCaller();
462
+ it('GIVEN_planner_requests_context_refresh_before_amendment_WHEN_executed_THEN_uses_refreshed_plan_version', async () => {
463
+ const provider = makeSequentialProvider([
464
+ {
465
+ type: 'REQUEST',
466
+ request: { action: 'context_refresh' },
467
+ },
468
+ {
469
+ type: 'REQUEST',
470
+ request: {
471
+ action: 'amend_plan',
472
+ plan_json: {
473
+ feature_id: 'feature_a',
474
+ plan_version: 5,
475
+ },
476
+ },
477
+ },
478
+ ]);
479
+ const toolCaller = {
480
+ callTool: vi.fn(async (_role: string, toolName: string, _input?: unknown) => {
481
+ if (toolName === TOOLS.FEATURE_GET_CONTEXT) {
482
+ return { ok: true, data: { plan: { plan_version: 4 } } };
483
+ }
484
+ return { ok: true, data: { accepted: true, plan_version: 5 } };
485
+ }),
486
+ };
300
487
  const loop = new WorkerDecisionLoop({
301
488
  provider: provider as never,
302
489
  toolCaller: toolCaller as never,
303
490
  });
304
491
 
305
492
  const result = await loop.execute({
306
- role: 'builder',
493
+ role: 'planner',
307
494
  featureId: 'feature_a',
308
- contextBundle: {},
309
- instructions: 'note',
495
+ contextBundle: { plan: { plan_version: 3 } },
496
+ instructions: 'plan',
310
497
  });
311
498
 
312
- expect(result.noteLogged).toBe(true);
499
+ expect(result.requestHandled).toBe(true);
500
+ expect(result.planSubmission).toBe(true);
501
+ expect(result.contextRequestCount).toBe(1);
502
+ expect(provider.runWorker).toHaveBeenCalledTimes(2);
313
503
  expect(toolCaller.callTool).toHaveBeenCalledWith(
314
- 'orchestrator',
315
- TOOLS.FEATURE_LOG_APPEND,
504
+ 'planner',
505
+ TOOLS.PLAN_UPDATE,
316
506
  expect.objectContaining({
317
507
  feature_id: 'feature_a',
318
- note: 'worker note',
508
+ expected_plan_version: 4,
319
509
  }),
320
510
  );
321
511
  });
322
512
 
323
- it('GIVEN_note_output_WHEN_log_append_fails_THEN_swallows_error_and_keeps_note_unset', async () => {
324
- const provider = makeProvider({
325
- type: 'NOTE',
326
- metadata: { info: 'fallback-json' },
327
- });
328
- const toolCaller = {
329
- callTool: vi.fn(async (role: string, toolName: string) => {
330
- if (role === 'orchestrator' && toolName === TOOLS.FEATURE_LOG_APPEND) {
331
- throw new Error('append_failed');
332
- }
333
- return { ok: true, data: {} };
334
- }),
335
- };
513
+ it('GIVEN_context_refresh_request_WHEN_executed_THEN_retries_same_turn_with_refreshed_context', async () => {
514
+ const provider = makeSequentialProvider([
515
+ {
516
+ type: 'REQUEST',
517
+ request: { action: 'more_context' },
518
+ },
519
+ {
520
+ type: 'NOTE',
521
+ content: 'refreshed context applied',
522
+ },
523
+ ]);
524
+ const toolCaller = makeToolCaller();
336
525
  const loop = new WorkerDecisionLoop({
337
526
  provider: provider as never,
338
527
  toolCaller: toolCaller as never,
@@ -341,22 +530,44 @@ describe('WorkerDecisionLoop', () => {
341
530
  const result = await loop.execute({
342
531
  role: 'qa',
343
532
  featureId: 'feature_a',
344
- contextBundle: {},
345
- instructions: 'note',
533
+ contextBundle: { stale: true },
534
+ instructions: 'context',
346
535
  });
347
536
 
348
- expect(result.noteLogged).toBe(false);
349
- expect(result.toolResults).toEqual([]);
537
+ expect(result.contextRequestCount).toBe(1);
538
+ expect(result.contextStall).toBe(false);
539
+ expect(result.noteLogged).toBe(true);
540
+ expect(result.noProgress).toBe(true);
541
+ expect(provider.runWorker).toHaveBeenCalledTimes(2);
542
+ expect(provider.runWorker).toHaveBeenNthCalledWith(
543
+ 2,
544
+ expect.objectContaining({
545
+ context_bundle: { refreshed: true },
546
+ last_tool_results: [
547
+ expect.objectContaining({
548
+ tool_name: TOOLS.FEATURE_GET_CONTEXT,
549
+ feature_id: 'feature_a',
550
+ }),
551
+ ],
552
+ }),
553
+ );
350
554
  });
351
555
 
352
- it('GIVEN_request_prioritize_with_empty_order_WHEN_executed_THEN_request_remains_unhandled', async () => {
353
- const provider = makeProvider({
354
- type: 'REQUEST',
355
- request: {
356
- action: 'prioritize',
357
- feature_order: [null, ' '],
556
+ it('GIVEN_repeated_context_refresh_requests_WHEN_executed_THEN_marks_context_stall', async () => {
557
+ const provider = makeSequentialProvider([
558
+ {
559
+ type: 'REQUEST',
560
+ request: { action: 'context_refresh' },
358
561
  },
359
- });
562
+ {
563
+ outputs: [
564
+ {
565
+ type: 'REQUEST',
566
+ request: { action: 'context_refresh' },
567
+ },
568
+ ],
569
+ },
570
+ ]);
360
571
  const toolCaller = makeToolCaller();
361
572
  const loop = new WorkerDecisionLoop({
362
573
  provider: provider as never,
@@ -364,106 +575,143 @@ describe('WorkerDecisionLoop', () => {
364
575
  });
365
576
 
366
577
  const result = await loop.execute({
367
- role: 'orchestrator',
368
- featureId: 'global',
578
+ role: 'builder',
579
+ featureId: 'feature_a',
369
580
  contextBundle: {},
370
- instructions: 'prioritize',
581
+ instructions: 'build',
371
582
  });
372
583
 
373
- expect(result.requestHandled).toBe(false);
374
- expect(result.priorityOrder).toEqual([]);
584
+ expect(result.contextStall).toBe(true);
585
+ expect(result.errorCode).toBe('provider_context_stall');
586
+ expect(result.noProgress).toBe(true);
587
+ expect(result.contextRequestCount).toBe(2);
588
+ const contextCalls = toolCaller.callTool.mock.calls.filter(
589
+ (call) => call[1] === TOOLS.FEATURE_GET_CONTEXT,
590
+ );
591
+ expect(contextCalls).toHaveLength(1);
375
592
  });
376
593
 
377
- it('GIVEN_request_lock_acquire_and_release_WHEN_executed_THEN_routes_lock_operations', async () => {
594
+ it('GIVEN_worker_requests_user_input_WHEN_executed_THEN_creates_structured_question', async () => {
378
595
  const provider = makeProvider({
379
- outputs: [
380
- {
381
- type: 'REQUEST',
382
- request: { action: 'lock_acquire', resources: ['openapi'] },
383
- },
384
- {
385
- type: 'REQUEST',
386
- request: { action: 'lock_release', resources: ['openapi'] },
596
+ type: 'REQUEST',
597
+ request: {
598
+ action: 'ask_user_input',
599
+ question_type: 'external_decision',
600
+ prompt: 'Choose release strategy',
601
+ details: { impact: 'changes deployment timing' },
602
+ expected_answer: {
603
+ kind: 'single_choice',
604
+ choices: ['ship_now', 'hold'],
387
605
  },
388
- ],
606
+ },
389
607
  });
390
- const toolCaller = makeToolCaller();
608
+ const toolCaller = {
609
+ callTool: vi.fn(async (_role: string, toolName: string, input?: Record<string, unknown>) => {
610
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
611
+ return {
612
+ ok: true,
613
+ data: {
614
+ question_id: 'q_123',
615
+ prompt: input?.prompt,
616
+ },
617
+ };
618
+ }
619
+ return { ok: true, data: { accepted: true } };
620
+ }),
621
+ };
391
622
  const loop = new WorkerDecisionLoop({
392
623
  provider: provider as never,
393
624
  toolCaller: toolCaller as never,
625
+ resolveRoleSessionId: () => 'session-build-1',
394
626
  });
395
627
 
396
628
  const result = await loop.execute({
397
629
  role: 'builder',
398
630
  featureId: 'feature_a',
399
- contextBundle: {},
400
- instructions: 'locks',
631
+ contextBundle: {
632
+ state: {
633
+ front_matter: {
634
+ status: 'building',
635
+ },
636
+ },
637
+ },
638
+ instructions: 'build',
401
639
  });
402
640
 
403
641
  expect(result.requestHandled).toBe(true);
642
+ expect(result.questionRequested).toBe(true);
643
+ expect(result.noProgress).toBe(false);
404
644
  expect(toolCaller.callTool).toHaveBeenCalledWith(
405
645
  'orchestrator',
406
- TOOLS.LOCKS_ACQUIRE,
407
- expect.objectContaining({ feature_id: 'feature_a', resources: ['openapi'] }),
408
- );
409
- expect(toolCaller.callTool).toHaveBeenCalledWith(
410
- 'orchestrator',
411
- TOOLS.LOCKS_RELEASE,
412
- expect.objectContaining({ feature_id: 'feature_a', resources: ['openapi'] }),
646
+ TOOLS.FEATURE_QUESTION_CREATE,
647
+ expect.objectContaining({
648
+ feature_id: 'feature_a',
649
+ role: 'builder',
650
+ session_id: 'session-build-1',
651
+ question_type: 'external_decision',
652
+ prompt: 'Choose release strategy',
653
+ phase: 'building',
654
+ expected_answer: {
655
+ kind: 'single_choice',
656
+ choices: ['ship_now', 'hold'],
657
+ },
658
+ }),
413
659
  );
414
660
  });
415
661
 
416
- it('GIVEN_lock_requests_with_missing_resources_WHEN_executed_THEN_skips_lock_tool_calls', async () => {
417
- const provider = makeProvider({
418
- outputs: [
419
- {
420
- type: 'REQUEST',
421
- request: { action: 'lock_acquire', resources: [] },
422
- },
423
- {
424
- type: 'REQUEST',
425
- request: { action: 'lock_release', resources: [null] },
426
- },
427
- ],
428
- });
662
+ it('GIVEN_planner_blocked_in_unverified_intake_WHEN_executed_THEN_runs_with_intake_phase', async () => {
663
+ const provider = makeProvider({ type: 'NOTE', content: 'intake retry' });
429
664
  const toolCaller = makeToolCaller();
430
665
  const loop = new WorkerDecisionLoop({
431
666
  provider: provider as never,
432
667
  toolCaller: toolCaller as never,
433
668
  });
434
669
 
435
- const result = await loop.execute({
670
+ await loop.execute({
436
671
  role: 'planner',
437
672
  featureId: 'feature_a',
438
- contextBundle: {},
439
- instructions: 'locks',
673
+ contextBundle: {
674
+ state: {
675
+ front_matter: {
676
+ status: STATUS.BLOCKED,
677
+ intake: {
678
+ status: 'in_progress',
679
+ bootstrap_manifest_version: 1,
680
+ verified_manifest_version: null,
681
+ },
682
+ },
683
+ },
684
+ intake: {
685
+ summary: {
686
+ status: 'in_progress',
687
+ verified_manifest_version: null,
688
+ },
689
+ verified_manifest: null,
690
+ },
691
+ },
692
+ instructions: 'intake recovery',
440
693
  });
441
694
 
442
- expect(result.requestHandled).toBe(false);
443
- expect(toolCaller.callTool).not.toHaveBeenCalledWith(
444
- 'orchestrator',
445
- TOOLS.LOCKS_ACQUIRE,
446
- expect.any(Object),
447
- );
448
- expect(toolCaller.callTool).not.toHaveBeenCalledWith(
449
- 'orchestrator',
450
- TOOLS.LOCKS_RELEASE,
451
- expect.any(Object),
695
+ expect(provider.runWorker).toHaveBeenCalledWith(
696
+ expect.objectContaining({
697
+ planner_phase: 'intake',
698
+ }),
452
699
  );
453
700
  });
454
701
 
455
- it('GIVEN_context_refresh_requests_WHEN_executed_THEN_requests_feature_context', async () => {
702
+ it('GIVEN_planner_intake_submission_WHEN_executed_THEN_routes_to_feature_intake_submit', async () => {
456
703
  const provider = makeProvider({
457
- outputs: [
458
- {
459
- type: 'REQUEST',
460
- request: { action: 'more_context' },
461
- },
462
- {
463
- type: 'REQUEST',
464
- request: { request_type: 'context_refresh' },
704
+ type: 'INTAKE_SUBMISSION',
705
+ intake_submission: {
706
+ verified_manifest: {
707
+ feature_id: 'feature_a',
708
+ manifest_version: 1,
709
+ obligations: [{ obligation_id: 'OBL-001', name: 'Ship intake gating' }],
465
710
  },
466
- ],
711
+ resolved_ambiguities: ['AMB-001'],
712
+ open_ambiguities: [],
713
+ requires_user_input: false,
714
+ },
467
715
  });
468
716
  const toolCaller = makeToolCaller();
469
717
  const loop = new WorkerDecisionLoop({
@@ -472,28 +720,33 @@ describe('WorkerDecisionLoop', () => {
472
720
  });
473
721
 
474
722
  const result = await loop.execute({
475
- role: 'qa',
723
+ role: 'planner',
476
724
  featureId: 'feature_a',
477
725
  contextBundle: {},
478
- instructions: 'context',
726
+ instructions: 'intake',
479
727
  });
480
728
 
481
- expect(result.requestHandled).toBe(true);
482
- const contextCalls = toolCaller.callTool.mock.calls.filter(
483
- (call) => call[1] === TOOLS.FEATURE_GET_CONTEXT,
729
+ expect(result.intakeSubmission).toBe(true);
730
+ expect(result.planSubmission).toBe(false);
731
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
732
+ 'planner',
733
+ TOOLS.FEATURE_INTAKE_SUBMIT,
734
+ expect.objectContaining({
735
+ feature_id: 'feature_a',
736
+ intake_submission: expect.objectContaining({
737
+ requires_user_input: false,
738
+ }),
739
+ }),
484
740
  );
485
- expect(contextCalls).toHaveLength(2);
486
741
  });
487
742
 
488
- it('GIVEN_planner_amend_plan_request_WHEN_existing_version_missing_THEN_submits_new_plan', async () => {
743
+ it('GIVEN_planner_submission_with_verified_manifest_WHEN_executed_THEN_allows_plan_submit', async () => {
489
744
  const provider = makeProvider({
490
- type: 'REQUEST',
491
- request: {
492
- action: 'amend_plan',
493
- plan_json: {
494
- feature_id: 'feature_a',
495
- plan_version: 1,
496
- },
745
+ type: 'PLAN_SUBMISSION',
746
+ plan_json: {
747
+ feature_id: 'feature_a',
748
+ plan_version: 1,
749
+ summary: 'Planning can begin after verified intake.',
497
750
  },
498
751
  });
499
752
  const toolCaller = makeToolCaller();
@@ -505,175 +758,279 @@ describe('WorkerDecisionLoop', () => {
505
758
  const result = await loop.execute({
506
759
  role: 'planner',
507
760
  featureId: 'feature_a',
508
- contextBundle: { plan: null },
509
- instructions: 'amend',
761
+ contextBundle: {
762
+ state: {
763
+ front_matter: {
764
+ status: STATUS.INTAKE,
765
+ },
766
+ },
767
+ intake: {
768
+ verified_manifest: {
769
+ feature_id: 'feature_a',
770
+ manifest_version: 1,
771
+ obligations: [{ obligation_id: 'OBL-001', name: 'Verified contract' }],
772
+ },
773
+ },
774
+ },
775
+ instructions: 'planning',
510
776
  });
511
777
 
512
- expect(result.requestHandled).toBe(true);
513
778
  expect(result.planSubmission).toBe(true);
514
779
  expect(toolCaller.callTool).toHaveBeenCalledWith(
515
780
  'planner',
516
781
  TOOLS.PLAN_SUBMIT,
517
- expect.objectContaining({ feature_id: 'feature_a' }),
782
+ expect.objectContaining({
783
+ feature_id: 'feature_a',
784
+ }),
518
785
  );
519
786
  });
520
787
 
521
- it('GIVEN_planner_amend_plan_with_unknown_plan_fields_WHEN_executed_THEN_strips_unrecognized_fields', async () => {
788
+ it('GIVEN_planner_emits_question_and_intake_submission_WHEN_executed_THEN_prioritizes_the_question_and_stops_the_turn', async () => {
522
789
  const provider = makeProvider({
523
- type: 'REQUEST',
524
- request: {
525
- action: 'amend_plan',
526
- plan_json: {
527
- feature_id: 'feature_a',
528
- plan_version: 1,
529
- summary: 'Plan with extra metadata',
530
- phase_notes: ['drop me'],
531
- gate_profile: 'merge',
790
+ outputs: [
791
+ {
792
+ type: 'REQUEST',
793
+ request: {
794
+ action: 'ask_user_input',
795
+ question_type: 'clarification',
796
+ prompt: 'Choose the provider/model truthfulness contract.',
797
+ details: {
798
+ ambiguity_ids: ['AMB-001', 'AMB-002'],
799
+ obligation_ids: ['OBL-001'],
800
+ },
801
+ expected_answer: { kind: 'free_text' },
802
+ blocking: true,
803
+ },
532
804
  },
533
- },
805
+ {
806
+ type: 'INTAKE_SUBMISSION',
807
+ intake_submission: {
808
+ verified_manifest: {
809
+ feature_id: 'feature_a',
810
+ manifest_version: 1,
811
+ obligations: [{ obligation_id: 'OBL-001', name: 'Should be ignored' }],
812
+ },
813
+ resolved_ambiguities: [],
814
+ open_ambiguities: [],
815
+ requires_user_input: false,
816
+ },
817
+ },
818
+ ],
534
819
  });
535
- const toolCaller = makeToolCaller();
820
+ const toolCaller = {
821
+ callTool: vi.fn(async (_role: string, toolName: string) => {
822
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
823
+ return {
824
+ ok: true,
825
+ data: {
826
+ question_id: 'q_intake_1',
827
+ },
828
+ };
829
+ }
830
+ return { ok: true, data: { accepted: true } };
831
+ }),
832
+ };
536
833
  const loop = new WorkerDecisionLoop({
537
834
  provider: provider as never,
538
835
  toolCaller: toolCaller as never,
836
+ resolveRoleSessionId: () => 'session-planner-1',
539
837
  });
540
838
 
541
839
  const result = await loop.execute({
542
840
  role: 'planner',
543
841
  featureId: 'feature_a',
544
- contextBundle: { plan: null },
545
- instructions: 'amend',
842
+ contextBundle: {
843
+ state: {
844
+ front_matter: {
845
+ status: 'intake',
846
+ },
847
+ },
848
+ },
849
+ instructions: 'intake',
546
850
  });
547
851
 
548
- expect(result.requestHandled).toBe(true);
549
- expect(result.planSubmission).toBe(true);
550
- const planSubmitCall = toolCaller.callTool.mock.calls.find(
551
- (call) => call[0] === 'planner' && call[1] === TOOLS.PLAN_SUBMIT,
852
+ expect(result.questionRequested).toBe(true);
853
+ expect(result.intakeSubmission).toBe(false);
854
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
855
+ 'orchestrator',
856
+ TOOLS.FEATURE_QUESTION_CREATE,
857
+ expect.objectContaining({
858
+ feature_id: 'feature_a',
859
+ }),
860
+ );
861
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
862
+ 'planner',
863
+ TOOLS.FEATURE_INTAKE_SUBMIT,
864
+ expect.any(Object),
552
865
  );
553
- expect(planSubmitCall).toBeDefined();
554
- const payload = planSubmitCall?.[2] as { plan_json?: Record<string, unknown> };
555
- expect(payload.plan_json?.phase_notes).toBeUndefined();
556
- expect(payload.plan_json?.gate_profile).toBeUndefined();
557
866
  });
558
867
 
559
- it('GIVEN_planner_amend_plan_request_WHEN_existing_version_present_THEN_updates_plan', async () => {
868
+ it('GIVEN_planner_requests_user_input_while_blocked_in_intake_WHEN_executed_THEN_question_resumes_to_intake', async () => {
560
869
  const provider = makeProvider({
561
870
  type: 'REQUEST',
562
871
  request: {
563
- action: 'amend_plan',
564
- expected_plan_version: 3,
565
- plan_json: {
566
- feature_id: 'feature_a',
567
- plan_version: 4,
872
+ action: 'ask_user_input',
873
+ question_type: 'clarification',
874
+ prompt: 'Clarify the intake ambiguity.',
875
+ details: {
876
+ ambiguity_ids: ['AMB-101'],
568
877
  },
878
+ expected_answer: { kind: 'free_text' },
879
+ blocking: true,
569
880
  },
570
881
  });
571
882
  const toolCaller = makeToolCaller();
572
883
  const loop = new WorkerDecisionLoop({
573
884
  provider: provider as never,
574
885
  toolCaller: toolCaller as never,
886
+ resolveRoleSessionId: () => 'session-planner-intake',
575
887
  });
576
888
 
577
- const result = await loop.execute({
889
+ await loop.execute({
578
890
  role: 'planner',
579
891
  featureId: 'feature_a',
580
- contextBundle: { plan: { plan_version: 2 } },
581
- instructions: 'amend',
892
+ contextBundle: {
893
+ state: {
894
+ front_matter: {
895
+ status: STATUS.BLOCKED,
896
+ intake: {
897
+ status: 'in_progress',
898
+ bootstrap_manifest_version: 1,
899
+ verified_manifest_version: null,
900
+ },
901
+ },
902
+ },
903
+ intake: {
904
+ summary: {
905
+ status: 'in_progress',
906
+ verified_manifest_version: null,
907
+ },
908
+ verified_manifest: null,
909
+ },
910
+ },
911
+ instructions: 'intake',
582
912
  });
583
913
 
584
- expect(result.requestHandled).toBe(true);
585
- expect(result.planSubmission).toBe(true);
586
914
  expect(toolCaller.callTool).toHaveBeenCalledWith(
587
- 'planner',
588
- TOOLS.PLAN_UPDATE,
915
+ 'orchestrator',
916
+ TOOLS.FEATURE_QUESTION_CREATE,
589
917
  expect.objectContaining({
590
918
  feature_id: 'feature_a',
591
- expected_plan_version: 3,
919
+ phase: STATUS.INTAKE,
920
+ resume_status: STATUS.INTAKE,
921
+ resume_phase: STATUS.INTAKE,
592
922
  }),
593
923
  );
594
924
  });
595
925
 
596
- it('GIVEN_planner_amend_plan_without_plan_payload_WHEN_executed_THEN_skips_amendment', async () => {
926
+ it('GIVEN_user_input_request_without_prompt_WHEN_executed_THEN_skips_question_creation', async () => {
597
927
  const provider = makeProvider({
598
928
  type: 'REQUEST',
599
929
  request: {
600
- action: 'amend_plan',
601
- plan_json: {},
930
+ action: 'ask_user_input',
931
+ question_type: 'clarification',
932
+ prompt: ' ',
602
933
  },
603
934
  });
604
935
  const toolCaller = makeToolCaller();
605
936
  const loop = new WorkerDecisionLoop({
606
937
  provider: provider as never,
607
938
  toolCaller: toolCaller as never,
939
+ resolveRoleSessionId: () => 'session-qa-1',
608
940
  });
609
941
 
610
942
  const result = await loop.execute({
611
- role: 'planner',
943
+ role: 'qa',
612
944
  featureId: 'feature_a',
613
- contextBundle: { plan: { plan_version: 2 } },
614
- instructions: 'amend',
945
+ contextBundle: {
946
+ state: {
947
+ front_matter: {
948
+ status: 'qa',
949
+ },
950
+ },
951
+ },
952
+ instructions: 'qa',
615
953
  });
616
954
 
617
955
  expect(result.requestHandled).toBe(false);
618
- expect(result.planSubmission).toBe(false);
619
- expect(toolCaller.callTool).not.toHaveBeenCalledWith(
620
- 'planner',
621
- TOOLS.PLAN_UPDATE,
622
- expect.any(Object),
623
- );
956
+ expect(result.questionRequested).toBe(false);
624
957
  expect(toolCaller.callTool).not.toHaveBeenCalledWith(
625
- 'planner',
626
- TOOLS.PLAN_SUBMIT,
958
+ 'orchestrator',
959
+ TOOLS.FEATURE_QUESTION_CREATE,
627
960
  expect.any(Object),
628
961
  );
629
962
  });
630
963
 
631
- it('GIVEN_amend_plan_without_payload_or_non_planner_role_WHEN_executed_THEN_ignores_request', async () => {
964
+ it('GIVEN_planner_emits_multiple_plan_mutations_in_one_turn_WHEN_executed_THEN_applies_only_the_first_mutation', async () => {
632
965
  const provider = makeProvider({
633
966
  outputs: [
634
967
  {
635
- type: 'REQUEST',
636
- request: {
637
- action: 'amend_plan',
638
- plan_json: {},
968
+ type: 'PLAN_SUBMISSION',
969
+ plan_json: {
970
+ feature_id: 'feature_a',
971
+ plan_version: 4,
972
+ summary: 'First revision',
639
973
  },
640
974
  },
641
975
  {
642
976
  type: 'REQUEST',
643
977
  request: {
644
978
  action: 'amend_plan',
979
+ expected_plan_version: 3,
645
980
  plan_json: {
646
981
  feature_id: 'feature_a',
982
+ plan_version: 4,
647
983
  },
648
984
  },
649
985
  },
650
986
  ],
651
987
  });
652
- const toolCaller = makeToolCaller();
988
+ const toolCaller = {
989
+ callTool: vi.fn(async (_role: string, toolName: string, _input?: unknown) => {
990
+ if (toolName === TOOLS.PLAN_UPDATE) {
991
+ return { ok: true, data: { accepted: true, plan_version: 4 } };
992
+ }
993
+ return { ok: true, data: {} };
994
+ }),
995
+ };
653
996
  const loop = new WorkerDecisionLoop({
654
997
  provider: provider as never,
655
998
  toolCaller: toolCaller as never,
656
999
  });
657
1000
 
658
1001
  const result = await loop.execute({
659
- role: 'builder',
1002
+ role: 'planner',
660
1003
  featureId: 'feature_a',
661
- contextBundle: {},
662
- instructions: 'amend',
1004
+ contextBundle: { plan: { plan_version: 3 } },
1005
+ instructions: 'plan',
663
1006
  });
664
1007
 
665
- expect(result.planSubmission).toBe(false);
1008
+ expect(result.planSubmission).toBe(true);
666
1009
  expect(result.requestHandled).toBe(false);
1010
+ const planUpdateCalls = toolCaller.callTool.mock.calls.filter(
1011
+ (call) => call[1] === TOOLS.PLAN_UPDATE,
1012
+ );
1013
+ expect(planUpdateCalls).toHaveLength(1);
1014
+ expect(planUpdateCalls[0]?.[2]).toMatchObject({
1015
+ feature_id: 'feature_a',
1016
+ expected_plan_version: 3,
1017
+ });
667
1018
  });
668
1019
 
669
- it('GIVEN_unknown_or_malformed_outputs_WHEN_executed_THEN_ignores_them_without_crashing', async () => {
1020
+ it('GIVEN_non_planner_or_empty_plan_submission_WHEN_executed_THEN_ignores_submission', async () => {
670
1021
  const provider = makeProvider({
671
1022
  outputs: [
672
- null,
673
- {},
674
- { type: 'UNSUPPORTED' },
675
- { type: 'REQUEST', request: {} },
676
- { type: 'NOTE', content: 'x' },
1023
+ {
1024
+ type: 'PLAN_SUBMISSION',
1025
+ plan_json: {},
1026
+ },
1027
+ {
1028
+ type: 'PLAN_SUBMISSION',
1029
+ plan_json: {
1030
+ feature_id: 'feature_a',
1031
+ plan_version: 1,
1032
+ },
1033
+ },
677
1034
  ],
678
1035
  });
679
1036
  const toolCaller = makeToolCaller();
@@ -683,76 +1040,60 @@ describe('WorkerDecisionLoop', () => {
683
1040
  });
684
1041
 
685
1042
  const result = await loop.execute({
686
- role: 'orchestrator',
687
- featureId: 'global',
1043
+ role: 'builder',
1044
+ featureId: 'feature_a',
688
1045
  contextBundle: {},
689
- instructions: 'noop',
1046
+ instructions: 'build',
690
1047
  });
691
1048
 
692
- expect(result.noteLogged).toBe(true);
693
1049
  expect(result.planSubmission).toBe(false);
694
- expect(result.patchApplied).toBe(false);
1050
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1051
+ 'planner',
1052
+ TOOLS.PLAN_SUBMIT,
1053
+ expect.any(Object),
1054
+ );
695
1055
  });
696
1056
 
697
- it('extracts orchestrator prioritize requests into deterministic priority ordering', async () => {
1057
+ it('GIVEN_builder_patch_output_WHEN_executed_THEN_routes_to_repo_apply_patch', async () => {
698
1058
  const provider = makeProvider({
699
- type: 'REQUEST',
700
- request: {
701
- action: 'prioritize',
702
- feature_order: ['feature_c', 'feature_a'],
703
- },
704
- });
705
-
706
- const toolCaller = makeToolCaller();
707
- const loop = new WorkerDecisionLoop({
1059
+ outputs: [
1060
+ {
1061
+ type: 'PATCH',
1062
+ diff: 'diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1 @@\n-a\n+b\n',
1063
+ },
1064
+ ],
1065
+ });
1066
+ const toolCaller = makeToolCaller();
1067
+ const loop = new WorkerDecisionLoop({
708
1068
  provider: provider as never,
709
1069
  toolCaller: toolCaller as never,
710
1070
  });
711
1071
 
712
1072
  const result = await loop.execute({
713
- role: 'orchestrator',
714
- featureId: 'global',
715
- contextBundle: {
716
- active_feature_ids: ['feature_a', 'feature_b', 'feature_c'],
717
- },
718
- instructions: 'prioritize',
1073
+ role: 'qa',
1074
+ featureId: 'feature_a',
1075
+ contextBundle: {},
1076
+ instructions: 'qa',
719
1077
  });
720
1078
 
721
- expect(result.requestHandled).toBe(true);
722
- expect(result.priorityOrder).toEqual(['feature_c', 'feature_a']);
723
- });
724
- });
725
-
726
- describe('WorkerDecisionLoop amend_plan revision_of branches', () => {
727
- function makeProvider(runWorkerResult: Record<string, unknown>) {
728
- return {
729
- selection: { provider: 'custom', model: 'model-test', provider_config_ref: null },
730
- runWorker: vi.fn(async () => runWorkerResult),
731
- };
732
- }
733
-
734
- function makeToolCaller() {
735
- return {
736
- callTool: vi.fn(async (_role: string, toolName: string, _input?: unknown) => {
737
- if (toolName === TOOLS.FEATURE_GET_CONTEXT) {
738
- return { ok: true, data: { refreshed: true } };
739
- }
740
- return { ok: true, data: { accepted: true } };
1079
+ expect(result.patchApplied).toBe(true);
1080
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1081
+ 'qa',
1082
+ TOOLS.REPO_APPLY_PATCH,
1083
+ expect.objectContaining({
1084
+ feature_id: 'feature_a',
741
1085
  }),
742
- };
743
- }
1086
+ );
1087
+ });
744
1088
 
745
- it('GIVEN_amend_plan_with_non_integer_revision_of_WHEN_existing_version_present_THEN_falls_back_to_existing_version', async () => {
1089
+ it('GIVEN_patch_without_diff_or_unsupported_role_WHEN_executed_THEN_skips_patch_routing', async () => {
746
1090
  const provider = makeProvider({
747
- type: 'REQUEST',
748
- request: {
749
- action: 'amend_plan',
750
- expected_plan_version: 'not-a-number',
751
- plan_json: {
752
- feature_id: 'feature_a',
753
- plan_version: 3,
1091
+ outputs: [
1092
+ {
1093
+ type: 'PATCH',
1094
+ unified_diff: ' ',
754
1095
  },
755
- },
1096
+ ],
756
1097
  });
757
1098
  const toolCaller = makeToolCaller();
758
1099
  const loop = new WorkerDecisionLoop({
@@ -763,195 +1104,2761 @@ describe('WorkerDecisionLoop amend_plan revision_of branches', () => {
763
1104
  const result = await loop.execute({
764
1105
  role: 'planner',
765
1106
  featureId: 'feature_a',
766
- contextBundle: { plan: { plan_version: 2 } },
767
- instructions: 'amend',
1107
+ contextBundle: {},
1108
+ instructions: 'plan',
768
1109
  });
769
1110
 
770
- expect(result.requestHandled).toBe(true);
771
- expect(result.planSubmission).toBe(true);
772
- expect(toolCaller.callTool).toHaveBeenCalledWith(
1111
+ expect(result.patchApplied).toBe(false);
1112
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
773
1113
  'planner',
774
- TOOLS.PLAN_UPDATE,
775
- expect.objectContaining({
776
- feature_id: 'feature_a',
777
- expected_plan_version: 2,
778
- }),
1114
+ TOOLS.REPO_APPLY_PATCH,
1115
+ expect.any(Object),
779
1116
  );
780
1117
  });
781
- });
782
-
783
- describe('WorkerDecisionLoop runtime event journaling runId branches', () => {
784
- function makeProvider(runWorkerResult: Record<string, unknown>) {
785
- return {
786
- selection: { provider: 'custom', model: 'model-test', provider_config_ref: null },
787
- runWorker: vi.fn(async () => runWorkerResult),
788
- };
789
- }
790
-
791
- function makeToolCaller() {
792
- return {
793
- callTool: vi.fn(async () => ({ ok: true, data: { accepted: true } })),
794
- };
795
- }
796
1118
 
797
- it('uses explicit string runId when journaling worker events', async () => {
798
- const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
1119
+ it('GIVEN_builder_patch_without_diff_WHEN_executed_THEN_skips_patch_routing', async () => {
1120
+ const provider = makeProvider({
1121
+ outputs: [
1122
+ {
1123
+ type: 'PATCH',
1124
+ },
1125
+ ],
1126
+ });
1127
+ const toolCaller = makeToolCaller();
799
1128
  const loop = new WorkerDecisionLoop({
800
- provider: makeProvider({ type: 'NOTE', content: 'hello' }) as never,
801
- toolCaller: makeToolCaller() as never,
802
- repoRoot,
803
- runId: 'run:string-id',
1129
+ provider: provider as never,
1130
+ toolCaller: toolCaller as never,
804
1131
  });
805
1132
 
806
- await loop.execute({
1133
+ const result = await loop.execute({
807
1134
  role: 'builder',
808
1135
  featureId: 'feature_a',
809
1136
  contextBundle: {},
810
- instructions: 'note',
1137
+ instructions: 'build',
811
1138
  });
812
1139
 
813
- const filePath = path.join(repoRoot, '.aop', 'runtime', 'worker-events', 'run:string-id.jsonl');
814
- await expect(fs.readFile(filePath, 'utf8')).resolves.toContain('"run_id":"run:string-id"');
1140
+ expect(result.patchApplied).toBe(false);
1141
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1142
+ 'builder',
1143
+ TOOLS.REPO_APPLY_PATCH,
1144
+ expect.any(Object),
1145
+ );
815
1146
  });
816
1147
 
817
- it('falls back to generated runId when configured runId string is empty', async () => {
818
- const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
1148
+ it('GIVEN_note_output_WHEN_log_append_succeeds_THEN_marks_note_logged', async () => {
1149
+ const provider = makeProvider({
1150
+ type: 'NOTE',
1151
+ content: 'worker note',
1152
+ });
1153
+ const toolCaller = makeToolCaller();
819
1154
  const loop = new WorkerDecisionLoop({
820
- provider: makeProvider({ type: 'NOTE', content: 'hello' }) as never,
821
- toolCaller: makeToolCaller() as never,
822
- repoRoot,
823
- runId: '',
1155
+ provider: provider as never,
1156
+ toolCaller: toolCaller as never,
824
1157
  });
825
1158
 
826
- await loop.execute({
1159
+ const result = await loop.execute({
827
1160
  role: 'builder',
828
1161
  featureId: 'feature_a',
829
1162
  contextBundle: {},
830
1163
  instructions: 'note',
831
1164
  });
832
1165
 
833
- const workerEventsDir = path.join(repoRoot, '.aop', 'runtime', 'worker-events');
834
- const files = await fs.readdir(workerEventsDir);
835
- expect(files.some((entry) => entry.startsWith('run:unknown:'))).toBe(true);
1166
+ expect(result.noteLogged).toBe(true);
1167
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1168
+ 'orchestrator',
1169
+ TOOLS.FEATURE_LOG_APPEND,
1170
+ expect.objectContaining({
1171
+ feature_id: 'feature_a',
1172
+ note: 'worker note',
1173
+ }),
1174
+ );
836
1175
  });
837
- });
838
1176
 
839
- describe('WorkerDecisionLoop runtime failure event classification', () => {
840
- function makeToolCaller() {
841
- return {
842
- callTool: vi.fn(async () => ({ ok: true, data: {} })),
1177
+ it('GIVEN_note_output_WHEN_log_append_fails_THEN_swallows_error_and_keeps_note_unset', async () => {
1178
+ const provider = makeProvider({
1179
+ type: 'NOTE',
1180
+ metadata: { info: 'fallback-json' },
1181
+ });
1182
+ const toolCaller = {
1183
+ callTool: vi.fn(async (role: string, toolName: string) => {
1184
+ if (role === 'orchestrator' && toolName === TOOLS.FEATURE_LOG_APPEND) {
1185
+ throw new Error('append_failed');
1186
+ }
1187
+ return { ok: true, data: {} };
1188
+ }),
843
1189
  };
844
- }
1190
+ const loop = new WorkerDecisionLoop({
1191
+ provider: provider as never,
1192
+ toolCaller: toolCaller as never,
1193
+ });
845
1194
 
846
- async function readWorkerEvents(
847
- repoRoot: string,
848
- runId: string,
849
- ): Promise<Record<string, unknown>[]> {
850
- const filePath = path.join(repoRoot, '.aop', 'runtime', 'worker-events', `${runId}.jsonl`);
851
- const content = await fs.readFile(filePath, 'utf8');
852
- return content
853
- .trim()
854
- .split('\n')
855
- .filter((line) => line.length > 0)
856
- .map((line) => JSON.parse(line) as Record<string, unknown>);
857
- }
1195
+ const result = await loop.execute({
1196
+ role: 'qa',
1197
+ featureId: 'feature_a',
1198
+ contextBundle: {},
1199
+ instructions: 'note',
1200
+ });
858
1201
 
859
- it('classifies timeout-family runtime failures as worker_timeout with watchdog metadata', async () => {
860
- const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-failure-'));
861
- const timeoutError = new Error('idle timeout') as Error & {
862
- code?: string;
863
- details?: Record<string, unknown>;
864
- };
865
- timeoutError.code = 'provider_stall_timeout';
866
- timeoutError.details = {
867
- watchdog_reason: 'idle_timeout',
868
- timeout_ms: 600000,
869
- idle_timeout_ms: 120000,
870
- spawn_timeout_ms: 15000,
871
- idle_for_ms: 120500,
872
- elapsed_ms: 130000,
873
- signal: 'SIGKILL',
874
- exit_code: 1,
875
- };
1202
+ expect(result.noteLogged).toBe(false);
1203
+ expect(result.toolResults).toEqual([]);
1204
+ });
876
1205
 
1206
+ it('GIVEN_request_prioritize_with_empty_order_WHEN_executed_THEN_request_remains_unhandled', async () => {
1207
+ const provider = makeProvider({
1208
+ type: 'REQUEST',
1209
+ request: {
1210
+ action: 'prioritize',
1211
+ feature_order: [null, ' '],
1212
+ },
1213
+ });
1214
+ const toolCaller = makeToolCaller();
877
1215
  const loop = new WorkerDecisionLoop({
878
- provider: {
879
- selection: { provider: 'codex', model: 'model-test', provider_config_ref: null },
880
- runWorker: vi.fn(async () => {
881
- throw timeoutError;
882
- }),
883
- } as never,
884
- toolCaller: makeToolCaller() as never,
885
- repoRoot,
886
- runId: 'run:timeout',
1216
+ provider: provider as never,
1217
+ toolCaller: toolCaller as never,
887
1218
  });
888
1219
 
889
- await expect(
890
- loop.execute({
891
- role: 'builder',
892
- featureId: 'feature-a',
893
- contextBundle: {},
894
- instructions: 'build',
895
- }),
896
- ).rejects.toMatchObject({ code: 'provider_stall_timeout' });
1220
+ const result = await loop.execute({
1221
+ role: 'orchestrator',
1222
+ featureId: 'global',
1223
+ contextBundle: {},
1224
+ instructions: 'prioritize',
1225
+ });
897
1226
 
898
- const events = await readWorkerEvents(repoRoot, 'run:timeout');
899
- expect(events.at(-1)).toMatchObject({
900
- event_type: 'worker_timeout',
901
- error_code: 'provider_stall_timeout',
902
- watchdog_reason: 'idle_timeout',
903
- timeout_ms: 600000,
904
- idle_timeout_ms: 120000,
905
- spawn_timeout_ms: 15000,
906
- idle_for_ms: 120500,
907
- elapsed_ms: 130000,
908
- signal: 'SIGKILL',
909
- exit_code: 1,
1227
+ expect(result.requestHandled).toBe(false);
1228
+ expect(result.priorityOrder).toEqual([]);
1229
+ });
1230
+
1231
+ it('GIVEN_request_lock_acquire_and_release_WHEN_executed_THEN_routes_lock_operations', async () => {
1232
+ const provider = makeProvider({
1233
+ outputs: [
1234
+ {
1235
+ type: 'REQUEST',
1236
+ request: { action: 'lock_acquire', resources: ['openapi'] },
1237
+ },
1238
+ {
1239
+ type: 'REQUEST',
1240
+ request: { action: 'lock_release', resources: ['openapi'] },
1241
+ },
1242
+ ],
1243
+ });
1244
+ const toolCaller = makeToolCaller();
1245
+ const loop = new WorkerDecisionLoop({
1246
+ provider: provider as never,
1247
+ toolCaller: toolCaller as never,
1248
+ });
1249
+
1250
+ const result = await loop.execute({
1251
+ role: 'builder',
1252
+ featureId: 'feature_a',
1253
+ contextBundle: {},
1254
+ instructions: 'locks',
1255
+ });
1256
+
1257
+ expect(result.requestHandled).toBe(true);
1258
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1259
+ 'orchestrator',
1260
+ TOOLS.LOCKS_ACQUIRE,
1261
+ expect.objectContaining({ feature_id: 'feature_a', resources: ['openapi'] }),
1262
+ );
1263
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1264
+ 'orchestrator',
1265
+ TOOLS.LOCKS_RELEASE,
1266
+ expect.objectContaining({ feature_id: 'feature_a', resources: ['openapi'] }),
1267
+ );
1268
+ });
1269
+
1270
+ it('GIVEN_lock_requests_with_missing_resources_WHEN_executed_THEN_skips_lock_tool_calls', async () => {
1271
+ const provider = makeProvider({
1272
+ outputs: [
1273
+ {
1274
+ type: 'REQUEST',
1275
+ request: { action: 'lock_acquire', resources: [] },
1276
+ },
1277
+ {
1278
+ type: 'REQUEST',
1279
+ request: { action: 'lock_release', resources: [null] },
1280
+ },
1281
+ ],
1282
+ });
1283
+ const toolCaller = makeToolCaller();
1284
+ const loop = new WorkerDecisionLoop({
1285
+ provider: provider as never,
1286
+ toolCaller: toolCaller as never,
1287
+ });
1288
+
1289
+ const result = await loop.execute({
1290
+ role: 'planner',
1291
+ featureId: 'feature_a',
1292
+ contextBundle: {},
1293
+ instructions: 'locks',
1294
+ });
1295
+
1296
+ expect(result.requestHandled).toBe(false);
1297
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1298
+ 'orchestrator',
1299
+ TOOLS.LOCKS_ACQUIRE,
1300
+ expect.any(Object),
1301
+ );
1302
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1303
+ 'orchestrator',
1304
+ TOOLS.LOCKS_RELEASE,
1305
+ expect.any(Object),
1306
+ );
1307
+ });
1308
+
1309
+ it('GIVEN_planner_amend_plan_request_WHEN_existing_version_missing_THEN_submits_new_plan', async () => {
1310
+ const provider = makeProvider({
1311
+ type: 'REQUEST',
1312
+ request: {
1313
+ action: 'amend_plan',
1314
+ plan_json: {
1315
+ feature_id: 'feature_a',
1316
+ plan_version: 1,
1317
+ },
1318
+ },
1319
+ });
1320
+ const toolCaller = makeToolCaller();
1321
+ const loop = new WorkerDecisionLoop({
1322
+ provider: provider as never,
1323
+ toolCaller: toolCaller as never,
1324
+ });
1325
+
1326
+ const result = await loop.execute({
1327
+ role: 'planner',
1328
+ featureId: 'feature_a',
1329
+ contextBundle: { plan: null },
1330
+ instructions: 'amend',
1331
+ });
1332
+
1333
+ expect(result.requestHandled).toBe(true);
1334
+ expect(result.planSubmission).toBe(true);
1335
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1336
+ 'planner',
1337
+ TOOLS.PLAN_SUBMIT,
1338
+ expect.objectContaining({ feature_id: 'feature_a' }),
1339
+ );
1340
+ });
1341
+
1342
+ it('GIVEN_planner_amend_plan_request_during_unverified_intake_WHEN_executed_THEN_routes_to_question_create_instead_of_plan_update', async () => {
1343
+ const provider = makeProvider({
1344
+ type: 'REQUEST',
1345
+ request: {
1346
+ action: 'amend_plan',
1347
+ plan_json: {
1348
+ feature_id: 'feature_a',
1349
+ plan_version: 2,
1350
+ summary: 'Should not amend a plan while intake is unresolved',
1351
+ },
1352
+ expected_plan_version: 1,
1353
+ },
1354
+ });
1355
+ const toolCaller = makeToolCaller();
1356
+ const loop = new WorkerDecisionLoop({
1357
+ provider: provider as never,
1358
+ toolCaller: toolCaller as never,
1359
+ resolveRoleSessionId: () => 'planner-session-intake',
1360
+ });
1361
+
1362
+ const result = await loop.execute({
1363
+ role: 'planner',
1364
+ featureId: 'feature_a',
1365
+ contextBundle: {
1366
+ plan: {
1367
+ plan_version: 1,
1368
+ },
1369
+ state: {
1370
+ front_matter: {
1371
+ status: STATUS.INTAKE,
1372
+ intake: {
1373
+ status: 'in_progress',
1374
+ verified_manifest_version: null,
1375
+ },
1376
+ },
1377
+ },
1378
+ human_input: {
1379
+ open_questions: [],
1380
+ },
1381
+ intake: {
1382
+ verified_manifest: null,
1383
+ summary: {
1384
+ status: 'in_progress',
1385
+ verified_manifest_version: null,
1386
+ },
1387
+ review: {
1388
+ ambiguities: [
1389
+ {
1390
+ id: 'AMB-101',
1391
+ status: 'open',
1392
+ summary: 'Clarify the persisted usage burn aggregation window.',
1393
+ obligation_ids: ['OBL-101'],
1394
+ },
1395
+ ],
1396
+ },
1397
+ },
1398
+ },
1399
+ instructions: 'intake',
1400
+ });
1401
+
1402
+ expect(result.requestHandled).toBe(true);
1403
+ expect(result.planSubmission).toBe(false);
1404
+ expect(result.questionRequested).toBe(true);
1405
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1406
+ 'orchestrator',
1407
+ TOOLS.FEATURE_QUESTION_CREATE,
1408
+ expect.objectContaining({
1409
+ feature_id: 'feature_a',
1410
+ details: expect.objectContaining({
1411
+ ambiguity_ids: ['AMB-101'],
1412
+ obligation_ids: ['OBL-101'],
1413
+ recovery_reason: 'verified_manifest_required',
1414
+ }),
1415
+ }),
1416
+ );
1417
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1418
+ 'planner',
1419
+ TOOLS.PLAN_UPDATE,
1420
+ expect.anything(),
1421
+ );
1422
+ });
1423
+
1424
+ it('GIVEN_unverified_intake_with_prior_clarification_answers_WHEN_amend_plan_is_rerouted_THEN_follow_up_prompt_includes_prior_answer_evidence', async () => {
1425
+ const provider = makeProvider({
1426
+ type: 'REQUEST',
1427
+ request: {
1428
+ action: 'amend_plan',
1429
+ plan_json: {
1430
+ feature_id: 'feature_a',
1431
+ plan_version: 2,
1432
+ summary: 'Should not amend a plan while intake is unresolved',
1433
+ },
1434
+ expected_plan_version: 1,
1435
+ },
1436
+ });
1437
+ const toolCaller = makeToolCaller();
1438
+ const loop = new WorkerDecisionLoop({
1439
+ provider: provider as never,
1440
+ toolCaller: toolCaller as never,
1441
+ resolveRoleSessionId: () => 'planner-session-intake',
1442
+ });
1443
+
1444
+ const result = await loop.execute({
1445
+ role: 'planner',
1446
+ featureId: 'feature_a',
1447
+ contextBundle: {
1448
+ plan: {
1449
+ plan_version: 1,
1450
+ },
1451
+ state: {
1452
+ front_matter: {
1453
+ status: STATUS.INTAKE,
1454
+ intake: {
1455
+ status: 'in_progress',
1456
+ verified_manifest_version: null,
1457
+ },
1458
+ },
1459
+ },
1460
+ human_input: {
1461
+ open_questions: [],
1462
+ },
1463
+ intake: {
1464
+ verified_manifest: null,
1465
+ summary: {
1466
+ status: 'in_progress',
1467
+ verified_manifest_version: null,
1468
+ },
1469
+ review: {
1470
+ ambiguities: [
1471
+ {
1472
+ id: 'AMB-101',
1473
+ status: 'open',
1474
+ summary: 'Clarify the persisted usage burn aggregation window.',
1475
+ obligation_ids: ['OBL-101'],
1476
+ },
1477
+ ],
1478
+ clarification_answers: [
1479
+ {
1480
+ question_id: 'q_prior',
1481
+ ambiguity_ids: ['AMB-101'],
1482
+ answer: {
1483
+ aggregation_window: 'daily',
1484
+ },
1485
+ answered_at: '2026-01-01T00:05:00.000Z',
1486
+ },
1487
+ ],
1488
+ },
1489
+ },
1490
+ },
1491
+ instructions: 'intake',
1492
+ });
1493
+
1494
+ expect(result.requestHandled).toBe(true);
1495
+ expect(result.questionRequested).toBe(true);
1496
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1497
+ 'orchestrator',
1498
+ TOOLS.FEATURE_QUESTION_CREATE,
1499
+ expect.objectContaining({
1500
+ feature_id: 'feature_a',
1501
+ prompt: expect.stringContaining('Prior answered clarification evidence:'),
1502
+ }),
1503
+ );
1504
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1505
+ 'orchestrator',
1506
+ TOOLS.FEATURE_QUESTION_CREATE,
1507
+ expect.objectContaining({
1508
+ prompt: expect.stringContaining('"aggregation_window": "daily"'),
1509
+ }),
1510
+ );
1511
+ });
1512
+
1513
+ it('GIVEN_intake_submission_resolves_unsupported_ambiguities_WHEN_validation_rejects_THEN_reroutes_to_clarification_question', async () => {
1514
+ const provider = makeProvider({
1515
+ type: 'INTAKE_SUBMISSION',
1516
+ intake_submission: {
1517
+ verified_manifest: {
1518
+ feature_id: 'feature_a',
1519
+ manifest_version: 1,
1520
+ artifact_type: 'verified',
1521
+ verification_basis: 'questions_resolved',
1522
+ verified_at: '2026-01-01T00:00:00.000Z',
1523
+ source_bootstrap_version: 1,
1524
+ obligations: [],
1525
+ },
1526
+ open_ambiguities: [],
1527
+ resolved_ambiguities: [
1528
+ {
1529
+ id: 'AMB-021',
1530
+ summary: 'Clarify the provider profile selection boundary.',
1531
+ status: 'resolved',
1532
+ obligation_ids: ['OBL-021'],
1533
+ },
1534
+ ],
1535
+ requires_user_input: false,
1536
+ },
1537
+ });
1538
+ const toolCaller = makeToolCaller();
1539
+ toolCaller.callTool.mockImplementation(
1540
+ async (_role: string, toolName: string, _input?: unknown) => {
1541
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
1542
+ throw {
1543
+ normalizedResponse: {
1544
+ error: {
1545
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
1546
+ message: 'resolved intake ambiguities must map to answered intake questions',
1547
+ details: {
1548
+ missing_ambiguity_evidence_ids: ['AMB-021'],
1549
+ },
1550
+ },
1551
+ },
1552
+ };
1553
+ }
1554
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
1555
+ return { ok: true, data: { accepted: true, question_id: 'q_follow_up' } };
1556
+ }
1557
+ return { ok: true, data: { accepted: true } };
1558
+ },
1559
+ );
1560
+ const loop = new WorkerDecisionLoop({
1561
+ provider: provider as never,
1562
+ toolCaller: toolCaller as never,
1563
+ resolveRoleSessionId: () => 'planner-session-intake',
1564
+ });
1565
+
1566
+ const result = await loop.execute({
1567
+ role: 'planner',
1568
+ featureId: 'feature_a',
1569
+ contextBundle: {
1570
+ state: {
1571
+ front_matter: {
1572
+ status: STATUS.INTAKE,
1573
+ intake: {
1574
+ status: 'in_progress',
1575
+ verified_manifest_version: null,
1576
+ },
1577
+ },
1578
+ },
1579
+ human_input: {
1580
+ open_questions: [],
1581
+ },
1582
+ intake: {
1583
+ verified_manifest: null,
1584
+ summary: {
1585
+ status: 'in_progress',
1586
+ verified_manifest_version: null,
1587
+ },
1588
+ review: {
1589
+ ambiguities: [
1590
+ {
1591
+ id: 'AMB-021',
1592
+ status: 'resolved',
1593
+ summary: 'Clarify the provider profile selection boundary.',
1594
+ obligation_ids: ['OBL-021'],
1595
+ },
1596
+ ],
1597
+ clarification_answers: [],
1598
+ },
1599
+ },
1600
+ },
1601
+ instructions: 'intake',
1602
+ });
1603
+
1604
+ expect(result.intakeSubmission).toBe(false);
1605
+ expect(result.requestHandled).toBe(true);
1606
+ expect(result.questionRequested).toBe(true);
1607
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1608
+ 'orchestrator',
1609
+ TOOLS.FEATURE_QUESTION_CREATE,
1610
+ expect.objectContaining({
1611
+ feature_id: 'feature_a',
1612
+ prompt: expect.stringContaining(
1613
+ 'AMB-021: Clarify the provider profile selection boundary.',
1614
+ ),
1615
+ details: expect.objectContaining({
1616
+ ambiguity_ids: ['AMB-021'],
1617
+ recovery_reason: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
1618
+ recovery_source: 'intake_submission_validation',
1619
+ }),
1620
+ }),
1621
+ );
1622
+ });
1623
+
1624
+ it('GIVEN_intake_submission_invalid_with_prior_answered_clarification_WHEN_rerouted_THEN_includes_prior_answer_evidence', async () => {
1625
+ const provider = makeProvider({
1626
+ type: 'INTAKE_SUBMISSION',
1627
+ intake_submission: {
1628
+ verified_manifest: {
1629
+ feature_id: 'feature_a',
1630
+ manifest_version: 1,
1631
+ artifact_type: 'verified',
1632
+ verification_basis: 'questions_resolved',
1633
+ verified_at: '2026-01-01T00:00:00.000Z',
1634
+ source_bootstrap_version: 1,
1635
+ obligations: [],
1636
+ },
1637
+ open_ambiguities: [],
1638
+ resolved_ambiguities: [],
1639
+ requires_user_input: false,
1640
+ },
1641
+ });
1642
+ const toolCaller = makeToolCaller();
1643
+ toolCaller.callTool.mockImplementation(
1644
+ async (_role: string, toolName: string, _input?: unknown) => {
1645
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
1646
+ throw {
1647
+ normalizedResponse: {
1648
+ error: {
1649
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
1650
+ message: 'resolved intake ambiguities must map to answered intake questions',
1651
+ details: {
1652
+ missing_ambiguity_evidence_ids: ['AMB-021'],
1653
+ },
1654
+ },
1655
+ },
1656
+ };
1657
+ }
1658
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
1659
+ return { ok: true, data: { accepted: true, question_id: 'q_follow_up' } };
1660
+ }
1661
+ return { ok: true, data: { accepted: true } };
1662
+ },
1663
+ );
1664
+ const loop = new WorkerDecisionLoop({
1665
+ provider: provider as never,
1666
+ toolCaller: toolCaller as never,
1667
+ resolveRoleSessionId: () => 'planner-session-intake',
1668
+ });
1669
+
1670
+ const result = await loop.execute({
1671
+ role: 'planner',
1672
+ featureId: 'feature_a',
1673
+ contextBundle: {
1674
+ state: {
1675
+ front_matter: {
1676
+ status: STATUS.INTAKE,
1677
+ },
1678
+ },
1679
+ human_input: {
1680
+ open_questions: [],
1681
+ },
1682
+ intake: {
1683
+ verified_manifest: null,
1684
+ review: {
1685
+ ambiguities: [
1686
+ {
1687
+ id: 'AMB-021',
1688
+ status: 'resolved',
1689
+ summary: 'Clarify the provider profile selection boundary.',
1690
+ obligation_ids: ['OBL-021'],
1691
+ },
1692
+ ],
1693
+ clarification_answers: [
1694
+ {
1695
+ question_id: 'q_prior',
1696
+ ambiguity_ids: ['AMB-021'],
1697
+ answer: {
1698
+ selected_profile: 'planner-fast',
1699
+ },
1700
+ answered_at: '2026-01-01T00:05:00.000Z',
1701
+ },
1702
+ ],
1703
+ },
1704
+ summary: {
1705
+ status: 'in_progress',
1706
+ verified_manifest_version: null,
1707
+ },
1708
+ },
1709
+ },
1710
+ instructions: 'intake',
1711
+ });
1712
+
1713
+ expect(result.questionRequested).toBe(true);
1714
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1715
+ 'orchestrator',
1716
+ TOOLS.FEATURE_QUESTION_CREATE,
1717
+ expect.objectContaining({
1718
+ prompt: expect.stringContaining('Prior answered clarification evidence:'),
1719
+ details: expect.objectContaining({
1720
+ prior_answered_ambiguity_ids: ['AMB-021'],
1721
+ }),
1722
+ }),
1723
+ );
1724
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1725
+ 'orchestrator',
1726
+ TOOLS.FEATURE_QUESTION_CREATE,
1727
+ expect.objectContaining({
1728
+ prompt: expect.stringContaining('"selected_profile": "planner-fast"'),
1729
+ }),
1730
+ );
1731
+ });
1732
+
1733
+ it('GIVEN_intake_submission_invalid_with_existing_open_question_WHEN_executed_THEN_skips_duplicate_question_creation', async () => {
1734
+ const provider = makeProvider({
1735
+ type: 'INTAKE_SUBMISSION',
1736
+ intake_submission: {
1737
+ verified_manifest: {
1738
+ feature_id: 'feature_a',
1739
+ manifest_version: 1,
1740
+ artifact_type: 'verified',
1741
+ verification_basis: 'questions_resolved',
1742
+ verified_at: '2026-01-01T00:00:00.000Z',
1743
+ source_bootstrap_version: 1,
1744
+ obligations: [],
1745
+ },
1746
+ open_ambiguities: [],
1747
+ resolved_ambiguities: [],
1748
+ requires_user_input: false,
1749
+ },
1750
+ });
1751
+ const toolCaller = makeToolCaller();
1752
+ toolCaller.callTool.mockImplementation(
1753
+ async (_role: string, toolName: string, _input?: unknown) => {
1754
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
1755
+ throw {
1756
+ normalizedResponse: {
1757
+ error: {
1758
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
1759
+ message: 'resolved intake ambiguities must map to answered intake questions',
1760
+ details: {
1761
+ missing_ambiguity_evidence_ids: ['AMB-021'],
1762
+ },
1763
+ },
1764
+ },
1765
+ };
1766
+ }
1767
+ return { ok: true, data: { accepted: true } };
1768
+ },
1769
+ );
1770
+ const loop = new WorkerDecisionLoop({
1771
+ provider: provider as never,
1772
+ toolCaller: toolCaller as never,
1773
+ });
1774
+
1775
+ const result = await loop.execute({
1776
+ role: 'planner',
1777
+ featureId: 'feature_a',
1778
+ contextBundle: {
1779
+ state: {
1780
+ front_matter: {
1781
+ status: STATUS.INTAKE,
1782
+ },
1783
+ },
1784
+ human_input: {
1785
+ open_questions: [{ question_id: 'q_existing' }],
1786
+ },
1787
+ intake: {
1788
+ verified_manifest: null,
1789
+ review: {
1790
+ ambiguities: [
1791
+ {
1792
+ id: 'AMB-021',
1793
+ status: 'open',
1794
+ summary: 'Clarify the provider profile selection boundary.',
1795
+ obligation_ids: ['OBL-021'],
1796
+ },
1797
+ ],
1798
+ clarification_answers: [],
1799
+ },
1800
+ summary: {
1801
+ status: 'awaiting_input',
1802
+ verified_manifest_version: null,
1803
+ },
1804
+ },
1805
+ },
1806
+ instructions: 'intake',
1807
+ });
1808
+
1809
+ expect(result.requestHandled).toBe(true);
1810
+ expect(result.questionRequested).toBe(true);
1811
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1812
+ 'planner',
1813
+ TOOLS.FEATURE_INTAKE_SUBMIT,
1814
+ expect.anything(),
1815
+ );
1816
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1817
+ 'orchestrator',
1818
+ TOOLS.FEATURE_QUESTION_CREATE,
1819
+ expect.anything(),
1820
+ );
1821
+ });
1822
+
1823
+ it('GIVEN_planner_amend_plan_with_unknown_plan_fields_WHEN_executed_THEN_strips_unrecognized_fields', async () => {
1824
+ const provider = makeProvider({
1825
+ type: 'REQUEST',
1826
+ request: {
1827
+ action: 'amend_plan',
1828
+ plan_json: {
1829
+ feature_id: 'feature_a',
1830
+ plan_version: 1,
1831
+ summary: 'Plan with extra metadata',
1832
+ phase_notes: ['drop me'],
1833
+ gate_profile: 'merge',
1834
+ },
1835
+ },
1836
+ });
1837
+ const toolCaller = makeToolCaller();
1838
+ const loop = new WorkerDecisionLoop({
1839
+ provider: provider as never,
1840
+ toolCaller: toolCaller as never,
1841
+ });
1842
+
1843
+ const result = await loop.execute({
1844
+ role: 'planner',
1845
+ featureId: 'feature_a',
1846
+ contextBundle: { plan: null },
1847
+ instructions: 'amend',
1848
+ });
1849
+
1850
+ expect(result.requestHandled).toBe(true);
1851
+ expect(result.planSubmission).toBe(true);
1852
+ const planSubmitCall = toolCaller.callTool.mock.calls.find(
1853
+ (call) => call[0] === 'planner' && call[1] === TOOLS.PLAN_SUBMIT,
1854
+ );
1855
+ expect(planSubmitCall).toBeDefined();
1856
+ const payload = planSubmitCall?.[2] as { plan_json?: Record<string, unknown> };
1857
+ expect(payload.plan_json?.phase_notes).toBeUndefined();
1858
+ expect(payload.plan_json?.gate_profile).toBeUndefined();
1859
+ });
1860
+
1861
+ it('GIVEN_planner_amend_plan_request_WHEN_existing_version_present_THEN_updates_plan', async () => {
1862
+ const provider = makeProvider({
1863
+ type: 'REQUEST',
1864
+ request: {
1865
+ action: 'amend_plan',
1866
+ expected_plan_version: 3,
1867
+ plan_json: {
1868
+ feature_id: 'feature_a',
1869
+ plan_version: 4,
1870
+ },
1871
+ },
1872
+ });
1873
+ const toolCaller = makeToolCaller();
1874
+ const loop = new WorkerDecisionLoop({
1875
+ provider: provider as never,
1876
+ toolCaller: toolCaller as never,
1877
+ });
1878
+
1879
+ const result = await loop.execute({
1880
+ role: 'planner',
1881
+ featureId: 'feature_a',
1882
+ contextBundle: { plan: { plan_version: 2 } },
1883
+ instructions: 'amend',
1884
+ });
1885
+
1886
+ expect(result.requestHandled).toBe(true);
1887
+ expect(result.planSubmission).toBe(true);
1888
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
1889
+ 'planner',
1890
+ TOOLS.PLAN_UPDATE,
1891
+ expect.objectContaining({
1892
+ feature_id: 'feature_a',
1893
+ expected_plan_version: 3,
1894
+ }),
1895
+ );
1896
+ });
1897
+
1898
+ it('GIVEN_planner_amend_plan_without_plan_payload_WHEN_executed_THEN_skips_amendment', async () => {
1899
+ const provider = makeProvider({
1900
+ type: 'REQUEST',
1901
+ request: {
1902
+ action: 'amend_plan',
1903
+ plan_json: {},
1904
+ },
1905
+ });
1906
+ const toolCaller = makeToolCaller();
1907
+ const loop = new WorkerDecisionLoop({
1908
+ provider: provider as never,
1909
+ toolCaller: toolCaller as never,
1910
+ });
1911
+
1912
+ const result = await loop.execute({
1913
+ role: 'planner',
1914
+ featureId: 'feature_a',
1915
+ contextBundle: { plan: { plan_version: 2 } },
1916
+ instructions: 'amend',
1917
+ });
1918
+
1919
+ expect(result.requestHandled).toBe(false);
1920
+ expect(result.planSubmission).toBe(false);
1921
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1922
+ 'planner',
1923
+ TOOLS.PLAN_UPDATE,
1924
+ expect.any(Object),
1925
+ );
1926
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
1927
+ 'planner',
1928
+ TOOLS.PLAN_SUBMIT,
1929
+ expect.any(Object),
1930
+ );
1931
+ });
1932
+
1933
+ it('GIVEN_amend_plan_without_payload_or_non_planner_role_WHEN_executed_THEN_ignores_request', async () => {
1934
+ const provider = makeProvider({
1935
+ outputs: [
1936
+ {
1937
+ type: 'REQUEST',
1938
+ request: {
1939
+ action: 'amend_plan',
1940
+ plan_json: {},
1941
+ },
1942
+ },
1943
+ {
1944
+ type: 'REQUEST',
1945
+ request: {
1946
+ action: 'amend_plan',
1947
+ plan_json: {
1948
+ feature_id: 'feature_a',
1949
+ },
1950
+ },
1951
+ },
1952
+ ],
1953
+ });
1954
+ const toolCaller = makeToolCaller();
1955
+ const loop = new WorkerDecisionLoop({
1956
+ provider: provider as never,
1957
+ toolCaller: toolCaller as never,
1958
+ });
1959
+
1960
+ const result = await loop.execute({
1961
+ role: 'builder',
1962
+ featureId: 'feature_a',
1963
+ contextBundle: {},
1964
+ instructions: 'amend',
1965
+ });
1966
+
1967
+ expect(result.planSubmission).toBe(false);
1968
+ expect(result.requestHandled).toBe(false);
1969
+ });
1970
+
1971
+ it('GIVEN_unknown_or_malformed_outputs_WHEN_executed_THEN_ignores_them_without_crashing', async () => {
1972
+ const provider = makeProvider({
1973
+ outputs: [
1974
+ null,
1975
+ {},
1976
+ { type: 'UNSUPPORTED' },
1977
+ { type: 'REQUEST', request: {} },
1978
+ { type: 'NOTE', content: 'x' },
1979
+ ],
1980
+ });
1981
+ const toolCaller = makeToolCaller();
1982
+ const loop = new WorkerDecisionLoop({
1983
+ provider: provider as never,
1984
+ toolCaller: toolCaller as never,
1985
+ });
1986
+
1987
+ const result = await loop.execute({
1988
+ role: 'orchestrator',
1989
+ featureId: 'global',
1990
+ contextBundle: {},
1991
+ instructions: 'noop',
1992
+ });
1993
+
1994
+ expect(result.noteLogged).toBe(true);
1995
+ expect(result.planSubmission).toBe(false);
1996
+ expect(result.patchApplied).toBe(false);
1997
+ });
1998
+
1999
+ it('extracts orchestrator prioritize requests into deterministic priority ordering', async () => {
2000
+ const provider = makeProvider({
2001
+ type: 'REQUEST',
2002
+ request: {
2003
+ action: 'prioritize',
2004
+ feature_order: ['feature_c', 'feature_a'],
2005
+ },
2006
+ });
2007
+
2008
+ const toolCaller = makeToolCaller();
2009
+ const loop = new WorkerDecisionLoop({
2010
+ provider: provider as never,
2011
+ toolCaller: toolCaller as never,
2012
+ });
2013
+
2014
+ const result = await loop.execute({
2015
+ role: 'orchestrator',
2016
+ featureId: 'global',
2017
+ contextBundle: {
2018
+ active_feature_ids: ['feature_a', 'feature_b', 'feature_c'],
2019
+ },
2020
+ instructions: 'prioritize',
2021
+ });
2022
+
2023
+ expect(result.requestHandled).toBe(true);
2024
+ expect(result.priorityOrder).toEqual(['feature_c', 'feature_a']);
2025
+ });
2026
+
2027
+ it('GIVEN_provider_returns_usage_WHEN_executed_THEN_usageRecords_populated', async () => {
2028
+ const usageData = {
2029
+ status: 'reported',
2030
+ input_tokens: 500,
2031
+ output_tokens: 200,
2032
+ total_tokens: 700,
2033
+ provider: 'claude',
2034
+ model: 'claude-3',
2035
+ role: 'builder',
2036
+ estimated_cost_usd: 0.02,
2037
+ };
2038
+ const provider = makeProvider({
2039
+ outputs: [{ type: 'NOTE', content: 'done' }],
2040
+ usage: usageData,
2041
+ });
2042
+ const toolCaller = makeToolCaller();
2043
+ const loop = new WorkerDecisionLoop({
2044
+ provider: provider as never,
2045
+ toolCaller: toolCaller as never,
2046
+ });
2047
+
2048
+ const result = await loop.execute({
2049
+ role: 'builder',
2050
+ featureId: 'feature_usage',
2051
+ contextBundle: {},
2052
+ instructions: 'build',
2053
+ });
2054
+
2055
+ expect(result.usageRecords).toHaveLength(1);
2056
+ expect(result.usageRecords[0]).toMatchObject({
2057
+ status: 'reported',
2058
+ input_tokens: 500,
2059
+ output_tokens: 200,
2060
+ total_tokens: 700,
2061
+ });
2062
+ });
2063
+
2064
+ it('GIVEN_provider_returns_no_usage_WHEN_executed_THEN_usageRecords_empty', async () => {
2065
+ const provider = makeProvider({
2066
+ outputs: [{ type: 'NOTE', content: 'done' }],
2067
+ });
2068
+ const toolCaller = makeToolCaller();
2069
+ const loop = new WorkerDecisionLoop({
2070
+ provider: provider as never,
2071
+ toolCaller: toolCaller as never,
2072
+ });
2073
+
2074
+ const result = await loop.execute({
2075
+ role: 'builder',
2076
+ featureId: 'feature_no_usage',
2077
+ contextBundle: {},
2078
+ instructions: 'build',
2079
+ });
2080
+
2081
+ expect(result.usageRecords).toHaveLength(0);
2082
+ });
2083
+
2084
+ it('GIVEN_provider_returns_invalid_usage_shape_WHEN_executed_THEN_usageRecords_empty', async () => {
2085
+ const provider = makeProvider({
2086
+ outputs: [{ type: 'NOTE', content: 'done' }],
2087
+ usage: 'not-an-object',
2088
+ });
2089
+ const toolCaller = makeToolCaller();
2090
+ const loop = new WorkerDecisionLoop({
2091
+ provider: provider as never,
2092
+ toolCaller: toolCaller as never,
2093
+ });
2094
+
2095
+ const result = await loop.execute({
2096
+ role: 'builder',
2097
+ featureId: 'feature_bad_usage',
2098
+ contextBundle: {},
2099
+ instructions: 'build',
2100
+ });
2101
+
2102
+ expect(result.usageRecords).toHaveLength(0);
2103
+ });
2104
+
2105
+ it('GIVEN_provider_returns_usage_object_without_status_key_WHEN_executed_THEN_usageRecords_empty', async () => {
2106
+ const provider = makeProvider({
2107
+ outputs: [{ type: 'NOTE', content: 'done' }],
2108
+ usage: { input_tokens: 100, output_tokens: 50 },
2109
+ });
2110
+ const toolCaller = makeToolCaller();
2111
+ const loop = new WorkerDecisionLoop({
2112
+ provider: provider as never,
2113
+ toolCaller: toolCaller as never,
2114
+ });
2115
+
2116
+ const result = await loop.execute({
2117
+ role: 'builder',
2118
+ featureId: 'feature_no_status',
2119
+ contextBundle: {},
2120
+ instructions: 'build',
2121
+ });
2122
+
2123
+ expect(result.usageRecords).toHaveLength(0);
2124
+ });
2125
+
2126
+ it('GIVEN_provider_returns_usage_object_with_non_string_status_WHEN_executed_THEN_usageRecords_empty', async () => {
2127
+ const provider = makeProvider({
2128
+ outputs: [{ type: 'NOTE', content: 'done' }],
2129
+ usage: { status: 123, input_tokens: 100, output_tokens: 50 },
2130
+ });
2131
+ const toolCaller = makeToolCaller();
2132
+ const loop = new WorkerDecisionLoop({
2133
+ provider: provider as never,
2134
+ toolCaller: toolCaller as never,
2135
+ });
2136
+
2137
+ const result = await loop.execute({
2138
+ role: 'builder',
2139
+ featureId: 'feature_numeric_status',
2140
+ contextBundle: {},
2141
+ instructions: 'build',
2142
+ });
2143
+
2144
+ expect(result.usageRecords).toHaveLength(0);
2145
+ });
2146
+
2147
+ it('GIVEN_context_refresh_retry_with_usage_on_both_calls_WHEN_executed_THEN_collects_both_usage_records', async () => {
2148
+ const usageFirst = {
2149
+ status: 'reported',
2150
+ input_tokens: 100,
2151
+ output_tokens: 50,
2152
+ total_tokens: 150,
2153
+ provider: 'claude',
2154
+ model: 'claude-3',
2155
+ role: 'qa',
2156
+ estimated_cost_usd: 0.01,
2157
+ };
2158
+ const usageSecond = {
2159
+ status: 'reported',
2160
+ input_tokens: 200,
2161
+ output_tokens: 100,
2162
+ total_tokens: 300,
2163
+ provider: 'claude',
2164
+ model: 'claude-3',
2165
+ role: 'qa',
2166
+ estimated_cost_usd: 0.02,
2167
+ };
2168
+ const provider = makeSequentialProvider([
2169
+ {
2170
+ type: 'REQUEST',
2171
+ request: { action: 'more_context' },
2172
+ usage: usageFirst,
2173
+ },
2174
+ {
2175
+ type: 'NOTE',
2176
+ content: 'refreshed context applied',
2177
+ usage: usageSecond,
2178
+ },
2179
+ ]);
2180
+ const toolCaller = makeToolCaller();
2181
+ const loop = new WorkerDecisionLoop({
2182
+ provider: provider as never,
2183
+ toolCaller: toolCaller as never,
2184
+ });
2185
+
2186
+ const result = await loop.execute({
2187
+ role: 'qa',
2188
+ featureId: 'feature_retry_usage',
2189
+ contextBundle: { stale: true },
2190
+ instructions: 'context',
2191
+ });
2192
+
2193
+ expect(result.usageRecords).toHaveLength(2);
2194
+ expect(result.usageRecords[0]).toMatchObject({ input_tokens: 100 });
2195
+ expect(result.usageRecords[1]).toMatchObject({ input_tokens: 200 });
2196
+ });
2197
+
2198
+ it('GIVEN_builder_emits_PLAN_SUBMISSION_WHEN_executed_THEN_ignores_plan_submission_for_non_planner', async () => {
2199
+ const provider = makeProvider({
2200
+ type: 'PLAN_SUBMISSION',
2201
+ plan_json: {
2202
+ feature_id: 'feature_a',
2203
+ plan_version: 1,
2204
+ summary: 'builder cannot submit plans',
2205
+ },
2206
+ });
2207
+ const toolCaller = makeToolCaller();
2208
+ const loop = new WorkerDecisionLoop({
2209
+ provider: provider as never,
2210
+ toolCaller: toolCaller as never,
2211
+ });
2212
+
2213
+ const result = await loop.execute({
2214
+ role: 'builder',
2215
+ featureId: 'feature_a',
2216
+ contextBundle: {},
2217
+ instructions: 'build',
2218
+ });
2219
+
2220
+ expect(result.planSubmission).toBe(false);
2221
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
2222
+ 'planner',
2223
+ TOOLS.PLAN_SUBMIT,
2224
+ expect.anything(),
2225
+ );
2226
+ });
2227
+
2228
+ it('GIVEN_builder_emits_INTAKE_SUBMISSION_WHEN_executed_THEN_ignores_intake_submission_for_non_planner', async () => {
2229
+ const provider = makeProvider({
2230
+ type: 'INTAKE_SUBMISSION',
2231
+ intake_submission: {
2232
+ verified_manifest: { feature_id: 'feature_a' },
2233
+ },
2234
+ });
2235
+ const toolCaller = makeToolCaller();
2236
+ const loop = new WorkerDecisionLoop({
2237
+ provider: provider as never,
2238
+ toolCaller: toolCaller as never,
2239
+ });
2240
+
2241
+ const result = await loop.execute({
2242
+ role: 'builder',
2243
+ featureId: 'feature_a',
2244
+ contextBundle: {},
2245
+ instructions: 'build',
2246
+ });
2247
+
2248
+ expect(result.intakeSubmission).toBe(false);
2249
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
2250
+ 'planner',
2251
+ TOOLS.FEATURE_INTAKE_SUBMIT,
2252
+ expect.anything(),
2253
+ );
2254
+ });
2255
+
2256
+ it('GIVEN_planner_emits_PLAN_SUBMISSION_with_plan_field_instead_of_plan_json_WHEN_executed_THEN_routes_via_plan_fallback', async () => {
2257
+ const provider = makeProvider({
2258
+ type: 'PLAN_SUBMISSION',
2259
+ plan: {
2260
+ feature_id: 'feature_a',
2261
+ plan_version: 1,
2262
+ summary: 'Plan via plan field',
2263
+ },
2264
+ });
2265
+ const toolCaller = makeToolCaller();
2266
+ const loop = new WorkerDecisionLoop({
2267
+ provider: provider as never,
2268
+ toolCaller: toolCaller as never,
2269
+ });
2270
+
2271
+ const result = await loop.execute({
2272
+ role: 'planner',
2273
+ featureId: 'feature_a',
2274
+ contextBundle: {},
2275
+ instructions: 'plan',
2276
+ });
2277
+
2278
+ expect(result.planSubmission).toBe(true);
2279
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
2280
+ 'planner',
2281
+ TOOLS.PLAN_SUBMIT,
2282
+ expect.objectContaining({
2283
+ feature_id: 'feature_a',
2284
+ }),
2285
+ );
2286
+ });
2287
+
2288
+ it('GIVEN_planner_emits_PLAN_SUBMISSION_with_empty_plan_json_WHEN_executed_THEN_skips_plan_submission', async () => {
2289
+ const provider = makeProvider({
2290
+ type: 'PLAN_SUBMISSION',
2291
+ plan_json: {},
2292
+ });
2293
+ const toolCaller = makeToolCaller();
2294
+ const loop = new WorkerDecisionLoop({
2295
+ provider: provider as never,
2296
+ toolCaller: toolCaller as never,
2297
+ });
2298
+
2299
+ const result = await loop.execute({
2300
+ role: 'planner',
2301
+ featureId: 'feature_a',
2302
+ contextBundle: {},
2303
+ instructions: 'plan',
2304
+ });
2305
+
2306
+ expect(result.planSubmission).toBe(false);
2307
+ expect(toolCaller.callTool).not.toHaveBeenCalledWith(
2308
+ 'planner',
2309
+ TOOLS.PLAN_SUBMIT,
2310
+ expect.anything(),
2311
+ );
2312
+ });
2313
+
2314
+ it('GIVEN_planner_emits_two_PLAN_SUBMISSIONS_WHEN_executed_THEN_only_first_mutation_applies', async () => {
2315
+ const provider = makeProvider({
2316
+ outputs: [
2317
+ {
2318
+ type: 'PLAN_SUBMISSION',
2319
+ plan_json: {
2320
+ feature_id: 'feature_a',
2321
+ plan_version: 1,
2322
+ summary: 'first plan',
2323
+ },
2324
+ },
2325
+ {
2326
+ type: 'PLAN_SUBMISSION',
2327
+ plan_json: {
2328
+ feature_id: 'feature_a',
2329
+ plan_version: 2,
2330
+ summary: 'second plan ignored',
2331
+ },
2332
+ },
2333
+ ],
2334
+ });
2335
+ const toolCaller = makeToolCaller();
2336
+ const loop = new WorkerDecisionLoop({
2337
+ provider: provider as never,
2338
+ toolCaller: toolCaller as never,
2339
+ });
2340
+
2341
+ const result = await loop.execute({
2342
+ role: 'planner',
2343
+ featureId: 'feature_a',
2344
+ contextBundle: {},
2345
+ instructions: 'plan',
2346
+ });
2347
+
2348
+ expect(result.planSubmission).toBe(true);
2349
+ const submitCalls = toolCaller.callTool.mock.calls.filter(
2350
+ (c: unknown[]) => c[1] === TOOLS.PLAN_SUBMIT,
2351
+ );
2352
+ expect(submitCalls).toHaveLength(1);
2353
+ });
2354
+
2355
+ it('GIVEN_planner_amend_plan_during_intake_with_verified_manifest_WHEN_executed_THEN_allows_plan_amendment', async () => {
2356
+ const provider = makeProvider({
2357
+ type: 'REQUEST',
2358
+ request: {
2359
+ action: 'amend_plan',
2360
+ plan_json: {
2361
+ feature_id: 'feature_a',
2362
+ plan_version: 2,
2363
+ summary: 'Amend plan after intake is verified',
2364
+ },
2365
+ expected_plan_version: 1,
2366
+ },
2367
+ });
2368
+ const toolCaller = makeToolCaller();
2369
+ const loop = new WorkerDecisionLoop({
2370
+ provider: provider as never,
2371
+ toolCaller: toolCaller as never,
2372
+ });
2373
+
2374
+ const result = await loop.execute({
2375
+ role: 'planner',
2376
+ featureId: 'feature_a',
2377
+ contextBundle: {
2378
+ plan: {
2379
+ plan_version: 1,
2380
+ },
2381
+ state: {
2382
+ front_matter: {
2383
+ status: STATUS.INTAKE,
2384
+ intake: {
2385
+ status: 'in_progress',
2386
+ verified_manifest_version: null,
2387
+ },
2388
+ },
2389
+ },
2390
+ human_input: {
2391
+ open_questions: [],
2392
+ },
2393
+ intake: {
2394
+ verified_manifest: {
2395
+ feature_id: 'feature_a',
2396
+ manifest_version: 1,
2397
+ obligations: [{ obligation_id: 'OBL-001', name: 'Already verified' }],
2398
+ },
2399
+ summary: {
2400
+ status: 'in_progress',
2401
+ verified_manifest_version: null,
2402
+ },
2403
+ review: {
2404
+ ambiguities: [],
2405
+ },
2406
+ },
2407
+ },
2408
+ instructions: 'intake',
2409
+ });
2410
+
2411
+ expect(result.planSubmission).toBe(true);
2412
+ expect(result.questionRequested).toBe(false);
2413
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
2414
+ 'planner',
2415
+ TOOLS.PLAN_UPDATE,
2416
+ expect.objectContaining({
2417
+ feature_id: 'feature_a',
2418
+ }),
2419
+ );
2420
+ });
2421
+
2422
+ it('GIVEN_activity_monitor_configured_WHEN_executed_THEN_checks_stuck_after_completion', async () => {
2423
+ const provider = makeProvider({
2424
+ outputs: [{ type: 'NOTE', content: 'done' }],
2425
+ });
2426
+ const toolCaller = makeToolCaller();
2427
+ const activityMonitor = {
2428
+ checkAndNotifyStuck: vi.fn(async () => undefined),
2429
+ };
2430
+ const loop = new WorkerDecisionLoop({
2431
+ provider: provider as never,
2432
+ toolCaller: toolCaller as never,
2433
+ activityMonitor: activityMonitor as never,
2434
+ });
2435
+
2436
+ await loop.execute({
2437
+ role: 'builder',
2438
+ featureId: 'feature_monitor',
2439
+ contextBundle: {},
2440
+ instructions: 'build',
2441
+ });
2442
+
2443
+ expect(activityMonitor.checkAndNotifyStuck).toHaveBeenCalledWith('feature_monitor');
2444
+ });
2445
+
2446
+ it('GIVEN_planner_intake_submission_fails_with_non_INTAKE_SUBMISSION_INVALID_error_WHEN_executed_THEN_rethrows_error', async () => {
2447
+ const provider = makeProvider({
2448
+ type: 'INTAKE_SUBMISSION',
2449
+ intake_submission: {
2450
+ verified_manifest: { feature_id: 'feature_a' },
2451
+ },
2452
+ });
2453
+ const toolCaller = makeToolCaller();
2454
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2455
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2456
+ throw {
2457
+ normalizedResponse: {
2458
+ error: {
2459
+ code: 'some_other_error',
2460
+ message: 'unexpected failure',
2461
+ details: {},
2462
+ },
2463
+ },
2464
+ };
2465
+ }
2466
+ return { ok: true, data: { accepted: true } };
2467
+ });
2468
+ const loop = new WorkerDecisionLoop({
2469
+ provider: provider as never,
2470
+ toolCaller: toolCaller as never,
2471
+ });
2472
+
2473
+ await expect(
2474
+ loop.execute({
2475
+ role: 'planner',
2476
+ featureId: 'feature_a',
2477
+ contextBundle: {
2478
+ state: {
2479
+ front_matter: {
2480
+ status: STATUS.INTAKE,
2481
+ },
2482
+ },
2483
+ intake: {
2484
+ verified_manifest: null,
2485
+ summary: { status: 'in_progress', verified_manifest_version: null },
2486
+ review: { ambiguities: [] },
2487
+ },
2488
+ },
2489
+ instructions: 'intake',
2490
+ }),
2491
+ ).rejects.toMatchObject({
2492
+ normalizedResponse: expect.objectContaining({
2493
+ error: expect.objectContaining({ code: 'some_other_error' }),
2494
+ }),
2495
+ });
2496
+ });
2497
+
2498
+ it('GIVEN_planner_intake_submission_invalid_without_session_resolver_WHEN_rerouted_THEN_falls_back_to_bootstrap_session_id', async () => {
2499
+ const provider = makeProvider({
2500
+ type: 'INTAKE_SUBMISSION',
2501
+ intake_submission: {
2502
+ verified_manifest: { feature_id: 'feature_a' },
2503
+ },
2504
+ });
2505
+ const toolCaller = makeToolCaller();
2506
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2507
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2508
+ throw {
2509
+ normalizedResponse: {
2510
+ error: {
2511
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
2512
+ message: 'missing ambiguity evidence',
2513
+ details: {
2514
+ missing_ambiguity_evidence_ids: ['AMB-101'],
2515
+ },
2516
+ },
2517
+ },
2518
+ };
2519
+ }
2520
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
2521
+ return { ok: true, data: { accepted: true, question_id: 'q_fallback' } };
2522
+ }
2523
+ return { ok: true, data: { accepted: true } };
2524
+ });
2525
+ const loop = new WorkerDecisionLoop({
2526
+ provider: provider as never,
2527
+ toolCaller: toolCaller as never,
2528
+ // No resolveRoleSessionId — should fall back to bootstrap:planner
2529
+ });
2530
+
2531
+ const result = await loop.execute({
2532
+ role: 'planner',
2533
+ featureId: 'feature_a',
2534
+ contextBundle: {
2535
+ state: {
2536
+ front_matter: {
2537
+ status: STATUS.INTAKE,
2538
+ },
2539
+ },
2540
+ human_input: {
2541
+ open_questions: [],
2542
+ },
2543
+ intake: {
2544
+ verified_manifest: null,
2545
+ summary: { status: 'in_progress', verified_manifest_version: null },
2546
+ review: {
2547
+ ambiguities: [
2548
+ {
2549
+ id: 'AMB-101',
2550
+ status: 'open',
2551
+ summary: 'Clarify the boundary.',
2552
+ obligation_ids: ['OBL-101'],
2553
+ },
2554
+ ],
2555
+ },
2556
+ },
2557
+ },
2558
+ instructions: 'intake',
2559
+ });
2560
+
2561
+ expect(result.questionRequested).toBe(true);
2562
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
2563
+ 'orchestrator',
2564
+ TOOLS.FEATURE_QUESTION_CREATE,
2565
+ expect.objectContaining({
2566
+ session_id: 'bootstrap:planner',
2567
+ }),
2568
+ );
2569
+ });
2570
+
2571
+ it('GIVEN_planner_intake_submission_throws_plain_error_WHEN_executed_THEN_rethrows_because_failure_details_are_null', async () => {
2572
+ const provider = makeProvider({
2573
+ type: 'INTAKE_SUBMISSION',
2574
+ intake_submission: {
2575
+ verified_manifest: { feature_id: 'feature_a' },
2576
+ },
2577
+ });
2578
+ const toolCaller = makeToolCaller();
2579
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2580
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2581
+ throw new Error('plain error');
2582
+ }
2583
+ return { ok: true, data: { accepted: true } };
2584
+ });
2585
+ const loop = new WorkerDecisionLoop({
2586
+ provider: provider as never,
2587
+ toolCaller: toolCaller as never,
2588
+ });
2589
+
2590
+ await expect(
2591
+ loop.execute({
2592
+ role: 'planner',
2593
+ featureId: 'feature_a',
2594
+ contextBundle: {
2595
+ state: { front_matter: { status: STATUS.INTAKE } },
2596
+ intake: {
2597
+ verified_manifest: null,
2598
+ summary: { status: 'in_progress', verified_manifest_version: null },
2599
+ review: { ambiguities: [] },
2600
+ },
2601
+ },
2602
+ instructions: 'intake',
2603
+ }),
2604
+ ).rejects.toThrow('plain error');
2605
+ });
2606
+
2607
+ it('GIVEN_intake_submission_invalid_with_empty_ambiguity_ids_WHEN_rerouted_THEN_falls_back_to_open_ambiguities', async () => {
2608
+ const provider = makeProvider({
2609
+ type: 'INTAKE_SUBMISSION',
2610
+ intake_submission: {
2611
+ verified_manifest: { feature_id: 'feature_a' },
2612
+ },
2613
+ });
2614
+ const toolCaller = makeToolCaller();
2615
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2616
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2617
+ throw {
2618
+ normalizedResponse: {
2619
+ error: {
2620
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
2621
+ message: 'no ambiguities specified',
2622
+ details: {},
2623
+ },
2624
+ },
2625
+ };
2626
+ }
2627
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
2628
+ return { ok: true, data: { accepted: true, question_id: 'q_open' } };
2629
+ }
2630
+ return { ok: true, data: { accepted: true } };
2631
+ });
2632
+ const loop = new WorkerDecisionLoop({
2633
+ provider: provider as never,
2634
+ toolCaller: toolCaller as never,
2635
+ });
2636
+
2637
+ const result = await loop.execute({
2638
+ role: 'planner',
2639
+ featureId: 'feature_a',
2640
+ contextBundle: {
2641
+ state: { front_matter: { status: STATUS.INTAKE } },
2642
+ human_input: { open_questions: [] },
2643
+ intake: {
2644
+ verified_manifest: null,
2645
+ summary: { status: 'in_progress', verified_manifest_version: null },
2646
+ review: {
2647
+ ambiguities: [
2648
+ {
2649
+ id: 'AMB-201',
2650
+ status: 'open',
2651
+ summary: 'Open ambiguity',
2652
+ obligation_ids: ['OBL-201'],
2653
+ },
2654
+ { id: 'AMB-202', status: 'resolved', summary: 'Resolved one', obligation_ids: [] },
2655
+ ],
2656
+ },
2657
+ },
2658
+ },
2659
+ instructions: 'intake',
2660
+ });
2661
+
2662
+ expect(result.questionRequested).toBe(true);
2663
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
2664
+ 'orchestrator',
2665
+ TOOLS.FEATURE_QUESTION_CREATE,
2666
+ expect.objectContaining({
2667
+ details: expect.objectContaining({
2668
+ ambiguity_ids: ['AMB-201'],
2669
+ }),
2670
+ }),
2671
+ );
2672
+ });
2673
+
2674
+ it('GIVEN_intake_submission_invalid_with_multiple_ambiguities_and_prior_answers_WHEN_rerouted_THEN_prompt_covers_all_ambiguities', async () => {
2675
+ const provider = makeProvider({
2676
+ type: 'INTAKE_SUBMISSION',
2677
+ intake_submission: { verified_manifest: { feature_id: 'feature_a' } },
2678
+ });
2679
+ const toolCaller = makeToolCaller();
2680
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2681
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2682
+ throw {
2683
+ normalizedResponse: {
2684
+ error: {
2685
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
2686
+ message: 'missing evidence',
2687
+ details: {
2688
+ missing_ambiguity_evidence_ids: ['AMB-301', 'AMB-302'],
2689
+ },
2690
+ },
2691
+ },
2692
+ };
2693
+ }
2694
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
2695
+ return { ok: true, data: { accepted: true, question_id: 'q_multi' } };
2696
+ }
2697
+ return { ok: true, data: { accepted: true } };
2698
+ });
2699
+ const loop = new WorkerDecisionLoop({
2700
+ provider: provider as never,
2701
+ toolCaller: toolCaller as never,
2702
+ });
2703
+
2704
+ const result = await loop.execute({
2705
+ role: 'planner',
2706
+ featureId: 'feature_a',
2707
+ contextBundle: {
2708
+ state: { front_matter: { status: STATUS.INTAKE } },
2709
+ human_input: { open_questions: [] },
2710
+ intake: {
2711
+ verified_manifest: null,
2712
+ summary: { status: 'in_progress', verified_manifest_version: null },
2713
+ review: {
2714
+ ambiguities: [
2715
+ {
2716
+ id: 'AMB-301',
2717
+ status: 'open',
2718
+ summary: 'First ambiguity',
2719
+ obligation_ids: ['OBL-301'],
2720
+ },
2721
+ {
2722
+ id: 'AMB-302',
2723
+ status: 'open',
2724
+ summary: 'Second ambiguity',
2725
+ obligation_ids: ['OBL-302'],
2726
+ },
2727
+ // Ambiguity missing summary — should be skipped by readAllIntakeAmbiguities
2728
+ { id: 'AMB-303', status: 'open', obligation_ids: [] },
2729
+ ],
2730
+ clarification_answers: [
2731
+ {
2732
+ // Missing question_id — falls back to 'unknown'
2733
+ ambiguity_ids: ['AMB-301'],
2734
+ // Non-string answer — gets JSON.stringified
2735
+ answer: { key: 'structured' },
2736
+ // Null answered_at — omits date from prompt line
2737
+ answered_at: null,
2738
+ },
2739
+ ],
2740
+ },
2741
+ },
2742
+ },
2743
+ instructions: 'intake',
2744
+ });
2745
+
2746
+ expect(result.questionRequested).toBe(true);
2747
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
2748
+ 'orchestrator',
2749
+ TOOLS.FEATURE_QUESTION_CREATE,
2750
+ expect.objectContaining({
2751
+ prompt: expect.stringContaining('AMB-301'),
2752
+ details: expect.objectContaining({
2753
+ ambiguity_ids: ['AMB-301', 'AMB-302'],
2754
+ prior_answered_ambiguity_ids: ['AMB-301'],
2755
+ }),
2756
+ }),
2757
+ );
2758
+ });
2759
+
2760
+ it('GIVEN_planner_intake_submission_throws_null_error_WHEN_executed_THEN_rethrows_because_failure_details_are_null', async () => {
2761
+ const provider = makeProvider({
2762
+ type: 'INTAKE_SUBMISSION',
2763
+ intake_submission: { verified_manifest: { feature_id: 'feature_a' } },
2764
+ });
2765
+ const toolCaller = makeToolCaller();
2766
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2767
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2768
+ throw null;
2769
+ }
2770
+ return { ok: true, data: { accepted: true } };
2771
+ });
2772
+ const loop = new WorkerDecisionLoop({
2773
+ provider: provider as never,
2774
+ toolCaller: toolCaller as never,
2775
+ });
2776
+
2777
+ await expect(
2778
+ loop.execute({
2779
+ role: 'planner',
2780
+ featureId: 'feature_a',
2781
+ contextBundle: {
2782
+ state: { front_matter: { status: STATUS.INTAKE } },
2783
+ intake: {
2784
+ verified_manifest: null,
2785
+ summary: { status: 'in_progress', verified_manifest_version: null },
2786
+ review: { ambiguities: [] },
2787
+ },
2788
+ },
2789
+ instructions: 'intake',
2790
+ }),
2791
+ ).rejects.toBeNull();
2792
+ });
2793
+
2794
+ it('GIVEN_intake_submission_invalid_with_ambiguity_missing_id_WHEN_rerouted_THEN_skips_malformed_ambiguity', async () => {
2795
+ const provider = makeProvider({
2796
+ type: 'INTAKE_SUBMISSION',
2797
+ intake_submission: { verified_manifest: { feature_id: 'feature_a' } },
2798
+ });
2799
+ const toolCaller = makeToolCaller();
2800
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2801
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2802
+ throw {
2803
+ normalizedResponse: {
2804
+ error: {
2805
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
2806
+ message: 'missing evidence',
2807
+ details: {},
2808
+ },
2809
+ },
2810
+ };
2811
+ }
2812
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
2813
+ return { ok: true, data: { accepted: true, question_id: 'q_skip' } };
2814
+ }
2815
+ return { ok: true, data: { accepted: true } };
2816
+ });
2817
+ const loop = new WorkerDecisionLoop({
2818
+ provider: provider as never,
2819
+ toolCaller: toolCaller as never,
2820
+ });
2821
+
2822
+ const result = await loop.execute({
2823
+ role: 'planner',
2824
+ featureId: 'feature_a',
2825
+ contextBundle: {
2826
+ state: { front_matter: { status: STATUS.INTAKE } },
2827
+ human_input: { open_questions: [] },
2828
+ intake: {
2829
+ verified_manifest: null,
2830
+ summary: { status: 'in_progress', verified_manifest_version: null },
2831
+ review: {
2832
+ ambiguities: [
2833
+ // Valid ambiguity
2834
+ { id: 'AMB-501', status: 'open', summary: 'Valid one', obligation_ids: ['OBL-501'] },
2835
+ // Missing id — should be skipped
2836
+ { status: 'open', summary: 'No id ambiguity', obligation_ids: [] },
2837
+ // Missing summary — should be skipped
2838
+ { id: 'AMB-503', status: 'open', obligation_ids: [] },
2839
+ // Non-array ambiguities context — tests fallback to empty array
2840
+ 'not-an-object',
2841
+ ],
2842
+ },
2843
+ },
2844
+ },
2845
+ instructions: 'intake',
2846
+ });
2847
+
2848
+ expect(result.questionRequested).toBe(true);
2849
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
2850
+ 'orchestrator',
2851
+ TOOLS.FEATURE_QUESTION_CREATE,
2852
+ expect.objectContaining({
2853
+ details: expect.objectContaining({
2854
+ ambiguity_ids: ['AMB-501'],
2855
+ }),
2856
+ }),
2857
+ );
2858
+ });
2859
+
2860
+ it('GIVEN_planner_amend_plan_during_intake_with_no_ambiguities_array_WHEN_executed_THEN_suppresses_plan_and_marks_handled', async () => {
2861
+ const provider = makeProvider({
2862
+ type: 'REQUEST',
2863
+ request: {
2864
+ action: 'amend_plan',
2865
+ plan_json: { feature_id: 'feature_a', plan_version: 2, summary: 'amend' },
2866
+ expected_plan_version: 1,
2867
+ },
2868
+ });
2869
+ const toolCaller = makeToolCaller();
2870
+ const loop = new WorkerDecisionLoop({
2871
+ provider: provider as never,
2872
+ toolCaller: toolCaller as never,
2873
+ });
2874
+
2875
+ const result = await loop.execute({
2876
+ role: 'planner',
2877
+ featureId: 'feature_a',
2878
+ contextBundle: {
2879
+ plan: { plan_version: 1 },
2880
+ state: {
2881
+ front_matter: {
2882
+ status: STATUS.INTAKE,
2883
+ intake: { status: 'in_progress', verified_manifest_version: null },
2884
+ },
2885
+ },
2886
+ human_input: 'not-an-object',
2887
+ intake: {
2888
+ verified_manifest: null,
2889
+ summary: { status: 'in_progress', verified_manifest_version: null },
2890
+ // No review.ambiguities — tests non-array fallback branches
2891
+ review: {},
2892
+ },
2893
+ },
2894
+ instructions: 'intake',
2895
+ });
2896
+
2897
+ expect(result.requestHandled).toBe(true);
2898
+ expect(result.planSubmission).toBe(false);
2899
+ expect(result.questionRequested).toBe(false);
2900
+ });
2901
+
2902
+ it('GIVEN_intake_submission_invalid_with_non_array_ambiguities_WHEN_recovery_routes_THEN_handles_gracefully', async () => {
2903
+ const provider = makeProvider({
2904
+ type: 'INTAKE_SUBMISSION',
2905
+ intake_submission: { verified_manifest: { feature_id: 'feature_a' } },
2906
+ });
2907
+ const toolCaller = makeToolCaller();
2908
+ toolCaller.callTool.mockImplementation(async (_role: string, toolName: string) => {
2909
+ if (toolName === TOOLS.FEATURE_INTAKE_SUBMIT) {
2910
+ throw {
2911
+ normalizedResponse: {
2912
+ error: {
2913
+ code: ERROR_CODES.INTAKE_SUBMISSION_INVALID,
2914
+ message: 'missing evidence',
2915
+ details: { open_ambiguity_ids: ['AMB-601'] },
2916
+ },
2917
+ },
2918
+ };
2919
+ }
2920
+ if (toolName === TOOLS.FEATURE_QUESTION_CREATE) {
2921
+ return { ok: true, data: { accepted: true, question_id: 'q_fallback' } };
2922
+ }
2923
+ return { ok: true, data: { accepted: true } };
2924
+ });
2925
+ const loop = new WorkerDecisionLoop({
2926
+ provider: provider as never,
2927
+ toolCaller: toolCaller as never,
2928
+ });
2929
+
2930
+ const result = await loop.execute({
2931
+ role: 'planner',
2932
+ featureId: 'feature_a',
2933
+ contextBundle: {
2934
+ state: { front_matter: { status: STATUS.INTAKE } },
2935
+ human_input: { open_questions: [] },
2936
+ intake: {
2937
+ verified_manifest: null,
2938
+ summary: { status: 'in_progress', verified_manifest_version: null },
2939
+ // review.ambiguities is not an array — tests non-array fallback
2940
+ review: { ambiguities: 'not-an-array' },
2941
+ },
2942
+ },
2943
+ instructions: 'intake',
2944
+ });
2945
+
2946
+ expect(result.questionRequested).toBe(true);
2947
+ // AMB-601 comes from error details, not from context ambiguities
2948
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
2949
+ 'orchestrator',
2950
+ TOOLS.FEATURE_QUESTION_CREATE,
2951
+ expect.objectContaining({
2952
+ details: expect.objectContaining({
2953
+ ambiguity_ids: ['AMB-601'],
2954
+ }),
2955
+ }),
2956
+ );
2957
+ });
2958
+
2959
+ it('GIVEN_planner_amend_plan_during_unverified_intake_without_session_resolver_WHEN_rerouted_THEN_uses_bootstrap_session_id', async () => {
2960
+ const provider = makeProvider({
2961
+ type: 'REQUEST',
2962
+ request: {
2963
+ action: 'amend_plan',
2964
+ plan_json: {
2965
+ feature_id: 'feature_a',
2966
+ plan_version: 2,
2967
+ summary: 'Amend attempt during intake',
2968
+ },
2969
+ expected_plan_version: 1,
2970
+ },
2971
+ });
2972
+ const toolCaller = makeToolCaller();
2973
+ const loop = new WorkerDecisionLoop({
2974
+ provider: provider as never,
2975
+ toolCaller: toolCaller as never,
2976
+ // No resolveRoleSessionId — should fall back to bootstrap:planner
2977
+ });
2978
+
2979
+ const result = await loop.execute({
2980
+ role: 'planner',
2981
+ featureId: 'feature_a',
2982
+ contextBundle: {
2983
+ plan: { plan_version: 1 },
2984
+ state: {
2985
+ front_matter: {
2986
+ status: STATUS.INTAKE,
2987
+ intake: { status: 'in_progress', verified_manifest_version: null },
2988
+ },
2989
+ },
2990
+ human_input: { open_questions: [] },
2991
+ intake: {
2992
+ verified_manifest: null,
2993
+ summary: { status: 'in_progress', verified_manifest_version: null },
2994
+ review: {
2995
+ ambiguities: [
2996
+ {
2997
+ id: 'AMB-401',
2998
+ status: 'open',
2999
+ summary: 'Clarify boundary.',
3000
+ obligation_ids: ['OBL-401'],
3001
+ },
3002
+ ],
3003
+ },
3004
+ },
3005
+ },
3006
+ instructions: 'intake',
3007
+ });
3008
+
3009
+ expect(result.requestHandled).toBe(true);
3010
+ expect(result.questionRequested).toBe(true);
3011
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
3012
+ 'orchestrator',
3013
+ TOOLS.FEATURE_QUESTION_CREATE,
3014
+ expect.objectContaining({
3015
+ session_id: 'bootstrap:planner',
3016
+ }),
3017
+ );
3018
+ });
3019
+ });
3020
+
3021
+ describe('WorkerDecisionLoop amend_plan revision_of branches', () => {
3022
+ function makeProvider(runWorkerResult: Record<string, unknown>) {
3023
+ return {
3024
+ selection: { provider: 'custom', model: 'model-test', provider_config_ref: null },
3025
+ runWorker: vi.fn(async () => runWorkerResult),
3026
+ };
3027
+ }
3028
+
3029
+ function makeToolCaller() {
3030
+ return {
3031
+ callTool: vi.fn(async (_role: string, toolName: string, _input?: unknown) => {
3032
+ if (toolName === TOOLS.FEATURE_GET_CONTEXT) {
3033
+ return { ok: true, data: { refreshed: true } };
3034
+ }
3035
+ return { ok: true, data: { accepted: true } };
3036
+ }),
3037
+ };
3038
+ }
3039
+
3040
+ it('GIVEN_amend_plan_with_non_integer_revision_of_WHEN_existing_version_present_THEN_falls_back_to_existing_version', async () => {
3041
+ const provider = makeProvider({
3042
+ type: 'REQUEST',
3043
+ request: {
3044
+ action: 'amend_plan',
3045
+ expected_plan_version: 'not-a-number',
3046
+ plan_json: {
3047
+ feature_id: 'feature_a',
3048
+ plan_version: 3,
3049
+ },
3050
+ },
3051
+ });
3052
+ const toolCaller = makeToolCaller();
3053
+ const loop = new WorkerDecisionLoop({
3054
+ provider: provider as never,
3055
+ toolCaller: toolCaller as never,
3056
+ });
3057
+
3058
+ const result = await loop.execute({
3059
+ role: 'planner',
3060
+ featureId: 'feature_a',
3061
+ contextBundle: { plan: { plan_version: 2 } },
3062
+ instructions: 'amend',
3063
+ });
3064
+
3065
+ expect(result.requestHandled).toBe(true);
3066
+ expect(result.planSubmission).toBe(true);
3067
+ expect(toolCaller.callTool).toHaveBeenCalledWith(
3068
+ 'planner',
3069
+ TOOLS.PLAN_UPDATE,
3070
+ expect.objectContaining({
3071
+ feature_id: 'feature_a',
3072
+ expected_plan_version: 2,
3073
+ }),
3074
+ );
3075
+ });
3076
+ });
3077
+
3078
+ describe('WorkerDecisionLoop runtime event journaling runId branches', () => {
3079
+ function makeProvider(runWorkerResult: Record<string, unknown>) {
3080
+ return {
3081
+ selection: { provider: 'custom', model: 'model-test', provider_config_ref: null },
3082
+ runWorker: vi.fn(async () => runWorkerResult),
3083
+ };
3084
+ }
3085
+
3086
+ function makeToolCaller() {
3087
+ return {
3088
+ callTool: vi.fn(async () => ({ ok: true, data: { accepted: true } })),
3089
+ };
3090
+ }
3091
+
3092
+ it('uses explicit string runId when journaling worker events', async () => {
3093
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
3094
+ const loop = new WorkerDecisionLoop({
3095
+ provider: makeProvider({ type: 'NOTE', content: 'hello' }) as never,
3096
+ toolCaller: makeToolCaller() as never,
3097
+ repoRoot,
3098
+ runId: 'run:string-id',
3099
+ });
3100
+
3101
+ await loop.execute({
3102
+ role: 'builder',
3103
+ featureId: 'feature_a',
3104
+ contextBundle: {},
3105
+ instructions: 'note',
3106
+ });
3107
+
3108
+ const filePath = path.join(repoRoot, '.aop', 'runtime', 'worker-events', 'run:string-id.jsonl');
3109
+ await expect(fs.readFile(filePath, 'utf8')).resolves.toContain('"run_id":"run:string-id"');
3110
+ });
3111
+
3112
+ it('falls back to generated runId when configured runId string is empty', async () => {
3113
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
3114
+ const loop = new WorkerDecisionLoop({
3115
+ provider: makeProvider({ type: 'NOTE', content: 'hello' }) as never,
3116
+ toolCaller: makeToolCaller() as never,
3117
+ repoRoot,
3118
+ runId: '',
3119
+ });
3120
+
3121
+ await loop.execute({
3122
+ role: 'builder',
3123
+ featureId: 'feature_a',
3124
+ contextBundle: {},
3125
+ instructions: 'note',
3126
+ });
3127
+
3128
+ const workerEventsDir = path.join(repoRoot, '.aop', 'runtime', 'worker-events');
3129
+ const files = await fs.readdir(workerEventsDir);
3130
+ expect(files.some((entry) => entry.startsWith('run:unknown:'))).toBe(true);
3131
+ });
3132
+
3133
+ it('uses function-type runId when journaling worker events', async () => {
3134
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
3135
+ const loop = new WorkerDecisionLoop({
3136
+ provider: makeProvider({ type: 'NOTE', content: 'hello' }) as never,
3137
+ toolCaller: makeToolCaller() as never,
3138
+ repoRoot,
3139
+ runId: () => 'run:from-fn',
3140
+ });
3141
+
3142
+ await loop.execute({
3143
+ role: 'builder',
3144
+ featureId: 'feature_a',
3145
+ contextBundle: {},
3146
+ instructions: 'note',
3147
+ });
3148
+
3149
+ const filePath = path.join(repoRoot, '.aop', 'runtime', 'worker-events', 'run:from-fn.jsonl');
3150
+ await expect(fs.readFile(filePath, 'utf8')).resolves.toContain('"run_id":"run:from-fn"');
3151
+ });
3152
+
3153
+ it('falls back to generated runId when function-type runId returns empty string', async () => {
3154
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
3155
+ const loop = new WorkerDecisionLoop({
3156
+ provider: makeProvider({ type: 'NOTE', content: 'hello' }) as never,
3157
+ toolCaller: makeToolCaller() as never,
3158
+ repoRoot,
3159
+ runId: () => '',
3160
+ });
3161
+
3162
+ await loop.execute({
3163
+ role: 'builder',
3164
+ featureId: 'feature_a',
3165
+ contextBundle: {},
3166
+ instructions: 'note',
3167
+ });
3168
+
3169
+ const workerEventsDir = path.join(repoRoot, '.aop', 'runtime', 'worker-events');
3170
+ const files = await fs.readdir(workerEventsDir);
3171
+ expect(files.some((entry) => entry.startsWith('run:unknown:'))).toBe(true);
3172
+ });
3173
+
3174
+ it('includes usage metadata in journal event when provider reports usage', async () => {
3175
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
3176
+ const usageData = {
3177
+ status: 'reported',
3178
+ input_tokens: 1000,
3179
+ output_tokens: 500,
3180
+ total_tokens: 1500,
3181
+ provider: 'claude',
3182
+ model: 'claude-3',
3183
+ role: 'builder',
3184
+ estimated_cost_usd: 0.05,
3185
+ };
3186
+ const loop = new WorkerDecisionLoop({
3187
+ provider: makeProvider({
3188
+ outputs: [{ type: 'NOTE', content: 'done' }],
3189
+ usage: usageData,
3190
+ }) as never,
3191
+ toolCaller: makeToolCaller() as never,
3192
+ repoRoot,
3193
+ runId: 'run:usage-journal',
3194
+ });
3195
+
3196
+ await loop.execute({
3197
+ role: 'builder',
3198
+ featureId: 'feature_usage_journal',
3199
+ contextBundle: {},
3200
+ instructions: 'build',
3201
+ });
3202
+
3203
+ const filePath = path.join(
3204
+ repoRoot,
3205
+ '.aop',
3206
+ 'runtime',
3207
+ 'worker-events',
3208
+ 'run:usage-journal.jsonl',
3209
+ );
3210
+ const content = await fs.readFile(filePath, 'utf8');
3211
+ const events = content
3212
+ .trim()
3213
+ .split('\n')
3214
+ .map((l) => JSON.parse(l));
3215
+ const successEvent = events.find(
3216
+ (e: Record<string, unknown>) => e.event_type === 'worker_completed',
3217
+ );
3218
+ expect(successEvent).toBeDefined();
3219
+ expect(successEvent.usage).toMatchObject({
3220
+ status: 'reported',
3221
+ input_tokens: 1000,
3222
+ output_tokens: 500,
3223
+ total_tokens: 1500,
3224
+ });
3225
+ });
3226
+
3227
+ it('sets usage to null in journal event when provider returns no usage', async () => {
3228
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-events-'));
3229
+ const loop = new WorkerDecisionLoop({
3230
+ provider: makeProvider({ outputs: [{ type: 'NOTE', content: 'done' }] }) as never,
3231
+ toolCaller: makeToolCaller() as never,
3232
+ repoRoot,
3233
+ runId: 'run:no-usage-journal',
3234
+ });
3235
+
3236
+ await loop.execute({
3237
+ role: 'builder',
3238
+ featureId: 'feature_no_usage_journal',
3239
+ contextBundle: {},
3240
+ instructions: 'build',
3241
+ });
3242
+
3243
+ const filePath = path.join(
3244
+ repoRoot,
3245
+ '.aop',
3246
+ 'runtime',
3247
+ 'worker-events',
3248
+ 'run:no-usage-journal.jsonl',
3249
+ );
3250
+ const content = await fs.readFile(filePath, 'utf8');
3251
+ const events = content
3252
+ .trim()
3253
+ .split('\n')
3254
+ .map((l) => JSON.parse(l));
3255
+ const successEvent = events.find(
3256
+ (e: Record<string, unknown>) => e.event_type === 'worker_completed',
3257
+ );
3258
+ expect(successEvent).toBeDefined();
3259
+ expect(successEvent.usage).toBeNull();
3260
+ });
3261
+ });
3262
+
3263
+ describe('WorkerDecisionLoop runtime failure event classification', () => {
3264
+ function makeToolCaller() {
3265
+ return {
3266
+ callTool: vi.fn(async () => ({ ok: true, data: {} })),
3267
+ };
3268
+ }
3269
+
3270
+ async function readWorkerEvents(
3271
+ repoRoot: string,
3272
+ runId: string,
3273
+ ): Promise<Record<string, unknown>[]> {
3274
+ const filePath = path.join(repoRoot, '.aop', 'runtime', 'worker-events', `${runId}.jsonl`);
3275
+ const content = await fs.readFile(filePath, 'utf8');
3276
+ return content
3277
+ .trim()
3278
+ .split('\n')
3279
+ .filter((line) => line.length > 0)
3280
+ .map((line) => JSON.parse(line) as Record<string, unknown>);
3281
+ }
3282
+
3283
+ it('classifies timeout-family runtime failures as worker_timeout with watchdog metadata', async () => {
3284
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-failure-'));
3285
+ const timeoutError = new Error('idle timeout') as Error & {
3286
+ code?: string;
3287
+ details?: Record<string, unknown>;
3288
+ };
3289
+ timeoutError.code = 'provider_stall_timeout';
3290
+ timeoutError.details = {
3291
+ watchdog_reason: 'idle_timeout',
3292
+ timeout_ms: 600000,
3293
+ idle_timeout_ms: 120000,
3294
+ spawn_timeout_ms: 15000,
3295
+ idle_for_ms: 120500,
3296
+ elapsed_ms: 130000,
3297
+ signal: 'SIGKILL',
3298
+ exit_code: 1,
3299
+ };
3300
+
3301
+ const loop = new WorkerDecisionLoop({
3302
+ provider: {
3303
+ selection: { provider: 'codex', model: 'model-test', provider_config_ref: null },
3304
+ runWorker: vi.fn(async () => {
3305
+ throw timeoutError;
3306
+ }),
3307
+ } as never,
3308
+ toolCaller: makeToolCaller() as never,
3309
+ repoRoot,
3310
+ runId: 'run:timeout',
3311
+ });
3312
+
3313
+ await expect(
3314
+ loop.execute({
3315
+ role: 'builder',
3316
+ featureId: 'feature-a',
3317
+ contextBundle: {},
3318
+ instructions: 'build',
3319
+ }),
3320
+ ).rejects.toMatchObject({ code: 'provider_stall_timeout' });
3321
+
3322
+ const events = await readWorkerEvents(repoRoot, 'run:timeout');
3323
+ expect(events.at(-1)).toMatchObject({
3324
+ event_type: 'worker_timeout',
3325
+ error_code: 'provider_stall_timeout',
3326
+ watchdog_reason: 'idle_timeout',
3327
+ timeout_ms: 600000,
3328
+ idle_timeout_ms: 120000,
3329
+ spawn_timeout_ms: 15000,
3330
+ idle_for_ms: 120500,
3331
+ elapsed_ms: 130000,
3332
+ signal: 'SIGKILL',
3333
+ exit_code: 1,
3334
+ });
3335
+ });
3336
+
3337
+ it('classifies non-timeout runtime failures as worker_failed with fallback error code', async () => {
3338
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-failure-'));
3339
+ const failedError = new Error('provider crashed');
3340
+
3341
+ const loop = new WorkerDecisionLoop({
3342
+ provider: {
3343
+ selection: { provider: 'codex', model: 'model-test', provider_config_ref: null },
3344
+ runWorker: vi.fn(async () => {
3345
+ throw failedError;
3346
+ }),
3347
+ } as never,
3348
+ toolCaller: makeToolCaller() as never,
3349
+ repoRoot,
3350
+ runId: 'run:failed',
3351
+ });
3352
+
3353
+ await expect(
3354
+ loop.execute({
3355
+ role: 'qa',
3356
+ featureId: 'feature-b',
3357
+ contextBundle: {},
3358
+ instructions: 'qa',
3359
+ }),
3360
+ ).rejects.toThrow('provider crashed');
3361
+
3362
+ const events = await readWorkerEvents(repoRoot, 'run:failed');
3363
+ expect(events.at(-1)).toMatchObject({
3364
+ event_type: 'worker_failed',
3365
+ error_code: 'provider_runtime_unavailable',
3366
+ watchdog_reason: null,
3367
+ });
3368
+ });
3369
+
3370
+ it('includes stderr in failure event when provider error carries stderr details', async () => {
3371
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-failure-'));
3372
+ const stderrError = new Error('runtime crashed') as Error & {
3373
+ code?: string;
3374
+ details?: Record<string, unknown>;
3375
+ };
3376
+ stderrError.code = 'provider_stall_timeout';
3377
+ stderrError.details = {
3378
+ watchdog_reason: 'spawn_timeout',
3379
+ timeout_ms: 60000,
3380
+ stderr: 'fatal: out of memory',
3381
+ signal: 'SIGTERM',
3382
+ exit_code: 137,
3383
+ };
3384
+
3385
+ const loop = new WorkerDecisionLoop({
3386
+ provider: {
3387
+ selection: { provider: 'codex', model: 'model-test', provider_config_ref: null },
3388
+ runWorker: vi.fn(async () => {
3389
+ throw stderrError;
3390
+ }),
3391
+ } as never,
3392
+ toolCaller: makeToolCaller() as never,
3393
+ repoRoot,
3394
+ runId: 'run:stderr',
3395
+ });
3396
+
3397
+ await expect(
3398
+ loop.execute({
3399
+ role: 'builder',
3400
+ featureId: 'feature-stderr',
3401
+ contextBundle: {},
3402
+ instructions: 'build',
3403
+ }),
3404
+ ).rejects.toMatchObject({ code: 'provider_stall_timeout' });
3405
+
3406
+ const events = await readWorkerEvents(repoRoot, 'run:stderr');
3407
+ expect(events.at(-1)).toMatchObject({
3408
+ event_type: 'worker_timeout',
3409
+ stderr: 'fatal: out of memory',
3410
+ signal: 'SIGTERM',
3411
+ exit_code: 137,
3412
+ });
3413
+ });
3414
+ });
3415
+
3416
+ describe('WorkerDecisionLoop interactive execution mode', () => {
3417
+ function makeToolCaller() {
3418
+ return {
3419
+ callTool: vi.fn(async () => ({ ok: true, data: {} })),
3420
+ };
3421
+ }
3422
+
3423
+ it('passes working_directory and execution_mode to provider and records final checkpoint progress', async () => {
3424
+ const provider = {
3425
+ selection: {
3426
+ provider: 'custom',
3427
+ model: 'model-test',
3428
+ provider_config_ref: null,
3429
+ },
3430
+ getCapabilities: () => ({
3431
+ supportsInteractiveMode: true,
3432
+ supportsWorkingDirectory: true,
3433
+ supportsPauseResume: false,
3434
+ supportsMessagePassing: false,
3435
+ supportsAcknowledgment: false,
3436
+ }),
3437
+ runWorker: vi.fn(async () => ({
3438
+ outputs: [{ type: 'NOTE', content: 'interactive done' }],
3439
+ })),
3440
+ };
3441
+ const watchdog = {
3442
+ startWatching: vi.fn(async () => undefined),
3443
+ stopWatching: vi.fn(async () => undefined),
3444
+ on: vi.fn(),
3445
+ off: vi.fn(),
3446
+ };
3447
+ const checkpointService = {
3448
+ createCheckpoint: vi.fn(async () => ({
3449
+ checkpoint: {
3450
+ checkpoint_id: 'checkpoint-1',
3451
+ timestamp: new Date().toISOString(),
3452
+ files_changed: ['src/changed.ts'],
3453
+ validation_status: 'valid',
3454
+ violations: [],
3455
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-1.diff',
3456
+ net_new_worktree_change: true,
3457
+ },
3458
+ valid: true,
3459
+ blockMerge: false,
3460
+ })),
3461
+ };
3462
+ const toolCaller = makeToolCaller();
3463
+ const loop = new WorkerDecisionLoop({
3464
+ provider: provider as never,
3465
+ toolCaller: toolCaller as never,
3466
+ repoRoot: '/tmp/repo',
3467
+ resolveExecutionMode: async () => 'interactive',
3468
+ resolveInteractiveConfig: () => ({
3469
+ checkpointIntervalMs: 60_000,
3470
+ watchdogPollIntervalMs: 2_000,
3471
+ maxUncommittedChanges: 50,
3472
+ validationOnCheckpoint: true,
3473
+ revertOnViolation: false,
3474
+ violationSeverity: 'warning',
3475
+ }),
3476
+ watchdog: watchdog as never,
3477
+ checkpointService: checkpointService as never,
3478
+ resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
3479
+ resolveRoleSessionId: () => 'builder-session-1',
3480
+ });
3481
+
3482
+ const result = await loop.execute({
3483
+ role: 'builder',
3484
+ featureId: 'feature_a',
3485
+ contextBundle: {},
3486
+ instructions: 'work directly in the worktree',
3487
+ });
3488
+
3489
+ expect(provider.runWorker).toHaveBeenCalledWith(
3490
+ expect.objectContaining({
3491
+ execution_mode: 'interactive',
3492
+ instructions: expect.stringContaining(
3493
+ 'Ignore any generic instruction that asks you to emit PATCH or unified diff output.',
3494
+ ),
3495
+ working_directory: '/tmp/worktrees/feature_a',
3496
+ }),
3497
+ );
3498
+ expect(checkpointService.createCheckpoint).toHaveBeenCalledWith(
3499
+ expect.objectContaining({
3500
+ featureId: 'feature_a',
3501
+ trigger: 'final',
3502
+ }),
3503
+ );
3504
+ expect(result.patchApplied).toBe(true);
3505
+ expect(result.invalidOutput).toBe(false);
3506
+ });
3507
+
3508
+ it('marks decision checkpoint_invalid when final checkpoint validation fails with blockMerge', async () => {
3509
+ const provider = {
3510
+ selection: {
3511
+ provider: 'custom',
3512
+ model: 'model-test',
3513
+ provider_config_ref: null,
3514
+ },
3515
+ getCapabilities: () => ({
3516
+ supportsInteractiveMode: true,
3517
+ supportsWorkingDirectory: true,
3518
+ supportsPauseResume: false,
3519
+ supportsMessagePassing: false,
3520
+ supportsAcknowledgment: false,
3521
+ }),
3522
+ runWorker: vi.fn(async () => ({
3523
+ outputs: [{ type: 'NOTE', content: 'done' }],
3524
+ })),
3525
+ };
3526
+ const checkpointService = {
3527
+ createCheckpoint: vi.fn(async () => ({
3528
+ checkpoint: {
3529
+ checkpoint_id: 'checkpoint-invalid',
3530
+ timestamp: new Date().toISOString(),
3531
+ files_changed: ['src/disallowed.ts'],
3532
+ validation_status: 'invalid',
3533
+ violations: ['src/disallowed.ts: outside_allowed_areas'],
3534
+ severity: 'error',
3535
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-invalid.diff',
3536
+ net_new_worktree_change: true,
3537
+ },
3538
+ valid: false,
3539
+ blockMerge: true,
3540
+ })),
3541
+ };
3542
+ const loop = new WorkerDecisionLoop({
3543
+ provider: provider as never,
3544
+ toolCaller: makeToolCaller() as never,
3545
+ repoRoot: '/tmp/repo',
3546
+ resolveExecutionMode: async () => 'interactive',
3547
+ resolveInteractiveConfig: () => ({
3548
+ checkpointIntervalMs: 60_000,
3549
+ watchdogPollIntervalMs: 2_000,
3550
+ maxUncommittedChanges: 50,
3551
+ validationOnCheckpoint: true,
3552
+ revertOnViolation: false,
3553
+ violationSeverity: 'error',
3554
+ }),
3555
+ watchdog: {
3556
+ startWatching: vi.fn(async () => undefined),
3557
+ stopWatching: vi.fn(async () => undefined),
3558
+ on: vi.fn(),
3559
+ off: vi.fn(),
3560
+ } as never,
3561
+ checkpointService: checkpointService as never,
3562
+ resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
3563
+ resolveRoleSessionId: () => 'builder-session-1',
3564
+ });
3565
+
3566
+ const result = await loop.execute({
3567
+ role: 'builder',
3568
+ featureId: 'feature_a',
3569
+ contextBundle: {},
3570
+ instructions: 'write files',
3571
+ });
3572
+
3573
+ expect(result.invalidOutput).toBe(false);
3574
+ expect(result.errorCode).toBe('interactive_checkpoint_invalid');
3575
+ expect(result.interactiveOutcome).toBe('checkpoint_invalid');
3576
+ expect(result.checkpoint?.violations).toEqual(['src/disallowed.ts: outside_allowed_areas']);
3577
+ });
3578
+
3579
+ it('does not block interactive progress when checkpoint violations are warning-only', async () => {
3580
+ const provider = {
3581
+ selection: {
3582
+ provider: 'custom',
3583
+ model: 'model-test',
3584
+ provider_config_ref: null,
3585
+ },
3586
+ getCapabilities: () => ({
3587
+ supportsInteractiveMode: true,
3588
+ supportsWorkingDirectory: true,
3589
+ supportsPauseResume: false,
3590
+ supportsMessagePassing: false,
3591
+ supportsAcknowledgment: false,
3592
+ }),
3593
+ runWorker: vi.fn(async () => ({
3594
+ outputs: [{ type: 'NOTE', content: 'done' }],
3595
+ })),
3596
+ };
3597
+ const checkpointService = {
3598
+ createCheckpoint: vi.fn(async () => ({
3599
+ checkpoint: {
3600
+ checkpoint_id: 'checkpoint-warning',
3601
+ timestamp: new Date().toISOString(),
3602
+ files_changed: ['src/warn.ts'],
3603
+ validation_status: 'invalid',
3604
+ violations: ['src/warn.ts: outside_allowed_areas'],
3605
+ severity: 'warning',
3606
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-warning.diff',
3607
+ net_new_worktree_change: true,
3608
+ },
3609
+ valid: false,
3610
+ blockMerge: false,
3611
+ })),
3612
+ };
3613
+ const loop = new WorkerDecisionLoop({
3614
+ provider: provider as never,
3615
+ toolCaller: makeToolCaller() as never,
3616
+ repoRoot: '/tmp/repo',
3617
+ resolveExecutionMode: async () => 'interactive',
3618
+ resolveInteractiveConfig: () => ({
3619
+ checkpointIntervalMs: 60_000,
3620
+ watchdogPollIntervalMs: 2_000,
3621
+ maxUncommittedChanges: 50,
3622
+ validationOnCheckpoint: true,
3623
+ revertOnViolation: false,
3624
+ violationSeverity: 'warning',
3625
+ }),
3626
+ watchdog: {
3627
+ startWatching: vi.fn(async () => undefined),
3628
+ stopWatching: vi.fn(async () => undefined),
3629
+ on: vi.fn(),
3630
+ off: vi.fn(),
3631
+ } as never,
3632
+ checkpointService: checkpointService as never,
3633
+ resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
3634
+ resolveRoleSessionId: () => 'builder-session-1',
3635
+ });
3636
+
3637
+ const result = await loop.execute({
3638
+ role: 'builder',
3639
+ featureId: 'feature_a',
3640
+ contextBundle: {},
3641
+ instructions: 'write files',
3642
+ });
3643
+
3644
+ expect(result.errorCode).toBeNull();
3645
+ expect(result.interactiveOutcome).toBe('progress_applied');
3646
+ expect(result.patchApplied).toBe(true);
3647
+ expect(result.noProgress).toBe(false);
3648
+ });
3649
+
3650
+ it('captures error message in synthetic checkpoint when createCheckpoint throws', async () => {
3651
+ const provider = {
3652
+ selection: {
3653
+ provider: 'custom',
3654
+ model: 'model-test',
3655
+ provider_config_ref: null,
3656
+ },
3657
+ getCapabilities: () => ({
3658
+ supportsInteractiveMode: true,
3659
+ supportsWorkingDirectory: true,
3660
+ supportsPauseResume: false,
3661
+ supportsMessagePassing: false,
3662
+ supportsAcknowledgment: false,
3663
+ }),
3664
+ runWorker: vi.fn(async () => ({
3665
+ outputs: [{ type: 'NOTE', content: 'done' }],
3666
+ })),
3667
+ };
3668
+ const checkpointService = {
3669
+ createCheckpoint: vi.fn(async () => {
3670
+ throw new Error('Lock acquisition timed out');
3671
+ }),
3672
+ };
3673
+ const loop = new WorkerDecisionLoop({
3674
+ provider: provider as never,
3675
+ toolCaller: makeToolCaller() as never,
3676
+ repoRoot: '/tmp/repo',
3677
+ resolveExecutionMode: async () => 'interactive',
3678
+ resolveInteractiveConfig: () => ({
3679
+ checkpointIntervalMs: 60_000,
3680
+ watchdogPollIntervalMs: 2_000,
3681
+ maxUncommittedChanges: 50,
3682
+ validationOnCheckpoint: true,
3683
+ revertOnViolation: false,
3684
+ violationSeverity: 'error',
3685
+ }),
3686
+ watchdog: {
3687
+ startWatching: vi.fn(async () => undefined),
3688
+ stopWatching: vi.fn(async () => undefined),
3689
+ on: vi.fn(),
3690
+ off: vi.fn(),
3691
+ } as never,
3692
+ checkpointService: checkpointService as never,
3693
+ resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
3694
+ resolveRoleSessionId: () => 'builder-session-1',
3695
+ });
3696
+
3697
+ const result = await loop.execute({
3698
+ role: 'builder',
3699
+ featureId: 'feature_a',
3700
+ contextBundle: {},
3701
+ instructions: 'write files',
3702
+ });
3703
+
3704
+ expect(result.interactiveOutcome).toBe('checkpoint_invalid');
3705
+ expect(result.errorCode).toBe('interactive_checkpoint_invalid');
3706
+ expect(result.checkpoint).toBeDefined();
3707
+ expect(result.checkpoint?.violations).toEqual(['Lock acquisition timed out']);
3708
+ expect(result.checkpoint?.validation_status).toBe('invalid');
3709
+ });
3710
+
3711
+ it('marks malformed interactive provider output as provider_output_invalid', async () => {
3712
+ const provider = {
3713
+ selection: {
3714
+ provider: 'custom',
3715
+ model: 'model-test',
3716
+ provider_config_ref: null,
3717
+ },
3718
+ getCapabilities: () => ({
3719
+ supportsInteractiveMode: true,
3720
+ supportsWorkingDirectory: true,
3721
+ supportsPauseResume: false,
3722
+ supportsMessagePassing: false,
3723
+ supportsAcknowledgment: false,
3724
+ }),
3725
+ runWorker: vi.fn(async () => ({
3726
+ outputs: [{ content: 'missing type field' }],
3727
+ })),
3728
+ };
3729
+ const checkpointService = {
3730
+ createCheckpoint: vi.fn(async () => ({
3731
+ checkpoint: {
3732
+ checkpoint_id: 'checkpoint-valid',
3733
+ timestamp: new Date().toISOString(),
3734
+ files_changed: [],
3735
+ validation_status: 'valid',
3736
+ violations: [],
3737
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-valid.diff',
3738
+ net_new_worktree_change: false,
3739
+ },
3740
+ valid: true,
3741
+ blockMerge: false,
3742
+ })),
3743
+ };
3744
+ const loop = new WorkerDecisionLoop({
3745
+ provider: provider as never,
3746
+ toolCaller: makeToolCaller() as never,
3747
+ repoRoot: '/tmp/repo',
3748
+ resolveExecutionMode: async () => 'interactive',
3749
+ resolveInteractiveConfig: () => ({
3750
+ checkpointIntervalMs: 60_000,
3751
+ watchdogPollIntervalMs: 2_000,
3752
+ maxUncommittedChanges: 50,
3753
+ validationOnCheckpoint: true,
3754
+ revertOnViolation: false,
3755
+ violationSeverity: 'warning',
3756
+ }),
3757
+ watchdog: {
3758
+ startWatching: vi.fn(async () => undefined),
3759
+ stopWatching: vi.fn(async () => undefined),
3760
+ on: vi.fn(),
3761
+ off: vi.fn(),
3762
+ } as never,
3763
+ checkpointService: checkpointService as never,
3764
+ resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
3765
+ resolveRoleSessionId: () => 'builder-session-1',
3766
+ });
3767
+
3768
+ const result = await loop.execute({
3769
+ role: 'builder',
3770
+ featureId: 'feature_a',
3771
+ contextBundle: {},
3772
+ instructions: 'write files',
910
3773
  });
911
- });
912
3774
 
913
- it('classifies non-timeout runtime failures as worker_failed with fallback error code', async () => {
914
- const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-worker-failure-'));
915
- const failedError = new Error('provider crashed');
3775
+ expect(result.invalidOutput).toBe(true);
3776
+ expect(result.errorCode).toBe('provider_output_invalid');
3777
+ expect(result.interactiveOutcome).toBe('provider_output_invalid');
3778
+ });
916
3779
 
3780
+ it('treats empty interactive provider output with no file changes as provider_output_invalid', async () => {
3781
+ const provider = {
3782
+ selection: {
3783
+ provider: 'custom',
3784
+ model: 'model-test',
3785
+ provider_config_ref: null,
3786
+ },
3787
+ getCapabilities: () => ({
3788
+ supportsInteractiveMode: true,
3789
+ supportsWorkingDirectory: true,
3790
+ supportsPauseResume: false,
3791
+ supportsMessagePassing: false,
3792
+ supportsAcknowledgment: false,
3793
+ }),
3794
+ runWorker: vi.fn(async () => ({
3795
+ outputs: [
3796
+ {
3797
+ type: 'NOTE',
3798
+ content: 'Interactive worker exited without a textual summary.',
3799
+ },
3800
+ ],
3801
+ provider_meta: {
3802
+ provider: 'custom',
3803
+ model: 'model-test',
3804
+ parse_fallback_reason: 'empty_output',
3805
+ },
3806
+ })),
3807
+ };
3808
+ const checkpointService = {
3809
+ createCheckpoint: vi.fn(async () => ({
3810
+ checkpoint: {
3811
+ checkpoint_id: 'checkpoint-empty',
3812
+ timestamp: new Date().toISOString(),
3813
+ files_changed: [],
3814
+ validation_status: 'valid',
3815
+ violations: [],
3816
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-empty.diff',
3817
+ net_new_worktree_change: false,
3818
+ },
3819
+ valid: true,
3820
+ blockMerge: false,
3821
+ })),
3822
+ };
917
3823
  const loop = new WorkerDecisionLoop({
918
- provider: {
919
- selection: { provider: 'codex', model: 'model-test', provider_config_ref: null },
920
- runWorker: vi.fn(async () => {
921
- throw failedError;
922
- }),
923
- } as never,
3824
+ provider: provider as never,
924
3825
  toolCaller: makeToolCaller() as never,
925
- repoRoot,
926
- runId: 'run:failed',
927
- });
928
-
929
- await expect(
930
- loop.execute({
931
- role: 'qa',
932
- featureId: 'feature-b',
933
- contextBundle: {},
934
- instructions: 'qa',
3826
+ repoRoot: '/tmp/repo',
3827
+ resolveExecutionMode: async () => 'interactive',
3828
+ resolveInteractiveConfig: () => ({
3829
+ checkpointIntervalMs: 60_000,
3830
+ watchdogPollIntervalMs: 2_000,
3831
+ maxUncommittedChanges: 50,
3832
+ validationOnCheckpoint: true,
3833
+ revertOnViolation: false,
3834
+ violationSeverity: 'warning',
935
3835
  }),
936
- ).rejects.toThrow('provider crashed');
3836
+ watchdog: {
3837
+ startWatching: vi.fn(async () => undefined),
3838
+ stopWatching: vi.fn(async () => undefined),
3839
+ on: vi.fn(),
3840
+ off: vi.fn(),
3841
+ } as never,
3842
+ checkpointService: checkpointService as never,
3843
+ resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
3844
+ resolveRoleSessionId: () => 'builder-session-1',
3845
+ });
937
3846
 
938
- const events = await readWorkerEvents(repoRoot, 'run:failed');
939
- expect(events.at(-1)).toMatchObject({
940
- event_type: 'worker_failed',
941
- error_code: 'provider_runtime_unavailable',
942
- watchdog_reason: null,
3847
+ const result = await loop.execute({
3848
+ role: 'builder',
3849
+ featureId: 'feature_a',
3850
+ contextBundle: {},
3851
+ instructions: 'write files',
943
3852
  });
944
- });
945
- });
946
3853
 
947
- describe('WorkerDecisionLoop interactive execution mode', () => {
948
- function makeToolCaller() {
949
- return {
950
- callTool: vi.fn(async () => ({ ok: true, data: {} })),
951
- };
952
- }
3854
+ expect(result.invalidOutput).toBe(true);
3855
+ expect(result.errorCode).toBe('provider_output_invalid');
3856
+ expect(result.noteLogged).toBe(false);
3857
+ expect(result.noProgress).toBe(true);
3858
+ expect(result.interactiveOutcome).toBe('provider_output_invalid');
3859
+ });
953
3860
 
954
- it('passes working_directory and execution_mode to provider and records final checkpoint progress', async () => {
3861
+ it('does not treat PATCH payload output as progress in interactive mode without file changes', async () => {
955
3862
  const provider = {
956
3863
  selection: {
957
3864
  provider: 'custom',
@@ -966,33 +3873,27 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
966
3873
  supportsAcknowledgment: false,
967
3874
  }),
968
3875
  runWorker: vi.fn(async () => ({
969
- outputs: [{ type: 'NOTE', content: 'interactive done' }],
3876
+ outputs: [{ type: 'PATCH', unified_diff: '--- a/file\n+++ b/file\n@@ -0,0 +1 @@\n+x' }],
970
3877
  })),
971
3878
  };
972
- const watchdog = {
973
- startWatching: vi.fn(async () => undefined),
974
- stopWatching: vi.fn(async () => undefined),
975
- on: vi.fn(),
976
- off: vi.fn(),
977
- };
978
3879
  const checkpointService = {
979
3880
  createCheckpoint: vi.fn(async () => ({
980
3881
  checkpoint: {
981
- checkpoint_id: 'checkpoint-1',
3882
+ checkpoint_id: 'checkpoint-empty',
982
3883
  timestamp: new Date().toISOString(),
983
- files_changed: ['src/changed.ts'],
3884
+ files_changed: [],
984
3885
  validation_status: 'valid',
985
3886
  violations: [],
986
- diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-1.diff',
3887
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-empty.diff',
3888
+ net_new_worktree_change: false,
987
3889
  },
988
3890
  valid: true,
989
3891
  blockMerge: false,
990
3892
  })),
991
3893
  };
992
- const toolCaller = makeToolCaller();
993
3894
  const loop = new WorkerDecisionLoop({
994
3895
  provider: provider as never,
995
- toolCaller: toolCaller as never,
3896
+ toolCaller: makeToolCaller() as never,
996
3897
  repoRoot: '/tmp/repo',
997
3898
  resolveExecutionMode: async () => 'interactive',
998
3899
  resolveInteractiveConfig: () => ({
@@ -1003,7 +3904,12 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
1003
3904
  revertOnViolation: false,
1004
3905
  violationSeverity: 'warning',
1005
3906
  }),
1006
- watchdog: watchdog as never,
3907
+ watchdog: {
3908
+ startWatching: vi.fn(async () => undefined),
3909
+ stopWatching: vi.fn(async () => undefined),
3910
+ on: vi.fn(),
3911
+ off: vi.fn(),
3912
+ } as never,
1007
3913
  checkpointService: checkpointService as never,
1008
3914
  resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
1009
3915
  resolveRoleSessionId: () => 'builder-session-1',
@@ -1013,26 +3919,15 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
1013
3919
  role: 'builder',
1014
3920
  featureId: 'feature_a',
1015
3921
  contextBundle: {},
1016
- instructions: 'work directly in the worktree',
3922
+ instructions: 'build',
1017
3923
  });
1018
3924
 
1019
- expect(provider.runWorker).toHaveBeenCalledWith(
1020
- expect.objectContaining({
1021
- execution_mode: 'interactive',
1022
- working_directory: '/tmp/worktrees/feature_a',
1023
- }),
1024
- );
1025
- expect(checkpointService.createCheckpoint).toHaveBeenCalledWith(
1026
- expect.objectContaining({
1027
- featureId: 'feature_a',
1028
- trigger: 'final',
1029
- }),
1030
- );
1031
- expect(result.patchApplied).toBe(true);
3925
+ expect(result.patchApplied).toBe(false);
3926
+ expect(result.noProgress).toBe(true);
1032
3927
  expect(result.invalidOutput).toBe(false);
1033
3928
  });
1034
3929
 
1035
- it('marks decision invalid when final checkpoint validation fails', async () => {
3930
+ it('preserves interactive fallback NOTE output when the checkpoint proves file progress', async () => {
1036
3931
  const provider = {
1037
3932
  selection: {
1038
3933
  provider: 'custom',
@@ -1047,22 +3942,27 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
1047
3942
  supportsAcknowledgment: false,
1048
3943
  }),
1049
3944
  runWorker: vi.fn(async () => ({
1050
- outputs: [{ type: 'NOTE', content: 'done' }],
3945
+ outputs: [{ type: 'NOTE', content: 'Completed edits directly in worktree' }],
3946
+ provider_meta: {
3947
+ provider: 'custom',
3948
+ model: 'model-test',
3949
+ parse_fallback_reason: 'invalid_json',
3950
+ },
1051
3951
  })),
1052
3952
  };
1053
3953
  const checkpointService = {
1054
3954
  createCheckpoint: vi.fn(async () => ({
1055
3955
  checkpoint: {
1056
- checkpoint_id: 'checkpoint-invalid',
3956
+ checkpoint_id: 'checkpoint-progress',
1057
3957
  timestamp: new Date().toISOString(),
1058
- files_changed: ['src/disallowed.ts'],
1059
- validation_status: 'invalid',
1060
- violations: ['src/disallowed.ts: outside_allowed_areas'],
1061
- severity: 'error',
1062
- diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-invalid.diff',
3958
+ files_changed: ['src/feature.ts'],
3959
+ validation_status: 'valid',
3960
+ violations: [],
3961
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-progress.diff',
3962
+ net_new_worktree_change: true,
1063
3963
  },
1064
- valid: false,
1065
- blockMerge: true,
3964
+ valid: true,
3965
+ blockMerge: false,
1066
3966
  })),
1067
3967
  };
1068
3968
  const loop = new WorkerDecisionLoop({
@@ -1076,7 +3976,7 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
1076
3976
  maxUncommittedChanges: 50,
1077
3977
  validationOnCheckpoint: true,
1078
3978
  revertOnViolation: false,
1079
- violationSeverity: 'error',
3979
+ violationSeverity: 'warning',
1080
3980
  }),
1081
3981
  watchdog: {
1082
3982
  startWatching: vi.fn(async () => undefined),
@@ -1096,11 +3996,14 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
1096
3996
  instructions: 'write files',
1097
3997
  });
1098
3998
 
1099
- expect(result.invalidOutput).toBe(true);
1100
- expect(result.noProgress).toBe(false);
3999
+ expect(result.invalidOutput).toBe(false);
4000
+ expect(result.errorCode).toBeNull();
4001
+ expect(result.noteLogged).toBe(true);
4002
+ expect(result.patchApplied).toBe(true);
4003
+ expect(result.interactiveOutcome).toBe('progress_applied');
1101
4004
  });
1102
4005
 
1103
- it('falls back to deterministic worker input when interactive mode is requested but ineligible', async () => {
4006
+ it('does not treat duplicate checkpoint snapshots as new patch progress in interactive mode', async () => {
1104
4007
  const provider = {
1105
4008
  selection: {
1106
4009
  provider: 'custom',
@@ -1115,25 +4018,85 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
1115
4018
  supportsAcknowledgment: false,
1116
4019
  }),
1117
4020
  runWorker: vi.fn(async () => ({
1118
- outputs: [{ type: 'NOTE', content: 'planner-pass' }],
4021
+ outputs: [{ type: 'NOTE', content: 'no new changes' }],
4022
+ })),
4023
+ };
4024
+ const checkpointService = {
4025
+ createCheckpoint: vi.fn(async () => ({
4026
+ checkpoint: {
4027
+ checkpoint_id: 'checkpoint-duplicate',
4028
+ timestamp: new Date().toISOString(),
4029
+ files_changed: ['src/stale.ts'],
4030
+ validation_status: 'skipped',
4031
+ violations: [],
4032
+ diff_snapshot: '.aop/features/feature_a/checkpoints/checkpoint-duplicate.diff',
4033
+ net_new_worktree_change: false,
4034
+ duplicate_of_checkpoint_id: 'checkpoint-prev',
4035
+ },
4036
+ valid: true,
4037
+ blockMerge: false,
1119
4038
  })),
1120
4039
  };
1121
-
1122
4040
  const loop = new WorkerDecisionLoop({
1123
4041
  provider: provider as never,
1124
4042
  toolCaller: makeToolCaller() as never,
1125
4043
  repoRoot: '/tmp/repo',
1126
4044
  resolveExecutionMode: async () => 'interactive',
1127
- resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
4045
+ resolveInteractiveConfig: () => ({
4046
+ checkpointIntervalMs: 60_000,
4047
+ watchdogPollIntervalMs: 2_000,
4048
+ maxUncommittedChanges: 50,
4049
+ validationOnCheckpoint: true,
4050
+ revertOnViolation: false,
4051
+ violationSeverity: 'warning',
4052
+ }),
1128
4053
  watchdog: {
1129
4054
  startWatching: vi.fn(async () => undefined),
1130
4055
  stopWatching: vi.fn(async () => undefined),
1131
4056
  on: vi.fn(),
1132
4057
  off: vi.fn(),
1133
4058
  } as never,
1134
- checkpointService: {
1135
- createCheckpoint: vi.fn(),
1136
- } as never,
4059
+ checkpointService: checkpointService as never,
4060
+ resolveWorktreePath: (featureId: string) => `/tmp/worktrees/${featureId}`,
4061
+ resolveRoleSessionId: () => 'builder-session-1',
4062
+ });
4063
+
4064
+ const result = await loop.execute({
4065
+ role: 'builder',
4066
+ featureId: 'feature_a',
4067
+ contextBundle: {},
4068
+ instructions: 'inspect the worktree',
4069
+ });
4070
+
4071
+ expect(result.patchApplied).toBe(false);
4072
+ expect(result.noProgress).toBe(true);
4073
+ expect(result.invalidOutput).toBe(false);
4074
+ });
4075
+
4076
+ it('runs planner interactively from the feature directory when interactive mode is requested', async () => {
4077
+ const provider = {
4078
+ selection: {
4079
+ provider: 'custom',
4080
+ model: 'model-test',
4081
+ provider_config_ref: null,
4082
+ },
4083
+ getCapabilities: () => ({
4084
+ supportsInteractiveMode: true,
4085
+ supportsWorkingDirectory: true,
4086
+ supportsPauseResume: false,
4087
+ supportsMessagePassing: false,
4088
+ supportsAcknowledgment: false,
4089
+ }),
4090
+ runWorker: vi.fn(async () => ({
4091
+ outputs: [{ type: 'NOTE', content: 'planner-pass' }],
4092
+ })),
4093
+ };
4094
+
4095
+ const loop = new WorkerDecisionLoop({
4096
+ provider: provider as never,
4097
+ toolCaller: makeToolCaller() as never,
4098
+ repoRoot: '/tmp/repo',
4099
+ resolveExecutionMode: async () => 'interactive',
1137
4100
  });
1138
4101
 
1139
4102
  const result = await loop.execute({
@@ -1143,12 +4106,52 @@ describe('WorkerDecisionLoop interactive execution mode', () => {
1143
4106
  instructions: 'plan',
1144
4107
  });
1145
4108
 
4109
+ expect(provider.runWorker).toHaveBeenCalledWith(
4110
+ expect.objectContaining({
4111
+ execution_mode: 'interactive',
4112
+ working_directory: '/tmp/repo/.aop/features/feature_planner',
4113
+ }),
4114
+ );
4115
+ expect(result.invalidOutput).toBe(false);
4116
+ });
4117
+
4118
+ it('falls back to deterministic planner input when interactive mode is requested without a repo root', async () => {
4119
+ const provider = {
4120
+ selection: {
4121
+ provider: 'custom',
4122
+ model: 'model-test',
4123
+ provider_config_ref: null,
4124
+ },
4125
+ getCapabilities: () => ({
4126
+ supportsInteractiveMode: true,
4127
+ supportsWorkingDirectory: true,
4128
+ supportsPauseResume: false,
4129
+ supportsMessagePassing: false,
4130
+ supportsAcknowledgment: false,
4131
+ }),
4132
+ runWorker: vi.fn(async () => ({
4133
+ outputs: [{ type: 'NOTE', content: 'planner-pass' }],
4134
+ })),
4135
+ };
4136
+
4137
+ const loop = new WorkerDecisionLoop({
4138
+ provider: provider as never,
4139
+ toolCaller: makeToolCaller() as never,
4140
+ resolveExecutionMode: async () => 'interactive',
4141
+ });
4142
+
4143
+ await loop.execute({
4144
+ role: 'planner',
4145
+ featureId: 'feature_planner',
4146
+ contextBundle: {},
4147
+ instructions: 'plan',
4148
+ });
4149
+
1146
4150
  expect(provider.runWorker).toHaveBeenCalledWith(
1147
4151
  expect.objectContaining({
1148
4152
  execution_mode: 'deterministic',
1149
4153
  working_directory: undefined,
1150
4154
  }),
1151
4155
  );
1152
- expect(result.invalidOutput).toBe(false);
1153
4156
  });
1154
4157
  });