agentic-orchestrator 0.1.2 → 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 (300) 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 +8 -7
  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/aop.ts +1 -1
  49. package/apps/control-plane/src/cli/attach-command-handler.ts +120 -0
  50. package/apps/control-plane/src/cli/cleanup-command-handler.ts +190 -0
  51. package/apps/control-plane/src/cli/cli-argument-parser.ts +69 -3
  52. package/apps/control-plane/src/cli/dashboard-command-handler.ts +57 -0
  53. package/apps/control-plane/src/cli/help-command-handler.ts +163 -0
  54. package/apps/control-plane/src/cli/init-command-handler.ts +609 -0
  55. package/apps/control-plane/src/cli/resume-command-handler.ts +1 -0
  56. package/apps/control-plane/src/cli/retry-command-handler.ts +138 -0
  57. package/apps/control-plane/src/cli/run-command-handler.ts +115 -3
  58. package/apps/control-plane/src/cli/send-command-handler.ts +65 -0
  59. package/apps/control-plane/src/cli/status-command-handler.ts +102 -2
  60. package/apps/control-plane/src/cli/types.ts +26 -1
  61. package/apps/control-plane/src/core/constants.ts +8 -2
  62. package/apps/control-plane/src/core/error-codes.ts +3 -1
  63. package/apps/control-plane/src/core/gates.ts +170 -50
  64. package/apps/control-plane/src/core/kernel.ts +280 -5
  65. package/apps/control-plane/src/core/path-layout.ts +12 -0
  66. package/apps/control-plane/src/core/tool-caller.ts +36 -0
  67. package/apps/control-plane/src/core/workspace-hooks.ts +87 -0
  68. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +258 -9
  69. package/apps/control-plane/src/providers/providers.ts +235 -14
  70. package/apps/control-plane/src/supervisor/build-wave-executor.ts +129 -8
  71. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +123 -5
  72. package/apps/control-plane/src/supervisor/run-coordinator.ts +143 -6
  73. package/apps/control-plane/src/supervisor/runtime.ts +135 -6
  74. package/apps/control-plane/src/supervisor/types.ts +12 -21
  75. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +8 -0
  76. package/apps/control-plane/test/activity-monitor.spec.ts +294 -0
  77. package/apps/control-plane/test/adapter-registry.spec.ts +132 -0
  78. package/apps/control-plane/test/batch-operations.spec.ts +112 -0
  79. package/apps/control-plane/test/bootstrap-attach.spec.ts +102 -0
  80. package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +252 -0
  81. package/apps/control-plane/test/bootstrap.spec.ts +560 -0
  82. package/apps/control-plane/test/cleanup-command.spec.ts +301 -0
  83. package/apps/control-plane/test/cli-helpers.spec.ts +404 -1
  84. package/apps/control-plane/test/cli.unit.spec.ts +182 -1
  85. package/apps/control-plane/test/collision-queue.spec.ts +104 -1
  86. package/apps/control-plane/test/core-utils.spec.ts +175 -2
  87. package/apps/control-plane/test/cost-tracking.spec.ts +143 -0
  88. package/apps/control-plane/test/dashboard-api.integration.spec.ts +247 -0
  89. package/apps/control-plane/test/dashboard-client.spec.ts +116 -0
  90. package/apps/control-plane/test/dashboard-command.spec.ts +103 -0
  91. package/apps/control-plane/test/dependency-scheduler.spec.ts +189 -0
  92. package/apps/control-plane/test/epoch-tracking.spec.ts +4 -4
  93. package/apps/control-plane/test/feature-deletion-service.spec.ts +422 -0
  94. package/apps/control-plane/test/feature-lifecycle.spec.ts +202 -0
  95. package/apps/control-plane/test/git-spawn-error.spec.ts +24 -0
  96. package/apps/control-plane/test/incremental-gates.spec.ts +137 -0
  97. package/apps/control-plane/test/init-wizard.spec.ts +506 -0
  98. package/apps/control-plane/test/instance-isolation.spec.ts +83 -0
  99. package/apps/control-plane/test/issue-tracker.spec.ts +890 -0
  100. package/apps/control-plane/test/kernel.coverage.spec.ts +3 -5
  101. package/apps/control-plane/test/kernel.coverage2.spec.ts +871 -0
  102. package/apps/control-plane/test/kernel.spec.ts +13 -11
  103. package/apps/control-plane/test/lock-service.spec.ts +508 -0
  104. package/apps/control-plane/test/mcp-helpers.spec.ts +176 -0
  105. package/apps/control-plane/test/mcp.spec.ts +50 -15
  106. package/apps/control-plane/test/merge-service.spec.ts +67 -4
  107. package/apps/control-plane/test/multi-project.spec.ts +372 -0
  108. package/apps/control-plane/test/notifier-service.spec.ts +388 -0
  109. package/apps/control-plane/test/parallel-gates.spec.ts +312 -0
  110. package/apps/control-plane/test/patch-service.spec.ts +253 -0
  111. package/apps/control-plane/test/performance-analytics.spec.ts +338 -0
  112. package/apps/control-plane/test/planning-wave-executor.spec.ts +168 -0
  113. package/apps/control-plane/test/pr-monitor.spec.ts +385 -0
  114. package/apps/control-plane/test/providers.spec.ts +344 -1
  115. package/apps/control-plane/test/reactions.spec.ts +392 -0
  116. package/apps/control-plane/test/resume-command.spec.ts +390 -0
  117. package/apps/control-plane/test/run-coordinator.spec.ts +481 -2
  118. package/apps/control-plane/test/schema-date-time.spec.ts +46 -0
  119. package/apps/control-plane/test/service-retry-paths.spec.ts +30 -0
  120. package/apps/control-plane/test/services.spec.ts +95 -2
  121. package/apps/control-plane/test/session-management.spec.ts +450 -0
  122. package/apps/control-plane/test/spec-ingestion.spec.ts +190 -0
  123. package/apps/control-plane/test/supervisor-collaborators.spec.ts +699 -2
  124. package/apps/control-plane/test/supervisor.spec.ts +36 -30
  125. package/apps/control-plane/test/supervisor.unit.spec.ts +405 -0
  126. package/apps/control-plane/test/worker-decision-loop.spec.ts +57 -0
  127. package/apps/control-plane/test/workspace-hooks.spec.ts +177 -0
  128. package/apps/control-plane/vitest.config.ts +21 -5
  129. package/dist/apps/control-plane/application/adapters/adapter-registry.d.ts +44 -0
  130. package/dist/apps/control-plane/application/adapters/adapter-registry.js +76 -0
  131. package/dist/apps/control-plane/application/adapters/adapter-registry.js.map +1 -0
  132. package/dist/apps/control-plane/application/multi-project-loader.d.ts +31 -0
  133. package/dist/apps/control-plane/application/multi-project-loader.js +82 -0
  134. package/dist/apps/control-plane/application/multi-project-loader.js.map +1 -0
  135. package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +43 -0
  136. package/dist/apps/control-plane/application/services/activity-monitor-service.js +132 -0
  137. package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -0
  138. package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +28 -0
  139. package/dist/apps/control-plane/application/services/cost-tracking-service.js +48 -0
  140. package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -0
  141. package/dist/apps/control-plane/application/services/dependency-scheduler-service.d.ts +26 -0
  142. package/dist/apps/control-plane/application/services/dependency-scheduler-service.js +75 -0
  143. package/dist/apps/control-plane/application/services/dependency-scheduler-service.js.map +1 -0
  144. package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +2 -0
  145. package/dist/apps/control-plane/application/services/feature-deletion-service.js +6 -7
  146. package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
  147. package/dist/apps/control-plane/application/services/gate-interpolation-service.d.ts +7 -0
  148. package/dist/apps/control-plane/application/services/gate-interpolation-service.js +7 -0
  149. package/dist/apps/control-plane/application/services/gate-interpolation-service.js.map +1 -0
  150. package/dist/apps/control-plane/application/services/gate-service.js +32 -2
  151. package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
  152. package/dist/apps/control-plane/application/services/instance-isolation-service.d.ts +11 -0
  153. package/dist/apps/control-plane/application/services/instance-isolation-service.js +17 -0
  154. package/dist/apps/control-plane/application/services/instance-isolation-service.js.map +1 -0
  155. package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +65 -0
  156. package/dist/apps/control-plane/application/services/issue-tracker-service.js +358 -0
  157. package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -0
  158. package/dist/apps/control-plane/application/services/merge-service.d.ts +4 -0
  159. package/dist/apps/control-plane/application/services/merge-service.js +44 -2
  160. package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
  161. package/dist/apps/control-plane/application/services/notifier-service.d.ts +74 -0
  162. package/dist/apps/control-plane/application/services/notifier-service.js +212 -0
  163. package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -0
  164. package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +39 -0
  165. package/dist/apps/control-plane/application/services/performance-analytics-service.js +75 -0
  166. package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -0
  167. package/dist/apps/control-plane/application/services/plan-service.d.ts +1 -0
  168. package/dist/apps/control-plane/application/services/plan-service.js +53 -0
  169. package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
  170. package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +44 -0
  171. package/dist/apps/control-plane/application/services/pr-monitor-service.js +192 -0
  172. package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -0
  173. package/dist/apps/control-plane/application/services/reactions-service.d.ts +67 -0
  174. package/dist/apps/control-plane/application/services/reactions-service.js +114 -0
  175. package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -0
  176. package/dist/apps/control-plane/application/services/reporting-service.d.ts +1 -0
  177. package/dist/apps/control-plane/application/services/reporting-service.js +13 -2
  178. package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
  179. package/dist/apps/control-plane/application/services/run-lease-service.d.ts +2 -0
  180. package/dist/apps/control-plane/application/services/run-lease-service.js +14 -38
  181. package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
  182. package/dist/apps/control-plane/application/tools/tool-metadata.js +3 -1
  183. package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
  184. package/dist/apps/control-plane/cli/aop.d.ts +1 -1
  185. package/dist/apps/control-plane/cli/aop.js +1 -1
  186. package/dist/apps/control-plane/cli/attach-command-handler.d.ts +12 -0
  187. package/dist/apps/control-plane/cli/attach-command-handler.js +98 -0
  188. package/dist/apps/control-plane/cli/attach-command-handler.js.map +1 -0
  189. package/dist/apps/control-plane/cli/cleanup-command-handler.d.ts +12 -0
  190. package/dist/apps/control-plane/cli/cleanup-command-handler.js +162 -0
  191. package/dist/apps/control-plane/cli/cleanup-command-handler.js.map +1 -0
  192. package/dist/apps/control-plane/cli/cli-argument-parser.js +73 -3
  193. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  194. package/dist/apps/control-plane/cli/dashboard-command-handler.d.ts +7 -0
  195. package/dist/apps/control-plane/cli/dashboard-command-handler.js +45 -0
  196. package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -0
  197. package/dist/apps/control-plane/cli/help-command-handler.d.ts +8 -0
  198. package/dist/apps/control-plane/cli/help-command-handler.js +146 -0
  199. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -0
  200. package/dist/apps/control-plane/cli/init-command-handler.d.ts +26 -0
  201. package/dist/apps/control-plane/cli/init-command-handler.js +517 -0
  202. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -0
  203. package/dist/apps/control-plane/cli/resume-command-handler.js +1 -1
  204. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  205. package/dist/apps/control-plane/cli/retry-command-handler.d.ts +8 -0
  206. package/dist/apps/control-plane/cli/retry-command-handler.js +111 -0
  207. package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -0
  208. package/dist/apps/control-plane/cli/run-command-handler.d.ts +5 -0
  209. package/dist/apps/control-plane/cli/run-command-handler.js +82 -3
  210. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  211. package/dist/apps/control-plane/cli/send-command-handler.d.ts +8 -0
  212. package/dist/apps/control-plane/cli/send-command-handler.js +55 -0
  213. package/dist/apps/control-plane/cli/send-command-handler.js.map +1 -0
  214. package/dist/apps/control-plane/cli/status-command-handler.d.ts +12 -1
  215. package/dist/apps/control-plane/cli/status-command-handler.js +55 -2
  216. package/dist/apps/control-plane/cli/status-command-handler.js.map +1 -1
  217. package/dist/apps/control-plane/cli/types.d.ts +26 -1
  218. package/dist/apps/control-plane/cli/types.js +15 -1
  219. package/dist/apps/control-plane/cli/types.js.map +1 -1
  220. package/dist/apps/control-plane/core/constants.d.ts +6 -0
  221. package/dist/apps/control-plane/core/constants.js +8 -2
  222. package/dist/apps/control-plane/core/constants.js.map +1 -1
  223. package/dist/apps/control-plane/core/error-codes.d.ts +2 -0
  224. package/dist/apps/control-plane/core/error-codes.js +3 -1
  225. package/dist/apps/control-plane/core/error-codes.js.map +1 -1
  226. package/dist/apps/control-plane/core/gates.d.ts +4 -0
  227. package/dist/apps/control-plane/core/gates.js +140 -43
  228. package/dist/apps/control-plane/core/gates.js.map +1 -1
  229. package/dist/apps/control-plane/core/kernel.d.ts +50 -1
  230. package/dist/apps/control-plane/core/kernel.js +220 -7
  231. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  232. package/dist/apps/control-plane/core/path-layout.d.ts +3 -0
  233. package/dist/apps/control-plane/core/path-layout.js +9 -0
  234. package/dist/apps/control-plane/core/path-layout.js.map +1 -1
  235. package/dist/apps/control-plane/core/tool-caller.d.ts +32 -0
  236. package/dist/apps/control-plane/core/tool-caller.js +2 -0
  237. package/dist/apps/control-plane/core/tool-caller.js.map +1 -0
  238. package/dist/apps/control-plane/core/workspace-hooks.d.ts +20 -0
  239. package/dist/apps/control-plane/core/workspace-hooks.js +69 -0
  240. package/dist/apps/control-plane/core/workspace-hooks.js.map +1 -0
  241. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +245 -9
  242. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  243. package/dist/apps/control-plane/providers/providers.d.ts +42 -3
  244. package/dist/apps/control-plane/providers/providers.js +216 -5
  245. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  246. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
  247. package/dist/apps/control-plane/supervisor/build-wave-executor.js +115 -6
  248. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  249. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
  250. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +109 -5
  251. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  252. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +15 -0
  253. package/dist/apps/control-plane/supervisor/run-coordinator.js +132 -6
  254. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  255. package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
  256. package/dist/apps/control-plane/supervisor/runtime.js +110 -6
  257. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  258. package/dist/apps/control-plane/supervisor/types.d.ts +9 -16
  259. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  260. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +3 -0
  261. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +5 -0
  262. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  263. package/eslint.config.mjs +2 -1
  264. package/package.json +12 -2
  265. package/packages/web-dashboard/next-env.d.ts +5 -0
  266. package/packages/web-dashboard/next.config.js +7 -0
  267. package/packages/web-dashboard/package.json +26 -0
  268. package/packages/web-dashboard/src/app/api/actions/route.ts +64 -0
  269. package/packages/web-dashboard/src/app/api/events/route.ts +51 -0
  270. package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +256 -0
  271. package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +10 -0
  272. package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +25 -0
  273. package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +63 -0
  274. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +16 -0
  275. package/packages/web-dashboard/src/app/api/projects/route.ts +31 -0
  276. package/packages/web-dashboard/src/app/api/status/route.ts +15 -0
  277. package/packages/web-dashboard/src/app/globals.css +2 -0
  278. package/packages/web-dashboard/src/app/layout.tsx +15 -0
  279. package/packages/web-dashboard/src/app/page.tsx +393 -0
  280. package/packages/web-dashboard/src/lib/aop-client.ts +244 -0
  281. package/packages/web-dashboard/src/lib/multi-project-config.ts +116 -0
  282. package/packages/web-dashboard/src/lib/orchestrator-tools.ts +284 -0
  283. package/packages/web-dashboard/src/lib/types.ts +58 -0
  284. package/packages/web-dashboard/tsconfig.json +40 -0
  285. package/packages/web-dashboard/vitest.config.ts +6 -0
  286. package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +1764 -0
  287. package/spec-files/outstanding/agentic_orchestrator_enterprise_governance_dashboard_spec.md +348 -0
  288. package/spec-files/outstanding/agentic_orchestrator_knowledge_canary_spec.md +344 -0
  289. package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +374 -0
  290. package/spec-files/outstanding/agentic_orchestrator_performance_improvements_spec.md +1059 -0
  291. package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +466 -0
  292. package/spec-files/outstanding/agentic_orchestrator_quality_adoption_execution_spec.md +198 -0
  293. package/spec-files/outstanding/agentic_orchestrator_validator_hardening_spec.md +365 -0
  294. package/spec-files/progress.md +481 -52
  295. /package/spec-files/{agentic_orchestrator_cli_delete_command_spec.md → completed/agentic_orchestrator_cli_delete_command_spec.md} +0 -0
  296. /package/spec-files/{agentic_orchestrator_dot_aop_generated_artifacts_spec.md → completed/agentic_orchestrator_dot_aop_generated_artifacts_spec.md} +0 -0
  297. /package/spec-files/{agentic_orchestrator_mcp_formalization_spec.md → completed/agentic_orchestrator_mcp_formalization_spec.md} +0 -0
  298. /package/spec-files/{agentic_orchestrator_oop_refactor_spec.md → completed/agentic_orchestrator_oop_refactor_spec.md} +0 -0
  299. /package/spec-files/{agentic_orchestrator_single_global_orchestrator_spec.md → completed/agentic_orchestrator_single_global_orchestrator_spec.md} +0 -0
  300. /package/spec-files/{agentic_orchestrator_spec.md → completed/agentic_orchestrator_spec.md} +0 -0
@@ -44,11 +44,11 @@ describe('AopKernel', () => {
44
44
  const kernel = new AopKernel(repoRoot);
45
45
  await kernel.ensureLoaded();
46
46
 
47
- const index = await (kernel as any).readIndex();
47
+ const runLease = await kernel.readRunLease();
48
48
  expect(await fs.readFile(path.join(repoRoot, '.aop', 'features', 'legacy_feature', 'spec.md'), 'utf8')).toContain('Legacy Spec');
49
- expect(index.runtime_sessions).toBeTruthy();
50
- expect(index.runtime_sessions.orchestrator_session_id).toBe('unknown');
51
- expect(typeof index.runtime_sessions.lease_expires_at).toBe('string');
49
+ expect(runLease).toBeTruthy();
50
+ expect(runLease.orchestrator_session_id).toBe('unknown');
51
+ expect(typeof runLease.lease_expires_at).toBe('string');
52
52
  });
53
53
 
54
54
  it('prefers existing .aop runtime artifacts over legacy agentic artifacts when both exist', async () => {
@@ -208,17 +208,19 @@ describe('AopKernel', () => {
208
208
  required_resources: ['openapi']
209
209
  }
210
210
  ];
211
- const runtimeSessions = index.runtime_sessions as { feature_sessions?: Record<string, unknown> };
212
- runtimeSessions.feature_sessions = {
213
- ...(runtimeSessions.feature_sessions ?? {}),
211
+ await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
212
+
213
+ // Set up feature session in run-lease.json (separate from index.json)
214
+ const currentLease = await kernel.readRunLease();
215
+ currentLease.feature_sessions = {
216
+ ...(currentLease.feature_sessions ?? {}),
214
217
  feature_delete: {
215
218
  planner_session_id: 'planner:1',
216
219
  builder_session_id: 'builder:1',
217
220
  qa_session_id: 'qa:1'
218
221
  }
219
222
  };
220
- index.runtime_sessions = runtimeSessions;
221
- await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
223
+ await kernel.writeRunLease(currentLease);
222
224
 
223
225
  const preview = await kernel.invoke(
224
226
  'feature.delete',
@@ -265,12 +267,12 @@ describe('AopKernel', () => {
265
267
  active: string[];
266
268
  blocked_queue: Array<{ feature_id?: string }>;
267
269
  locks: Record<string, string | null>;
268
- runtime_sessions: { feature_sessions?: Record<string, unknown> };
269
270
  };
270
271
  expect(postIndex.active).not.toContain('feature_delete');
271
272
  expect(postIndex.blocked_queue.map((entry) => entry.feature_id)).not.toContain('feature_delete');
272
273
  expect(postIndex.locks.openapi).toBeNull();
273
- expect(postIndex.runtime_sessions.feature_sessions?.feature_delete).toBeUndefined();
274
+ const postLease = await kernel.readRunLease();
275
+ expect(postLease.feature_sessions?.feature_delete).toBeUndefined();
274
276
 
275
277
  const rerun = await kernel.invoke(
276
278
  'feature.delete',
@@ -0,0 +1,508 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { LockService } from '../src/application/services/lock-service.js';
3
+ import type { LockServicePort } from '../src/application/services/lock-service.js';
4
+ import { ERROR_CODES } from '../src/core/error-codes.js';
5
+ import { pathExists } from '../src/core/fs.js';
6
+
7
+ vi.mock('../src/core/fs.js', async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import('../src/core/fs.js')>();
9
+ return {
10
+ ...actual,
11
+ pathExists: vi.fn(async () => false),
12
+ nowIso: actual.nowIso
13
+ };
14
+ });
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ type AnyRecord = Record<string, any>;
18
+
19
+ function makeIndex(overrides: Partial<AnyRecord> = {}): AnyRecord {
20
+ return {
21
+ version: 1,
22
+ active: ['feature-a'],
23
+ blocked: [],
24
+ merged: [],
25
+ locks: {},
26
+ lock_leases: {},
27
+ updated_at: '2026-01-01T00:00:00.000Z',
28
+ ...overrides
29
+ };
30
+ }
31
+
32
+ function makePolicy(overrides: Partial<AnyRecord> = {}): AnyRecord {
33
+ return {
34
+ locks: {
35
+ resources: ['ci', 'deploy'],
36
+ acquire_behavior: 'wait',
37
+ default_wait_timeout_seconds: 1,
38
+ lease_ttl_seconds: 60,
39
+ acquire_backoff: {
40
+ initial_ms: 10,
41
+ max_ms: 100,
42
+ multiplier: 2,
43
+ jitter_ms: 0
44
+ },
45
+ ...overrides
46
+ }
47
+ };
48
+ }
49
+
50
+ function makePort(indexData: AnyRecord, policyData: AnyRecord): LockServicePort {
51
+ let stored = { ...indexData };
52
+ return {
53
+ withIndexLock: vi.fn(async (fn: () => Promise<unknown>) => fn()),
54
+ readIndex: vi.fn(async () => ({ ...stored })),
55
+ writeIndex: vi.fn(async (idx: AnyRecord) => {
56
+ stored = { ...idx };
57
+ }),
58
+ updateState: vi.fn(async () => ({})),
59
+ statePath: vi.fn((featureId: string) => `/tmp/.aop/features/${featureId}/state.md`),
60
+ getPolicySnapshot: vi.fn(() => policyData)
61
+ };
62
+ }
63
+
64
+ describe('LockService', () => {
65
+ describe('locksAcquire', () => {
66
+ it('GIVEN_valid_resource_and_featureId_WHEN_lock_is_free_THEN_acquires_lock', async () => {
67
+ const port = makePort(makeIndex(), makePolicy());
68
+ const svc = new LockService(port);
69
+
70
+ const result = await svc.locksAcquire('ci', 'feature-a', null);
71
+
72
+ expect(result.data.resource).toBe('ci');
73
+ expect(result.data.feature_id).toBe('feature-a');
74
+ expect(result.data.lease_id).toBeDefined();
75
+ expect(result.data.expires_at).toBeDefined();
76
+ expect(result.data.stale_reclaimed).toBe(false);
77
+ expect(port.writeIndex).toHaveBeenCalled();
78
+ expect(port.updateState).toHaveBeenCalled();
79
+ });
80
+
81
+ it('GIVEN_featureId_already_holds_lock_WHEN_locksAcquire_called_THEN_re_acquires_same_lock', async () => {
82
+ const index = makeIndex({
83
+ locks: { ci: 'feature-a' },
84
+ lock_leases: {
85
+ ci: {
86
+ holder: 'feature-a',
87
+ lease_id: 'existing-lease',
88
+ expires_at: new Date(Date.now() + 60000).toISOString()
89
+ }
90
+ }
91
+ });
92
+ const port = makePort(index, makePolicy());
93
+ const svc = new LockService(port);
94
+
95
+ const result = await svc.locksAcquire('ci', 'feature-a', null);
96
+
97
+ expect(result.data.feature_id).toBe('feature-a');
98
+ expect(result.data.stale_reclaimed).toBe(false);
99
+ });
100
+
101
+ it('GIVEN_expired_lease_WHEN_locksAcquire_called_THEN_reclaims_stale_lock', async () => {
102
+ const index = makeIndex({
103
+ locks: { ci: 'feature-old' },
104
+ lock_leases: {
105
+ ci: {
106
+ holder: 'feature-old',
107
+ lease_id: 'old-lease',
108
+ expires_at: new Date(Date.now() - 5000).toISOString()
109
+ }
110
+ }
111
+ });
112
+ const port = makePort(index, makePolicy());
113
+ const svc = new LockService(port);
114
+
115
+ const result = await svc.locksAcquire('ci', 'feature-a', null);
116
+
117
+ expect(result.data.stale_reclaimed).toBe(true);
118
+ expect(result.evidence).toBeDefined();
119
+ expect(result.evidence?.code).toBe(ERROR_CODES.STALE_LOCK_RECLAIMED);
120
+ });
121
+
122
+ it('GIVEN_unknown_resource_WHEN_locksAcquire_called_THEN_throws_invalid_argument', async () => {
123
+ const port = makePort(makeIndex(), makePolicy());
124
+ const svc = new LockService(port);
125
+
126
+ await expect(svc.locksAcquire('unknown-resource', 'feature-a', null)).rejects.toMatchObject({
127
+ normalizedResponse: expect.objectContaining({
128
+ error: expect.objectContaining({ code: ERROR_CODES.INVALID_ARGUMENT })
129
+ })
130
+ });
131
+ });
132
+
133
+ it('GIVEN_null_resource_WHEN_locksAcquire_called_THEN_throws_invalid_argument', async () => {
134
+ const port = makePort(makeIndex(), makePolicy());
135
+ const svc = new LockService(port);
136
+
137
+ await expect(svc.locksAcquire(null, 'feature-a', null)).rejects.toMatchObject({
138
+ normalizedResponse: expect.objectContaining({
139
+ error: expect.objectContaining({ code: ERROR_CODES.INVALID_ARGUMENT })
140
+ })
141
+ });
142
+ });
143
+
144
+ it('GIVEN_null_featureId_WHEN_locksAcquire_called_THEN_throws_invalid_argument', async () => {
145
+ const port = makePort(makeIndex(), makePolicy());
146
+ const svc = new LockService(port);
147
+
148
+ await expect(svc.locksAcquire('ci', null, null)).rejects.toMatchObject({
149
+ normalizedResponse: expect.objectContaining({
150
+ error: expect.objectContaining({ code: ERROR_CODES.INVALID_ARGUMENT })
151
+ })
152
+ });
153
+ });
154
+
155
+ it('GIVEN_lock_held_by_other_AND_behavior_is_fail_WHEN_locksAcquire_called_THEN_throws_lock_conflict', async () => {
156
+ const index = makeIndex({
157
+ locks: { ci: 'feature-b' },
158
+ lock_leases: {
159
+ ci: {
160
+ holder: 'feature-b',
161
+ lease_id: 'b-lease',
162
+ expires_at: new Date(Date.now() + 60000).toISOString()
163
+ }
164
+ }
165
+ });
166
+ const policy = makePolicy({ acquire_behavior: 'fail' });
167
+ const port = makePort(index, policy);
168
+ const svc = new LockService(port);
169
+
170
+ await expect(svc.locksAcquire('ci', 'feature-a', null)).rejects.toMatchObject({
171
+ normalizedResponse: expect.objectContaining({
172
+ error: expect.objectContaining({ code: ERROR_CODES.LOCK_CONFLICT })
173
+ })
174
+ });
175
+ });
176
+
177
+ it('GIVEN_lock_held_AND_behavior_is_wait_AND_timeout_exceeded_THEN_throws_lock_conflict', async () => {
178
+ const index = makeIndex({
179
+ locks: { ci: 'feature-b' },
180
+ lock_leases: {
181
+ ci: {
182
+ holder: 'feature-b',
183
+ lease_id: 'b-lease',
184
+ expires_at: new Date(Date.now() + 60000).toISOString()
185
+ }
186
+ }
187
+ });
188
+ // Very short timeout so the test completes quickly
189
+ const policy = makePolicy({
190
+ acquire_behavior: 'wait',
191
+ default_wait_timeout_seconds: 0,
192
+ acquire_backoff: { initial_ms: 5, max_ms: 5, multiplier: 1, jitter_ms: 0 }
193
+ });
194
+ const port = makePort(index, policy);
195
+ const svc = new LockService(port);
196
+
197
+ await expect(svc.locksAcquire('ci', 'feature-a', 0)).rejects.toMatchObject({
198
+ normalizedResponse: expect.objectContaining({
199
+ error: expect.objectContaining({ code: ERROR_CODES.LOCK_CONFLICT })
200
+ })
201
+ });
202
+ });
203
+
204
+ it('GIVEN_explicit_wait_timeout_WHEN_locksAcquire_called_THEN_uses_provided_timeout', async () => {
205
+ const index = makeIndex({
206
+ locks: { ci: 'feature-b' },
207
+ lock_leases: {
208
+ ci: {
209
+ holder: 'feature-b',
210
+ lease_id: 'b-lease',
211
+ expires_at: new Date(Date.now() + 60000).toISOString()
212
+ }
213
+ }
214
+ });
215
+ const policy = makePolicy({
216
+ acquire_behavior: 'wait',
217
+ acquire_backoff: { initial_ms: 5, max_ms: 5, multiplier: 1, jitter_ms: 0 }
218
+ });
219
+ const port = makePort(index, policy);
220
+ const svc = new LockService(port);
221
+
222
+ // Pass 0 seconds explicitly (overrides policy default)
223
+ await expect(svc.locksAcquire('ci', 'feature-a', 0)).rejects.toMatchObject({
224
+ normalizedResponse: expect.objectContaining({
225
+ error: expect.objectContaining({ code: ERROR_CODES.LOCK_CONFLICT })
226
+ })
227
+ });
228
+ });
229
+ });
230
+
231
+ describe('locksRelease', () => {
232
+ it('GIVEN_feature_holds_lock_WHEN_locksRelease_called_THEN_releases_lock', async () => {
233
+ const index = makeIndex({
234
+ locks: { ci: 'feature-a' },
235
+ lock_leases: {
236
+ ci: { holder: 'feature-a', lease_id: 'lease-1', expires_at: new Date(Date.now() + 60000).toISOString() }
237
+ }
238
+ });
239
+ const port = makePort(index, makePolicy());
240
+ const svc = new LockService(port);
241
+
242
+ const result = await svc.locksRelease('ci', 'feature-a');
243
+
244
+ expect(result.data.released).toBe(true);
245
+ expect(result.data.resource).toBe('ci');
246
+ expect(port.writeIndex).toHaveBeenCalled();
247
+ expect(port.updateState).toHaveBeenCalled();
248
+ });
249
+
250
+ it('GIVEN_null_args_WHEN_locksRelease_called_THEN_throws_invalid_argument', async () => {
251
+ const port = makePort(makeIndex(), makePolicy());
252
+ const svc = new LockService(port);
253
+
254
+ await expect(svc.locksRelease(null, null)).rejects.toMatchObject({
255
+ normalizedResponse: expect.objectContaining({
256
+ error: expect.objectContaining({ code: ERROR_CODES.INVALID_ARGUMENT })
257
+ })
258
+ });
259
+ });
260
+
261
+ it('GIVEN_null_resource_WHEN_locksRelease_called_THEN_throws_invalid_argument', async () => {
262
+ const port = makePort(makeIndex(), makePolicy());
263
+ const svc = new LockService(port);
264
+
265
+ await expect(svc.locksRelease(null, 'feature-a')).rejects.toMatchObject({
266
+ normalizedResponse: expect.objectContaining({
267
+ error: expect.objectContaining({ code: ERROR_CODES.INVALID_ARGUMENT })
268
+ })
269
+ });
270
+ });
271
+
272
+ it('GIVEN_lock_held_by_other_feature_WHEN_locksRelease_called_THEN_throws_lock_conflict', async () => {
273
+ const index = makeIndex({
274
+ locks: { ci: 'feature-b' },
275
+ lock_leases: {
276
+ ci: { holder: 'feature-b', lease_id: 'lease-b', expires_at: new Date(Date.now() + 60000).toISOString() }
277
+ }
278
+ });
279
+ const port = makePort(index, makePolicy());
280
+ const svc = new LockService(port);
281
+
282
+ await expect(svc.locksRelease('ci', 'feature-a')).rejects.toMatchObject({
283
+ normalizedResponse: expect.objectContaining({
284
+ error: expect.objectContaining({ code: ERROR_CODES.LOCK_CONFLICT })
285
+ })
286
+ });
287
+ });
288
+
289
+ it('GIVEN_lock_not_held_WHEN_locksRelease_called_THEN_releases_successfully', async () => {
290
+ const index = makeIndex({ locks: { ci: null }, lock_leases: { ci: null } });
291
+ const port = makePort(index, makePolicy());
292
+ const svc = new LockService(port);
293
+
294
+ const result = await svc.locksRelease('ci', 'feature-a');
295
+
296
+ expect(result.data.released).toBe(true);
297
+ });
298
+
299
+ it('GIVEN_collisionQueueService_present_WHEN_locksRelease_called_THEN_reDrives_queue', async () => {
300
+ const index = makeIndex({ locks: { ci: 'feature-a' }, lock_leases: { ci: null } });
301
+ const port = makePort(index, makePolicy());
302
+ const reDriveFn = vi.fn(async () => ({ data: { processed: 1, retried: 1, still_blocked: 0 } }));
303
+ port.collisionQueueService = { reDriveBlockedQueue: reDriveFn };
304
+ const svc = new LockService(port);
305
+
306
+ await svc.locksRelease('ci', 'feature-a');
307
+
308
+ expect(reDriveFn).toHaveBeenCalled();
309
+ });
310
+
311
+ it('GIVEN_collisionQueueService_throws_WHEN_locksRelease_called_THEN_still_succeeds', async () => {
312
+ const index = makeIndex({ locks: { ci: 'feature-a' }, lock_leases: { ci: null } });
313
+ const port = makePort(index, makePolicy());
314
+ port.collisionQueueService = {
315
+ reDriveBlockedQueue: vi.fn(async () => {
316
+ throw new Error('queue error');
317
+ })
318
+ };
319
+ const svc = new LockService(port);
320
+
321
+ // Should not throw despite reDrive failure
322
+ const result = await svc.locksRelease('ci', 'feature-a');
323
+ expect(result.data.released).toBe(true);
324
+ });
325
+
326
+ it('GIVEN_no_collisionQueueService_WHEN_locksRelease_called_THEN_succeeds_without_redriving', async () => {
327
+ const index = makeIndex({ locks: { ci: 'feature-a' }, lock_leases: { ci: null } });
328
+ const port = makePort(index, makePolicy());
329
+ // No collisionQueueService attached
330
+ const svc = new LockService(port);
331
+
332
+ const result = await svc.locksRelease('ci', 'feature-a');
333
+ expect(result.data.released).toBe(true);
334
+ });
335
+ });
336
+
337
+ describe('renewLeases', () => {
338
+ it('GIVEN_matching_lease_holders_WHEN_renewLeases_called_THEN_extends_expiry', async () => {
339
+ const oldExpiry = new Date(Date.now() + 10000).toISOString();
340
+ const index = makeIndex({
341
+ locks: { ci: 'feature-a' },
342
+ lock_leases: {
343
+ ci: { holder: 'feature-a', lease_id: 'lease-1', expires_at: oldExpiry }
344
+ }
345
+ });
346
+ const port = makePort(index, makePolicy());
347
+ const svc = new LockService(port);
348
+
349
+ await svc.renewLeases(['feature-a']);
350
+
351
+ expect(port.writeIndex).toHaveBeenCalled();
352
+ const written = vi.mocked(port.writeIndex).mock.calls[0][0];
353
+ const newExpiry = written.lock_leases.ci.expires_at;
354
+ expect(new Date(newExpiry).getTime()).toBeGreaterThan(new Date(oldExpiry).getTime());
355
+ });
356
+
357
+ it('GIVEN_no_matching_holders_WHEN_renewLeases_called_THEN_does_not_write_index', async () => {
358
+ const index = makeIndex({
359
+ lock_leases: {
360
+ ci: { holder: 'feature-b', lease_id: 'lease-b', expires_at: new Date(Date.now() + 60000).toISOString() }
361
+ }
362
+ });
363
+ const port = makePort(index, makePolicy());
364
+ const svc = new LockService(port);
365
+
366
+ await svc.renewLeases(['feature-a']);
367
+
368
+ expect(port.writeIndex).not.toHaveBeenCalled();
369
+ });
370
+
371
+ it('GIVEN_null_lease_entry_WHEN_renewLeases_called_THEN_skips_entry', async () => {
372
+ const index = makeIndex({
373
+ lock_leases: { ci: null }
374
+ });
375
+ const port = makePort(index, makePolicy());
376
+ const svc = new LockService(port);
377
+
378
+ await svc.renewLeases(['feature-a']);
379
+
380
+ expect(port.writeIndex).not.toHaveBeenCalled();
381
+ });
382
+
383
+ it('GIVEN_empty_featureIds_WHEN_renewLeases_called_THEN_does_not_write_index', async () => {
384
+ const index = makeIndex({
385
+ lock_leases: {
386
+ ci: { holder: 'feature-a', lease_id: 'lease-1', expires_at: new Date(Date.now() + 60000).toISOString() }
387
+ }
388
+ });
389
+ const port = makePort(index, makePolicy());
390
+ const svc = new LockService(port);
391
+
392
+ await svc.renewLeases([]);
393
+
394
+ expect(port.writeIndex).not.toHaveBeenCalled();
395
+ });
396
+ });
397
+
398
+ describe('recoverFromState', () => {
399
+ it('GIVEN_no_stale_leases_WHEN_recoverFromState_called_THEN_returns_empty_stale_list', async () => {
400
+ const index = makeIndex({
401
+ lock_leases: {
402
+ ci: { holder: 'feature-a', lease_id: 'lease-1', expires_at: new Date(Date.now() + 60000).toISOString() }
403
+ }
404
+ });
405
+ const port = makePort(index, makePolicy());
406
+ const svc = new LockService(port);
407
+
408
+ const result = await svc.recoverFromState();
409
+
410
+ expect(result.data.recovered).toBe(true);
411
+ expect(result.data.stale_features).toEqual([]);
412
+ expect(port.writeIndex).not.toHaveBeenCalled();
413
+ });
414
+
415
+ it('GIVEN_stale_leases_with_valid_state_paths_WHEN_recoverFromState_called_THEN_clears_and_updates_state', async () => {
416
+ vi.mocked(pathExists).mockResolvedValueOnce(true);
417
+ const index = makeIndex({
418
+ lock_leases: {
419
+ ci: {
420
+ holder: 'feature-stale',
421
+ lease_id: 'old-lease',
422
+ expires_at: new Date(Date.now() - 10000).toISOString()
423
+ }
424
+ },
425
+ locks: { ci: 'feature-stale' }
426
+ });
427
+ const port = makePort(index, makePolicy());
428
+ const svc = new LockService(port);
429
+
430
+ const result = await svc.recoverFromState();
431
+
432
+ expect(result.data.recovered).toBe(true);
433
+ expect(result.data.stale_features).toContain('feature-stale');
434
+ expect(port.writeIndex).toHaveBeenCalled();
435
+ expect(port.updateState).toHaveBeenCalled();
436
+ });
437
+
438
+ it('GIVEN_null_lease_entry_WHEN_recoverFromState_called_THEN_skips_entry', async () => {
439
+ const index = makeIndex({
440
+ lock_leases: { ci: null }
441
+ });
442
+ const port = makePort(index, makePolicy());
443
+ const svc = new LockService(port);
444
+
445
+ const result = await svc.recoverFromState();
446
+
447
+ expect(result.data.stale_features).toEqual([]);
448
+ });
449
+
450
+ it('GIVEN_stale_lease_with_missing_state_file_WHEN_recoverFromState_called_THEN_skips_updateState', async () => {
451
+ // pathExists defaults to false (mock at top of file), so updateState should be skipped
452
+ const index = makeIndex({
453
+ lock_leases: {
454
+ deploy: {
455
+ holder: 'ghost-feature',
456
+ lease_id: 'ghost-lease',
457
+ expires_at: new Date(Date.now() - 5000).toISOString()
458
+ }
459
+ },
460
+ locks: { deploy: 'ghost-feature' }
461
+ });
462
+ const port = makePort(index, makePolicy());
463
+ const svc = new LockService(port);
464
+
465
+ const result = await svc.recoverFromState();
466
+
467
+ expect(result.data.stale_features).toContain('ghost-feature');
468
+ // updateState should NOT be called since the file doesn't exist
469
+ expect(port.updateState).not.toHaveBeenCalled();
470
+ });
471
+
472
+ it('GIVEN_multiple_stale_leases_for_same_feature_WHEN_recoverFromState_THEN_deduplicates', async () => {
473
+ // pathExists defaults to false; updateState will be skipped but stale_features should still be deduplicated
474
+ const index = makeIndex({
475
+ lock_leases: {
476
+ ci: {
477
+ holder: 'feature-x',
478
+ lease_id: 'lease-1',
479
+ expires_at: new Date(Date.now() - 5000).toISOString()
480
+ },
481
+ deploy: {
482
+ holder: 'feature-x',
483
+ lease_id: 'lease-2',
484
+ expires_at: new Date(Date.now() - 5000).toISOString()
485
+ }
486
+ },
487
+ locks: { ci: 'feature-x', deploy: 'feature-x' }
488
+ });
489
+ const port = makePort(index, makePolicy());
490
+ const svc = new LockService(port);
491
+
492
+ const result = await svc.recoverFromState();
493
+
494
+ // Deduplicated: feature-x appears once
495
+ expect(result.data.stale_features).toEqual(['feature-x']);
496
+ });
497
+
498
+ it('GIVEN_active_features_in_index_WHEN_recoverFromState_THEN_returns_active_in_data', async () => {
499
+ const index = makeIndex({ active: ['feature-z'], lock_leases: {} });
500
+ const port = makePort(index, makePolicy());
501
+ const svc = new LockService(port);
502
+
503
+ const result = await svc.recoverFromState();
504
+
505
+ expect(result.data.active).toEqual(['feature-z']);
506
+ });
507
+ });
508
+ });