agentic-orchestrator 0.1.3 → 0.1.4

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 (294) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/CLAUDE.md +126 -0
  3. package/README.md +166 -25
  4. package/agentic/orchestrator/adapters.yaml +3 -0
  5. package/agentic/orchestrator/gates.yaml +47 -0
  6. package/agentic/orchestrator/policy.yaml +89 -0
  7. package/agentic/orchestrator/schemas/adapters.schema.json +12 -0
  8. package/agentic/orchestrator/schemas/gates.schema.json +6 -1
  9. package/agentic/orchestrator/schemas/index.schema.json +14 -0
  10. package/agentic/orchestrator/schemas/multi-project.schema.json +41 -0
  11. package/agentic/orchestrator/schemas/policy.schema.json +449 -52
  12. package/agentic/orchestrator/schemas/state.schema.json +16 -0
  13. package/agentic/orchestrator/tools/catalog.json +68 -0
  14. package/agentic/orchestrator/tools/schemas/input/cost.get.input.schema.json +10 -0
  15. package/agentic/orchestrator/tools/schemas/input/cost.record.input.schema.json +13 -0
  16. package/agentic/orchestrator/tools/schemas/input/feature.send_message.input.schema.json +11 -0
  17. package/agentic/orchestrator/tools/schemas/input/performance.get_analytics.input.schema.json +10 -0
  18. package/agentic/orchestrator/tools/schemas/input/performance.record_outcome.input.schema.json +18 -0
  19. package/agentic/orchestrator/tools/schemas/output/cost.get.output.schema.json +13 -0
  20. package/agentic/orchestrator/tools/schemas/output/cost.record.output.schema.json +13 -0
  21. package/agentic/orchestrator/tools/schemas/output/feature.ready_to_merge.output.schema.json +7 -0
  22. package/agentic/orchestrator/tools/schemas/output/feature.send_message.output.schema.json +23 -0
  23. package/agentic/orchestrator/tools/schemas/output/performance.get_analytics.output.schema.json +46 -0
  24. package/agentic/orchestrator/tools/schemas/output/performance.record_outcome.output.schema.json +10 -0
  25. package/agentic/orchestrator/tools.md +5 -0
  26. package/apps/control-plane/scripts/validate-architecture-rules.mjs +28 -2
  27. package/apps/control-plane/scripts/validate-docker-mcp-contract.mjs +12 -0
  28. package/apps/control-plane/scripts/validate-mcp-contracts.ts +92 -0
  29. package/apps/control-plane/src/application/adapters/adapter-registry.ts +169 -0
  30. package/apps/control-plane/src/application/multi-project-loader.ts +119 -0
  31. package/apps/control-plane/src/application/services/activity-monitor-service.ts +199 -0
  32. package/apps/control-plane/src/application/services/cost-tracking-service.ts +82 -0
  33. package/apps/control-plane/src/application/services/dependency-scheduler-service.ts +86 -0
  34. package/apps/control-plane/src/application/services/feature-deletion-service.ts +7 -5
  35. package/apps/control-plane/src/application/services/gate-interpolation-service.ts +15 -0
  36. package/apps/control-plane/src/application/services/gate-service.ts +38 -2
  37. package/apps/control-plane/src/application/services/instance-isolation-service.ts +18 -0
  38. package/apps/control-plane/src/application/services/issue-tracker-service.ts +469 -0
  39. package/apps/control-plane/src/application/services/merge-service.ts +67 -3
  40. package/apps/control-plane/src/application/services/notifier-service.ts +295 -0
  41. package/apps/control-plane/src/application/services/performance-analytics-service.ts +122 -0
  42. package/apps/control-plane/src/application/services/plan-service.ts +51 -0
  43. package/apps/control-plane/src/application/services/pr-monitor-service.ts +262 -0
  44. package/apps/control-plane/src/application/services/reactions-service.ts +175 -0
  45. package/apps/control-plane/src/application/services/reporting-service.ts +17 -2
  46. package/apps/control-plane/src/application/services/run-lease-service.ts +16 -38
  47. package/apps/control-plane/src/application/tools/tool-metadata.ts +4 -1
  48. package/apps/control-plane/src/cli/attach-command-handler.ts +120 -0
  49. package/apps/control-plane/src/cli/cleanup-command-handler.ts +190 -0
  50. package/apps/control-plane/src/cli/cli-argument-parser.ts +69 -3
  51. package/apps/control-plane/src/cli/dashboard-command-handler.ts +57 -0
  52. package/apps/control-plane/src/cli/help-command-handler.ts +163 -0
  53. package/apps/control-plane/src/cli/init-command-handler.ts +609 -0
  54. package/apps/control-plane/src/cli/retry-command-handler.ts +138 -0
  55. package/apps/control-plane/src/cli/run-command-handler.ts +115 -3
  56. package/apps/control-plane/src/cli/send-command-handler.ts +65 -0
  57. package/apps/control-plane/src/cli/status-command-handler.ts +102 -2
  58. package/apps/control-plane/src/cli/types.ts +26 -1
  59. package/apps/control-plane/src/core/constants.ts +8 -2
  60. package/apps/control-plane/src/core/error-codes.ts +3 -1
  61. package/apps/control-plane/src/core/gates.ts +170 -50
  62. package/apps/control-plane/src/core/kernel.ts +280 -5
  63. package/apps/control-plane/src/core/path-layout.ts +12 -0
  64. package/apps/control-plane/src/core/tool-caller.ts +36 -0
  65. package/apps/control-plane/src/core/workspace-hooks.ts +87 -0
  66. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +258 -9
  67. package/apps/control-plane/src/providers/providers.ts +235 -14
  68. package/apps/control-plane/src/supervisor/build-wave-executor.ts +129 -8
  69. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +123 -5
  70. package/apps/control-plane/src/supervisor/run-coordinator.ts +143 -6
  71. package/apps/control-plane/src/supervisor/runtime.ts +135 -6
  72. package/apps/control-plane/src/supervisor/types.ts +12 -21
  73. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +8 -0
  74. package/apps/control-plane/test/activity-monitor.spec.ts +294 -0
  75. package/apps/control-plane/test/adapter-registry.spec.ts +132 -0
  76. package/apps/control-plane/test/batch-operations.spec.ts +112 -0
  77. package/apps/control-plane/test/bootstrap-attach.spec.ts +102 -0
  78. package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +252 -0
  79. package/apps/control-plane/test/bootstrap.spec.ts +560 -0
  80. package/apps/control-plane/test/cleanup-command.spec.ts +301 -0
  81. package/apps/control-plane/test/cli-helpers.spec.ts +404 -1
  82. package/apps/control-plane/test/cli.unit.spec.ts +182 -1
  83. package/apps/control-plane/test/collision-queue.spec.ts +104 -1
  84. package/apps/control-plane/test/core-utils.spec.ts +175 -2
  85. package/apps/control-plane/test/cost-tracking.spec.ts +143 -0
  86. package/apps/control-plane/test/dashboard-api.integration.spec.ts +247 -0
  87. package/apps/control-plane/test/dashboard-client.spec.ts +116 -0
  88. package/apps/control-plane/test/dashboard-command.spec.ts +103 -0
  89. package/apps/control-plane/test/dependency-scheduler.spec.ts +189 -0
  90. package/apps/control-plane/test/epoch-tracking.spec.ts +4 -4
  91. package/apps/control-plane/test/feature-deletion-service.spec.ts +422 -0
  92. package/apps/control-plane/test/feature-lifecycle.spec.ts +202 -0
  93. package/apps/control-plane/test/git-spawn-error.spec.ts +24 -0
  94. package/apps/control-plane/test/incremental-gates.spec.ts +137 -0
  95. package/apps/control-plane/test/init-wizard.spec.ts +506 -0
  96. package/apps/control-plane/test/instance-isolation.spec.ts +83 -0
  97. package/apps/control-plane/test/issue-tracker.spec.ts +890 -0
  98. package/apps/control-plane/test/kernel.coverage.spec.ts +3 -5
  99. package/apps/control-plane/test/kernel.coverage2.spec.ts +871 -0
  100. package/apps/control-plane/test/kernel.spec.ts +13 -11
  101. package/apps/control-plane/test/lock-service.spec.ts +508 -0
  102. package/apps/control-plane/test/mcp-helpers.spec.ts +176 -0
  103. package/apps/control-plane/test/mcp.spec.ts +50 -15
  104. package/apps/control-plane/test/merge-service.spec.ts +67 -4
  105. package/apps/control-plane/test/multi-project.spec.ts +372 -0
  106. package/apps/control-plane/test/notifier-service.spec.ts +388 -0
  107. package/apps/control-plane/test/parallel-gates.spec.ts +312 -0
  108. package/apps/control-plane/test/patch-service.spec.ts +253 -0
  109. package/apps/control-plane/test/performance-analytics.spec.ts +338 -0
  110. package/apps/control-plane/test/planning-wave-executor.spec.ts +168 -0
  111. package/apps/control-plane/test/pr-monitor.spec.ts +385 -0
  112. package/apps/control-plane/test/providers.spec.ts +344 -1
  113. package/apps/control-plane/test/reactions.spec.ts +392 -0
  114. package/apps/control-plane/test/resume-command.spec.ts +390 -0
  115. package/apps/control-plane/test/run-coordinator.spec.ts +481 -2
  116. package/apps/control-plane/test/schema-date-time.spec.ts +46 -0
  117. package/apps/control-plane/test/service-retry-paths.spec.ts +30 -0
  118. package/apps/control-plane/test/services.spec.ts +95 -2
  119. package/apps/control-plane/test/session-management.spec.ts +450 -0
  120. package/apps/control-plane/test/spec-ingestion.spec.ts +190 -0
  121. package/apps/control-plane/test/supervisor-collaborators.spec.ts +699 -2
  122. package/apps/control-plane/test/supervisor.spec.ts +36 -30
  123. package/apps/control-plane/test/supervisor.unit.spec.ts +405 -0
  124. package/apps/control-plane/test/worker-decision-loop.spec.ts +57 -0
  125. package/apps/control-plane/test/workspace-hooks.spec.ts +177 -0
  126. package/apps/control-plane/vitest.config.ts +21 -5
  127. package/dist/apps/control-plane/application/adapters/adapter-registry.d.ts +44 -0
  128. package/dist/apps/control-plane/application/adapters/adapter-registry.js +76 -0
  129. package/dist/apps/control-plane/application/adapters/adapter-registry.js.map +1 -0
  130. package/dist/apps/control-plane/application/multi-project-loader.d.ts +31 -0
  131. package/dist/apps/control-plane/application/multi-project-loader.js +82 -0
  132. package/dist/apps/control-plane/application/multi-project-loader.js.map +1 -0
  133. package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +43 -0
  134. package/dist/apps/control-plane/application/services/activity-monitor-service.js +132 -0
  135. package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -0
  136. package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +28 -0
  137. package/dist/apps/control-plane/application/services/cost-tracking-service.js +48 -0
  138. package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -0
  139. package/dist/apps/control-plane/application/services/dependency-scheduler-service.d.ts +26 -0
  140. package/dist/apps/control-plane/application/services/dependency-scheduler-service.js +75 -0
  141. package/dist/apps/control-plane/application/services/dependency-scheduler-service.js.map +1 -0
  142. package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +2 -0
  143. package/dist/apps/control-plane/application/services/feature-deletion-service.js +5 -5
  144. package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
  145. package/dist/apps/control-plane/application/services/gate-interpolation-service.d.ts +7 -0
  146. package/dist/apps/control-plane/application/services/gate-interpolation-service.js +7 -0
  147. package/dist/apps/control-plane/application/services/gate-interpolation-service.js.map +1 -0
  148. package/dist/apps/control-plane/application/services/gate-service.js +32 -2
  149. package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
  150. package/dist/apps/control-plane/application/services/instance-isolation-service.d.ts +11 -0
  151. package/dist/apps/control-plane/application/services/instance-isolation-service.js +17 -0
  152. package/dist/apps/control-plane/application/services/instance-isolation-service.js.map +1 -0
  153. package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +65 -0
  154. package/dist/apps/control-plane/application/services/issue-tracker-service.js +358 -0
  155. package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -0
  156. package/dist/apps/control-plane/application/services/merge-service.d.ts +4 -0
  157. package/dist/apps/control-plane/application/services/merge-service.js +44 -2
  158. package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
  159. package/dist/apps/control-plane/application/services/notifier-service.d.ts +74 -0
  160. package/dist/apps/control-plane/application/services/notifier-service.js +212 -0
  161. package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -0
  162. package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +39 -0
  163. package/dist/apps/control-plane/application/services/performance-analytics-service.js +75 -0
  164. package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -0
  165. package/dist/apps/control-plane/application/services/plan-service.d.ts +1 -0
  166. package/dist/apps/control-plane/application/services/plan-service.js +53 -0
  167. package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
  168. package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +44 -0
  169. package/dist/apps/control-plane/application/services/pr-monitor-service.js +192 -0
  170. package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -0
  171. package/dist/apps/control-plane/application/services/reactions-service.d.ts +67 -0
  172. package/dist/apps/control-plane/application/services/reactions-service.js +114 -0
  173. package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -0
  174. package/dist/apps/control-plane/application/services/reporting-service.d.ts +1 -0
  175. package/dist/apps/control-plane/application/services/reporting-service.js +13 -2
  176. package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
  177. package/dist/apps/control-plane/application/services/run-lease-service.d.ts +2 -0
  178. package/dist/apps/control-plane/application/services/run-lease-service.js +14 -38
  179. package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
  180. package/dist/apps/control-plane/application/tools/tool-metadata.js +3 -1
  181. package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
  182. package/dist/apps/control-plane/cli/attach-command-handler.d.ts +12 -0
  183. package/dist/apps/control-plane/cli/attach-command-handler.js +98 -0
  184. package/dist/apps/control-plane/cli/attach-command-handler.js.map +1 -0
  185. package/dist/apps/control-plane/cli/cleanup-command-handler.d.ts +12 -0
  186. package/dist/apps/control-plane/cli/cleanup-command-handler.js +162 -0
  187. package/dist/apps/control-plane/cli/cleanup-command-handler.js.map +1 -0
  188. package/dist/apps/control-plane/cli/cli-argument-parser.js +68 -3
  189. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  190. package/dist/apps/control-plane/cli/dashboard-command-handler.d.ts +7 -0
  191. package/dist/apps/control-plane/cli/dashboard-command-handler.js +45 -0
  192. package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -0
  193. package/dist/apps/control-plane/cli/help-command-handler.d.ts +8 -0
  194. package/dist/apps/control-plane/cli/help-command-handler.js +146 -0
  195. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -0
  196. package/dist/apps/control-plane/cli/init-command-handler.d.ts +26 -0
  197. package/dist/apps/control-plane/cli/init-command-handler.js +517 -0
  198. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -0
  199. package/dist/apps/control-plane/cli/retry-command-handler.d.ts +8 -0
  200. package/dist/apps/control-plane/cli/retry-command-handler.js +111 -0
  201. package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -0
  202. package/dist/apps/control-plane/cli/run-command-handler.d.ts +5 -0
  203. package/dist/apps/control-plane/cli/run-command-handler.js +82 -3
  204. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  205. package/dist/apps/control-plane/cli/send-command-handler.d.ts +8 -0
  206. package/dist/apps/control-plane/cli/send-command-handler.js +55 -0
  207. package/dist/apps/control-plane/cli/send-command-handler.js.map +1 -0
  208. package/dist/apps/control-plane/cli/status-command-handler.d.ts +12 -1
  209. package/dist/apps/control-plane/cli/status-command-handler.js +55 -2
  210. package/dist/apps/control-plane/cli/status-command-handler.js.map +1 -1
  211. package/dist/apps/control-plane/cli/types.d.ts +25 -1
  212. package/dist/apps/control-plane/cli/types.js +15 -1
  213. package/dist/apps/control-plane/cli/types.js.map +1 -1
  214. package/dist/apps/control-plane/core/constants.d.ts +6 -0
  215. package/dist/apps/control-plane/core/constants.js +8 -2
  216. package/dist/apps/control-plane/core/constants.js.map +1 -1
  217. package/dist/apps/control-plane/core/error-codes.d.ts +2 -0
  218. package/dist/apps/control-plane/core/error-codes.js +3 -1
  219. package/dist/apps/control-plane/core/error-codes.js.map +1 -1
  220. package/dist/apps/control-plane/core/gates.d.ts +4 -0
  221. package/dist/apps/control-plane/core/gates.js +140 -43
  222. package/dist/apps/control-plane/core/gates.js.map +1 -1
  223. package/dist/apps/control-plane/core/kernel.d.ts +48 -1
  224. package/dist/apps/control-plane/core/kernel.js +218 -5
  225. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  226. package/dist/apps/control-plane/core/path-layout.d.ts +3 -0
  227. package/dist/apps/control-plane/core/path-layout.js +9 -0
  228. package/dist/apps/control-plane/core/path-layout.js.map +1 -1
  229. package/dist/apps/control-plane/core/tool-caller.d.ts +32 -0
  230. package/dist/apps/control-plane/core/tool-caller.js +2 -0
  231. package/dist/apps/control-plane/core/tool-caller.js.map +1 -0
  232. package/dist/apps/control-plane/core/workspace-hooks.d.ts +20 -0
  233. package/dist/apps/control-plane/core/workspace-hooks.js +69 -0
  234. package/dist/apps/control-plane/core/workspace-hooks.js.map +1 -0
  235. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +245 -9
  236. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  237. package/dist/apps/control-plane/providers/providers.d.ts +39 -4
  238. package/dist/apps/control-plane/providers/providers.js +160 -10
  239. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  240. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
  241. package/dist/apps/control-plane/supervisor/build-wave-executor.js +115 -6
  242. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  243. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
  244. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +109 -5
  245. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  246. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +15 -0
  247. package/dist/apps/control-plane/supervisor/run-coordinator.js +132 -6
  248. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  249. package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
  250. package/dist/apps/control-plane/supervisor/runtime.js +110 -6
  251. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  252. package/dist/apps/control-plane/supervisor/types.d.ts +9 -16
  253. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  254. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +3 -0
  255. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +5 -0
  256. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  257. package/eslint.config.mjs +2 -1
  258. package/package.json +12 -2
  259. package/packages/web-dashboard/next-env.d.ts +5 -0
  260. package/packages/web-dashboard/next.config.js +7 -0
  261. package/packages/web-dashboard/package.json +26 -0
  262. package/packages/web-dashboard/src/app/api/actions/route.ts +64 -0
  263. package/packages/web-dashboard/src/app/api/events/route.ts +51 -0
  264. package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +256 -0
  265. package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +10 -0
  266. package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +25 -0
  267. package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +63 -0
  268. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +16 -0
  269. package/packages/web-dashboard/src/app/api/projects/route.ts +31 -0
  270. package/packages/web-dashboard/src/app/api/status/route.ts +15 -0
  271. package/packages/web-dashboard/src/app/globals.css +2 -0
  272. package/packages/web-dashboard/src/app/layout.tsx +15 -0
  273. package/packages/web-dashboard/src/app/page.tsx +393 -0
  274. package/packages/web-dashboard/src/lib/aop-client.ts +244 -0
  275. package/packages/web-dashboard/src/lib/multi-project-config.ts +116 -0
  276. package/packages/web-dashboard/src/lib/orchestrator-tools.ts +284 -0
  277. package/packages/web-dashboard/src/lib/types.ts +58 -0
  278. package/packages/web-dashboard/tsconfig.json +40 -0
  279. package/packages/web-dashboard/vitest.config.ts +6 -0
  280. package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +1764 -0
  281. package/spec-files/outstanding/agentic_orchestrator_enterprise_governance_dashboard_spec.md +348 -0
  282. package/spec-files/outstanding/agentic_orchestrator_knowledge_canary_spec.md +344 -0
  283. package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +374 -0
  284. package/spec-files/outstanding/agentic_orchestrator_performance_improvements_spec.md +1059 -0
  285. package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +466 -0
  286. package/spec-files/outstanding/agentic_orchestrator_quality_adoption_execution_spec.md +198 -0
  287. package/spec-files/outstanding/agentic_orchestrator_validator_hardening_spec.md +365 -0
  288. package/spec-files/progress.md +481 -52
  289. /package/spec-files/{agentic_orchestrator_cli_delete_command_spec.md → completed/agentic_orchestrator_cli_delete_command_spec.md} +0 -0
  290. /package/spec-files/{agentic_orchestrator_dot_aop_generated_artifacts_spec.md → completed/agentic_orchestrator_dot_aop_generated_artifacts_spec.md} +0 -0
  291. /package/spec-files/{agentic_orchestrator_mcp_formalization_spec.md → completed/agentic_orchestrator_mcp_formalization_spec.md} +0 -0
  292. /package/spec-files/{agentic_orchestrator_oop_refactor_spec.md → completed/agentic_orchestrator_oop_refactor_spec.md} +0 -0
  293. /package/spec-files/{agentic_orchestrator_single_global_orchestrator_spec.md → completed/agentic_orchestrator_single_global_orchestrator_spec.md} +0 -0
  294. /package/spec-files/{agentic_orchestrator_spec.md → completed/agentic_orchestrator_spec.md} +0 -0
@@ -0,0 +1,15 @@
1
+ export interface InterpolationContext {
2
+ base_branch: string;
3
+ feature_id: string;
4
+ worktree_path?: string;
5
+ }
6
+
7
+ export function interpolateGateCommands(commands: string[], context: InterpolationContext): string[] {
8
+ return commands.map((token) =>
9
+ token.replaceAll('{base_branch}', context.base_branch).replaceAll('{feature_id}', context.feature_id)
10
+ );
11
+ }
12
+
13
+ export function isIncrementalMode(mode: string): boolean {
14
+ return mode === 'fast';
15
+ }
@@ -2,9 +2,11 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { pathExists } from '../../core/fs.js';
4
4
  import { runGateMode } from '../../core/gates.js';
5
+ import type { GateProfile, GateStep } from '../../core/gates.js';
5
6
  import { ERROR_CODES } from '../../core/error-codes.js';
6
7
  import { fail } from '../../core/response.js';
7
8
  import { GATE_RESULT, STATUS } from '../../core/constants.js';
9
+ import { interpolateGateCommands, isIncrementalMode } from './gate-interpolation-service.js';
8
10
 
9
11
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
12
  type AnyRecord = Record<string, any>;
@@ -118,12 +120,37 @@ export class GateService {
118
120
  const profile = this.gateProfileAndMode(effectiveProfileName, mode);
119
121
  const repoRoot = this.port.getRepoRoot();
120
122
 
123
+ const policySnapshot = this.port.getPolicySnapshot();
124
+ const worktreeConfig = policySnapshot['worktree'] as { base_branch?: string } | undefined;
125
+ const baseBranch = worktreeConfig?.base_branch ?? 'main';
126
+
127
+ const profileAsGate = profile as unknown as GateProfile;
128
+ const originalModeSteps = profileAsGate.modes?.[mode] ?? [];
129
+ const hasBaseBranchTemplate =
130
+ isIncrementalMode(mode) &&
131
+ originalModeSteps.some((step) => step.cmd.some((token) => token.includes('{base_branch}')));
132
+
133
+ let effectiveProfile: AnyRecord = profile;
134
+ if (isIncrementalMode(mode)) {
135
+ const interpolatedSteps: GateStep[] = originalModeSteps.map((step) => ({
136
+ ...step,
137
+ cmd: interpolateGateCommands(step.cmd, { base_branch: baseBranch, feature_id: featureId })
138
+ }));
139
+ effectiveProfile = {
140
+ ...profile,
141
+ modes: {
142
+ ...(profileAsGate.modes ?? {}),
143
+ [mode]: interpolatedSteps
144
+ }
145
+ };
146
+ }
147
+
121
148
  const runResult = (await runGateMode({
122
149
  featureId,
123
150
  mode,
124
151
  profileName: effectiveProfileName,
125
- profile,
126
- policy: this.port.getPolicySnapshot() as Parameters<typeof runGateMode>[0]['policy'],
152
+ profile: effectiveProfile,
153
+ policy: policySnapshot as Parameters<typeof runGateMode>[0]['policy'],
127
154
  worktreePath: this.port.worktreePath(featureId),
128
155
  logDirectory: this.port.logsPath(featureId),
129
156
  evidenceDirectory: this.port.evidencePath(featureId)
@@ -150,6 +177,15 @@ export class GateService {
150
177
  })
151
178
  );
152
179
 
180
+ if (hasBaseBranchTemplate) {
181
+ const skippedInfo = { skipped_reason: 'incremental_mode' as const, base_branch: baseBranch };
182
+ for (const evidenceFile of [runResult.evidence_path, runResult.latest_path]) {
183
+ const raw = await fs.readFile(evidenceFile, 'utf8');
184
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
185
+ await fs.writeFile(evidenceFile, `${JSON.stringify({ ...parsed, ...skippedInfo }, null, 2)}\n`, 'utf8');
186
+ }
187
+ }
188
+
153
189
  if (runResult.overall === 'fail' && runResult.coverage_status === 'fail') {
154
190
  throw {
155
191
  normalizedResponse: fail(
@@ -0,0 +1,18 @@
1
+ import { stableHash } from '../../core/fs.js';
2
+
3
+ /**
4
+ * Computes a deterministic 12-character instance ID from a config file path.
5
+ * This ensures two orchestrator checkouts with different config paths get
6
+ * independent run-lease files and dashboard ports.
7
+ */
8
+ export function computeInstanceId(configPath: string): string {
9
+ return stableHash(configPath).substring(0, 12);
10
+ }
11
+
12
+ /**
13
+ * Returns the canonical config path for the default single-instance case.
14
+ * Used when no explicit --config flag is provided.
15
+ */
16
+ export function defaultConfigPath(repoRoot: string): string {
17
+ return `${repoRoot}/agentic/orchestrator/policy.yaml`;
18
+ }
@@ -0,0 +1,469 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ export interface Issue {
7
+ id: string;
8
+ title: string;
9
+ body: string;
10
+ status: string;
11
+ url: string;
12
+ }
13
+
14
+ export interface IssueTracker {
15
+ getIssue(issueId: string): Promise<Issue>;
16
+ updateIssueStatus(issueId: string, status: string): Promise<void>;
17
+ addComment(issueId: string, comment: string): Promise<void>;
18
+ }
19
+
20
+ export interface IssueTrackerConfig {
21
+ enabled?: boolean;
22
+ type: string;
23
+ config?: Record<string, string>;
24
+ }
25
+
26
+ // --- GitHub adapter (via gh CLI) ---
27
+
28
+ export type GhRunner = (args: string[]) => Promise<{ stdout: string; exitCode: number }>;
29
+ export type HttpRunner = (url: string, init: RequestInit) => Promise<{ status: number; ok: boolean; body: string }>;
30
+
31
+ export function createGhRunner(fn?: GhRunner): GhRunner {
32
+ if (fn !== undefined) {
33
+ return fn;
34
+ }
35
+ return async (args: string[]) => {
36
+ try {
37
+ const { stdout } = await execFileAsync('gh', args, { timeout: 15_000 });
38
+ return { stdout, exitCode: 0 };
39
+ } catch (err: unknown) {
40
+ const e = err as Record<string, unknown>;
41
+ if (e['code'] === 'ENOENT' || e['code'] === 127) {
42
+ return { stdout: '', exitCode: 127 };
43
+ }
44
+ const exitCode = typeof e['code'] === 'number' ? e['code'] : 1;
45
+ return { stdout: '', exitCode };
46
+ }
47
+ };
48
+ }
49
+
50
+ export function createHttpRunner(fn?: HttpRunner): HttpRunner {
51
+ if (fn !== undefined) {
52
+ return fn;
53
+ }
54
+ return async (url: string, init: RequestInit) => {
55
+ try {
56
+ const response = await fetch(url, init);
57
+ return {
58
+ status: response.status,
59
+ ok: response.ok,
60
+ body: await response.text()
61
+ };
62
+ } catch {
63
+ return { status: 0, ok: false, body: '' };
64
+ }
65
+ };
66
+ }
67
+
68
+ function tryParseJson(value: string): Record<string, unknown> | null {
69
+ if (!value) {
70
+ return null;
71
+ }
72
+ try {
73
+ const parsed = JSON.parse(value) as unknown;
74
+ if (parsed && typeof parsed === 'object') {
75
+ return parsed as Record<string, unknown>;
76
+ }
77
+ return null;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ export class GitHubIssueTracker implements IssueTracker {
84
+ private readonly repo: string | undefined;
85
+ private readonly ghRunner: GhRunner;
86
+
87
+ constructor(config: Record<string, string> = {}, ghRunner?: GhRunner) {
88
+ this.repo = config['repo'];
89
+ this.ghRunner = ghRunner ?? createGhRunner();
90
+ }
91
+
92
+ async getIssue(issueId: string): Promise<Issue> {
93
+ const repoArgs = this.repo ? ['--repo', this.repo] : [];
94
+ const result = await this.ghRunner([
95
+ 'issue', 'view', issueId,
96
+ ...repoArgs,
97
+ '--json', 'number,title,body,state,url'
98
+ ]);
99
+ if (result.exitCode !== 0 || !result.stdout) {
100
+ return { id: issueId, title: '', body: '', status: 'unknown', url: '' };
101
+ }
102
+ const parsed = JSON.parse(result.stdout) as {
103
+ number: number;
104
+ title: string;
105
+ body: string;
106
+ state: string;
107
+ url: string;
108
+ };
109
+ return {
110
+ id: String(parsed.number),
111
+ title: parsed.title,
112
+ body: parsed.body,
113
+ status: parsed.state.toLowerCase(),
114
+ url: parsed.url
115
+ };
116
+ }
117
+
118
+ async updateIssueStatus(issueId: string, status: string): Promise<void> {
119
+ const repoArgs = this.repo ? ['--repo', this.repo] : [];
120
+ const stateArg = status === 'closed' || status === 'merged' ? 'closed' : 'open';
121
+ await this.ghRunner(['issue', 'edit', issueId, ...repoArgs, '--state', stateArg]);
122
+ }
123
+
124
+ async addComment(issueId: string, comment: string): Promise<void> {
125
+ const repoArgs = this.repo ? ['--repo', this.repo] : [];
126
+ await this.ghRunner(['issue', 'comment', issueId, ...repoArgs, '--body', comment]);
127
+ }
128
+ }
129
+
130
+ interface LinearIssueNode {
131
+ id: string;
132
+ identifier: string;
133
+ title: string;
134
+ description: string;
135
+ url: string;
136
+ state: {
137
+ name: string;
138
+ } | null;
139
+ }
140
+
141
+ function readString(value: unknown): string {
142
+ return typeof value === 'string' ? value : '';
143
+ }
144
+
145
+ function lowerCaseStatus(value: string): string {
146
+ return value ? value.toLowerCase() : 'unknown';
147
+ }
148
+
149
+ function normalizeAopStatus(status: string): string {
150
+ return status.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_');
151
+ }
152
+
153
+ function mapDefaultLinearStatus(status: string): string | null {
154
+ const normalized = normalizeAopStatus(status);
155
+ if (normalized === 'merged' || normalized === 'closed') {
156
+ return 'done';
157
+ }
158
+ if (normalized === 'blocked' || normalized === 'failed') {
159
+ return 'blocked';
160
+ }
161
+ if (normalized === 'planning') {
162
+ return 'backlog';
163
+ }
164
+ if (normalized === 'qa' || normalized === 'ready_to_merge') {
165
+ return 'in_review';
166
+ }
167
+ if (normalized === 'building') {
168
+ return 'in_progress';
169
+ }
170
+ return 'in_progress';
171
+ }
172
+
173
+ function mapDefaultJiraTransition(status: string): string {
174
+ const normalized = normalizeAopStatus(status);
175
+ if (normalized === 'merged' || normalized === 'closed') {
176
+ return 'done';
177
+ }
178
+ if (normalized === 'planning') {
179
+ return 'to do';
180
+ }
181
+ if (normalized === 'blocked' || normalized === 'failed') {
182
+ return 'blocked';
183
+ }
184
+ return 'in progress';
185
+ }
186
+
187
+ // --- Linear adapter (GraphQL HTTP) ---
188
+ export class LinearIssueTracker implements IssueTracker {
189
+ private readonly token: string;
190
+ private readonly baseUrl: string;
191
+ private readonly config: Record<string, string>;
192
+ private readonly httpRunner: HttpRunner;
193
+
194
+ constructor(config: Record<string, string> = {}, httpRunner?: HttpRunner) {
195
+ this.token = config['token'] ?? '';
196
+ this.baseUrl = config['base_url'] ?? 'https://api.linear.app/graphql';
197
+ this.config = config;
198
+ this.httpRunner = createHttpRunner(httpRunner);
199
+ }
200
+
201
+ private async graphQl<TData>(query: string, variables: Record<string, unknown>): Promise<TData | null> {
202
+ const headers: Record<string, string> = {
203
+ 'Content-Type': 'application/json'
204
+ };
205
+ if (this.token) {
206
+ headers['Authorization'] = `Bearer ${this.token}`;
207
+ }
208
+ const result = await this.httpRunner(this.baseUrl, {
209
+ method: 'POST',
210
+ headers,
211
+ body: JSON.stringify({ query, variables })
212
+ });
213
+ if (!result.ok || !result.body) {
214
+ return null;
215
+ }
216
+ const parsed = tryParseJson(result.body);
217
+ if (!parsed) {
218
+ return null;
219
+ }
220
+ if (Array.isArray(parsed['errors']) && parsed['errors'].length > 0) {
221
+ return null;
222
+ }
223
+ const data = parsed['data'];
224
+ if (data && typeof data === 'object') {
225
+ return data as TData;
226
+ }
227
+ return null;
228
+ }
229
+
230
+ private static parseIssueNode(value: unknown): LinearIssueNode | null {
231
+ if (!value || typeof value !== 'object') {
232
+ return null;
233
+ }
234
+ const raw = value as Record<string, unknown>;
235
+ const stateRaw = raw['state'];
236
+ return {
237
+ id: readString(raw['id']),
238
+ identifier: readString(raw['identifier']),
239
+ title: readString(raw['title']),
240
+ description: readString(raw['description']),
241
+ url: readString(raw['url']),
242
+ state:
243
+ stateRaw && typeof stateRaw === 'object'
244
+ ? {
245
+ name: readString((stateRaw as Record<string, unknown>)['name'])
246
+ }
247
+ : null
248
+ };
249
+ }
250
+
251
+ private async resolveIssueNode(issueId: string): Promise<LinearIssueNode | null> {
252
+ const byIdentifier = await this.graphQl<{ issueByIdentifier?: unknown }>(
253
+ 'query AopIssueByIdentifier($identifier: String!) { issueByIdentifier(identifier: $identifier) { id identifier title description url state { name } } }',
254
+ { identifier: issueId }
255
+ );
256
+ const identifierIssue = LinearIssueTracker.parseIssueNode(byIdentifier?.issueByIdentifier);
257
+ if (identifierIssue) {
258
+ return identifierIssue;
259
+ }
260
+
261
+ const byId = await this.graphQl<{ issue?: unknown }>(
262
+ 'query AopIssueById($id: String!) { issue(id: $id) { id identifier title description url state { name } } }',
263
+ { id: issueId }
264
+ );
265
+ return LinearIssueTracker.parseIssueNode(byId?.issue);
266
+ }
267
+
268
+ private resolveStateId(status: string): string | null {
269
+ const normalized = normalizeAopStatus(status);
270
+ const explicit = this.config[`state_id_${normalized}`];
271
+ if (explicit && explicit.length > 0) {
272
+ return explicit;
273
+ }
274
+ const defaultKey = mapDefaultLinearStatus(status);
275
+ if (!defaultKey) {
276
+ return null;
277
+ }
278
+ const defaultMapped = this.config[`state_id_${defaultKey}`];
279
+ return defaultMapped && defaultMapped.length > 0 ? defaultMapped : null;
280
+ }
281
+
282
+ async getIssue(issueId: string): Promise<Issue> {
283
+ const issue = await this.resolveIssueNode(issueId).catch(() => null);
284
+ if (!issue) {
285
+ return { id: issueId, title: '', body: '', status: 'unknown', url: '' };
286
+ }
287
+ return {
288
+ id: issue.identifier || issue.id || issueId,
289
+ title: issue.title,
290
+ body: issue.description,
291
+ status: lowerCaseStatus(issue.state?.name ?? ''),
292
+ url: issue.url
293
+ };
294
+ }
295
+
296
+ async updateIssueStatus(issueId: string, status: string): Promise<void> {
297
+ const issue = await this.resolveIssueNode(issueId).catch(() => null);
298
+ if (!issue?.id) {
299
+ return;
300
+ }
301
+ const stateId = this.resolveStateId(status);
302
+ if (!stateId) {
303
+ return;
304
+ }
305
+ await this.graphQl(
306
+ 'mutation AopIssueUpdate($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }',
307
+ { id: issue.id, stateId }
308
+ );
309
+ }
310
+
311
+ async addComment(issueId: string, comment: string): Promise<void> {
312
+ const issue = await this.resolveIssueNode(issueId).catch(() => null);
313
+ if (!issue?.id) {
314
+ return;
315
+ }
316
+ await this.graphQl(
317
+ 'mutation AopCommentCreate($issueId: String!, $body: String!) { commentCreate(input: { issueId: $issueId, body: $body }) { success } }',
318
+ { issueId: issue.id, body: comment }
319
+ );
320
+ }
321
+ }
322
+
323
+ function toJiraDescription(value: unknown): string {
324
+ if (typeof value === 'string') {
325
+ return value;
326
+ }
327
+ if (!value || typeof value !== 'object') {
328
+ return '';
329
+ }
330
+ return JSON.stringify(value);
331
+ }
332
+
333
+ // --- Jira adapter (REST HTTP) ---
334
+ export class JiraIssueTracker implements IssueTracker {
335
+ private readonly baseUrl: string;
336
+ private readonly token: string;
337
+ private readonly email: string;
338
+ private readonly config: Record<string, string>;
339
+ private readonly httpRunner: HttpRunner;
340
+
341
+ constructor(config: Record<string, string> = {}, httpRunner?: HttpRunner) {
342
+ this.baseUrl = (config['base_url'] ?? config['url'] ?? '').replace(/\/+$/, '');
343
+ this.token = config['token'] ?? '';
344
+ this.email = config['email'] ?? config['user'] ?? '';
345
+ this.config = config;
346
+ this.httpRunner = createHttpRunner(httpRunner);
347
+ }
348
+
349
+ private authHeader(): string | null {
350
+ if (this.email && this.token) {
351
+ return `Basic ${Buffer.from(`${this.email}:${this.token}`, 'utf8').toString('base64')}`;
352
+ }
353
+ if (this.token) {
354
+ return `Bearer ${this.token}`;
355
+ }
356
+ return null;
357
+ }
358
+
359
+ private async requestJson<T>(
360
+ endpoint: string,
361
+ method: 'GET' | 'POST',
362
+ body?: Record<string, unknown>
363
+ ): Promise<T | null> {
364
+ if (!this.baseUrl) {
365
+ return null;
366
+ }
367
+ const headers: Record<string, string> = {
368
+ Accept: 'application/json'
369
+ };
370
+ const authHeader = this.authHeader();
371
+ if (authHeader) {
372
+ headers['Authorization'] = authHeader;
373
+ }
374
+ if (body !== undefined) {
375
+ headers['Content-Type'] = 'application/json';
376
+ }
377
+ const result = await this.httpRunner(`${this.baseUrl}${endpoint}`, {
378
+ method,
379
+ headers,
380
+ body: body === undefined ? undefined : JSON.stringify(body)
381
+ });
382
+ if (!result.ok || !result.body) {
383
+ return null;
384
+ }
385
+ const parsed = tryParseJson(result.body);
386
+ return parsed as T | null;
387
+ }
388
+
389
+ private resolveTransitionName(status: string): string {
390
+ const normalized = normalizeAopStatus(status);
391
+ const explicit = this.config[`transition_${normalized}`];
392
+ if (explicit && explicit.length > 0) {
393
+ return explicit.toLowerCase();
394
+ }
395
+ return mapDefaultJiraTransition(status);
396
+ }
397
+
398
+ async getIssue(issueId: string): Promise<Issue> {
399
+ const payload = await this.requestJson<{
400
+ key?: string;
401
+ fields?: {
402
+ summary?: unknown;
403
+ description?: unknown;
404
+ status?: {
405
+ name?: unknown;
406
+ };
407
+ };
408
+ }>(`/rest/api/2/issue/${encodeURIComponent(issueId)}?fields=summary,description,status`, 'GET');
409
+ if (!payload) {
410
+ return { id: issueId, title: '', body: '', status: 'unknown', url: '' };
411
+ }
412
+ const fields = payload.fields ?? {};
413
+ return {
414
+ id: readString(payload.key) || issueId,
415
+ title: readString(fields.summary),
416
+ body: toJiraDescription(fields.description),
417
+ status: lowerCaseStatus(readString(fields.status?.name)),
418
+ url: this.baseUrl ? `${this.baseUrl}/browse/${issueId}` : ''
419
+ };
420
+ }
421
+
422
+ async updateIssueStatus(issueId: string, status: string): Promise<void> {
423
+ const transitionsPayload = await this.requestJson<{ transitions?: Array<{ id?: string; name?: string }> }>(
424
+ `/rest/api/2/issue/${encodeURIComponent(issueId)}/transitions`,
425
+ 'GET'
426
+ );
427
+ const transitions = transitionsPayload?.transitions ?? [];
428
+ const targetName = this.resolveTransitionName(status);
429
+ const transition = transitions.find(
430
+ (item) => typeof item.name === 'string' && item.name.toLowerCase() === targetName
431
+ );
432
+ if (!transition?.id) {
433
+ return;
434
+ }
435
+ await this.requestJson(
436
+ `/rest/api/2/issue/${encodeURIComponent(issueId)}/transitions`,
437
+ 'POST',
438
+ { transition: { id: transition.id } }
439
+ );
440
+ }
441
+
442
+ async addComment(issueId: string, comment: string): Promise<void> {
443
+ await this.requestJson(
444
+ `/rest/api/2/issue/${encodeURIComponent(issueId)}/comment`,
445
+ 'POST',
446
+ { body: comment }
447
+ );
448
+ }
449
+ }
450
+
451
+ // --- Factory ---
452
+ export function createIssueTracker(
453
+ config: IssueTrackerConfig | undefined,
454
+ ghRunner?: GhRunner,
455
+ httpRunner?: HttpRunner
456
+ ): IssueTracker | undefined {
457
+ if (!config) {return undefined;}
458
+ if (config.enabled === false) {return undefined;}
459
+ switch (config.type) {
460
+ case 'github':
461
+ return new GitHubIssueTracker(config.config ?? {}, ghRunner);
462
+ case 'linear':
463
+ return new LinearIssueTracker(config.config ?? {}, httpRunner);
464
+ case 'jira':
465
+ return new JiraIssueTracker(config.config ?? {}, httpRunner);
466
+ default:
467
+ return undefined;
468
+ }
469
+ }
@@ -4,6 +4,7 @@ import { runGit } from '../../core/git.js';
4
4
  import { ERROR_CODES } from '../../core/error-codes.js';
5
5
  import { fail } from '../../core/response.js';
6
6
  import { GATE_RESULT, STATUS } from '../../core/constants.js';
7
+ import { resolveDepBlocked } from './dependency-scheduler-service.js';
7
8
 
8
9
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
10
  type AnyRecord = Record<string, any>;
@@ -51,12 +52,58 @@ export class MergeService {
51
52
  this.port = port;
52
53
  }
53
54
 
55
+ private async cleanupMergedFeatureArtifacts(featureId: string): Promise<void> {
56
+ const repoRoot = this.port.getRepoRoot();
57
+ const worktreePath = this.port.worktreePath(featureId);
58
+
59
+ const removeWorktreeResult = await runGit(repoRoot, ['worktree', 'remove', '--force', worktreePath]);
60
+ if (removeWorktreeResult.code !== 0) {
61
+ // Best-effort cleanup; merged state must remain intact even if git cleanup fails.
62
+ console.warn(
63
+ `[aop] auto-cleanup worktree removal failed for ${featureId}: ${removeWorktreeResult.stderr.trim() || `exit ${removeWorktreeResult.code}`}`
64
+ );
65
+ }
66
+
67
+ const removeBranchResult = await runGit(repoRoot, ['branch', '-D', featureId]);
68
+ if (removeBranchResult.code !== 0) {
69
+ // Best-effort cleanup; branch may already be removed.
70
+ console.warn(
71
+ `[aop] auto-cleanup branch removal failed for ${featureId}: ${removeBranchResult.stderr.trim() || `exit ${removeBranchResult.code}`}`
72
+ );
73
+ }
74
+
75
+ await fs.rm(this.port.featurePath(featureId), { recursive: true, force: true });
76
+ }
77
+
78
+ private scheduleFeatureCleanup(featureId: string, gracePeriodSeconds: number): void {
79
+ const delayMs = Math.max(0, Math.floor(gracePeriodSeconds * 1000));
80
+ const timeout = setTimeout(() => {
81
+ void this.cleanupMergedFeatureArtifacts(featureId).catch((error) => {
82
+ console.warn(
83
+ `[aop] auto-cleanup failed for ${featureId}: ${error instanceof Error ? error.message : String(error)}`
84
+ );
85
+ });
86
+ }, delayMs);
87
+ if (typeof timeout.unref === 'function') {
88
+ timeout.unref();
89
+ }
90
+ }
91
+
54
92
  async featureReadyToMerge(
55
93
  featureId: string,
56
94
  commitMessage: string,
57
95
  mergeStrategy: string,
58
96
  userApprovalToken: string | null
59
- ): Promise<{ data: { feature_id: string; merge_sha: string; merge_strategy: string; status: string } }> {
97
+ ): Promise<{
98
+ data: {
99
+ feature_id: string;
100
+ merge_sha: string;
101
+ merge_strategy: string;
102
+ status: string;
103
+ retained_for_cleanup?: boolean;
104
+ cleanup_grace_period_seconds?: number;
105
+ };
106
+ }> {
60
107
  const policy = this.port.getPolicySnapshot();
61
108
  const state = await this.port.readState(featureId);
62
109
 
@@ -227,19 +274,36 @@ export class MergeService {
227
274
  runtimeSessions.feature_sessions = featureSessions;
228
275
  index.runtime_sessions = runtimeSessions;
229
276
 
277
+ // N4: Promote features that were blocked waiting for this feature to merge
278
+ resolveDepBlocked(index, featureId);
279
+
230
280
  index.version += 1;
231
281
  index.updated_at = nowIso();
232
282
  await this.port.writeIndex(index);
233
283
  });
234
284
 
235
- await fs.rm(this.port.featurePath(featureId), { recursive: true, force: true });
285
+ const cleanupPolicy = asRecord(policy.cleanup);
286
+ const autoAfterMerge = cleanupPolicy.auto_after_merge === true;
287
+ const gracePeriodSeconds =
288
+ typeof cleanupPolicy.grace_period_seconds === 'number' && Number.isFinite(cleanupPolicy.grace_period_seconds)
289
+ ? cleanupPolicy.grace_period_seconds
290
+ : 3600;
291
+ const shouldCleanupNow = autoAfterMerge && gracePeriodSeconds <= 0;
292
+
293
+ if (shouldCleanupNow) {
294
+ await this.cleanupMergedFeatureArtifacts(featureId);
295
+ } else if (autoAfterMerge) {
296
+ this.scheduleFeatureCleanup(featureId, gracePeriodSeconds);
297
+ }
236
298
 
237
299
  return {
238
300
  data: {
239
301
  feature_id: featureId,
240
302
  merge_sha: mergeSha,
241
303
  merge_strategy: mergeStrategy,
242
- status: STATUS.MERGED
304
+ status: STATUS.MERGED,
305
+ retained_for_cleanup: !shouldCleanupNow,
306
+ cleanup_grace_period_seconds: gracePeriodSeconds
243
307
  }
244
308
  };
245
309
  }