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
@@ -0,0 +1,890 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ GitHubIssueTracker,
4
+ LinearIssueTracker,
5
+ JiraIssueTracker,
6
+ createIssueTracker
7
+ } from '../src/application/services/issue-tracker-service.js';
8
+ import type { GhRunner, HttpRunner } from '../src/application/services/issue-tracker-service.js';
9
+
10
+ function makeGhRunner(stdout: string, exitCode = 0): GhRunner {
11
+ return vi.fn(async () => ({ stdout, exitCode }));
12
+ }
13
+
14
+ function makeHttpRunner(
15
+ responses: Array<{ status?: number; ok?: boolean; body?: unknown }>
16
+ ): HttpRunner & ReturnType<typeof vi.fn> {
17
+ const queue = [...responses];
18
+ const runner = vi.fn(async () => {
19
+ const next = queue.shift() ?? { status: 500, ok: false, body: '' };
20
+ const body =
21
+ typeof next.body === 'string'
22
+ ? next.body
23
+ : next.body === undefined
24
+ ? ''
25
+ : JSON.stringify(next.body);
26
+ return {
27
+ status: next.status ?? 200,
28
+ ok: next.ok ?? true,
29
+ body
30
+ };
31
+ });
32
+ return runner as HttpRunner & ReturnType<typeof vi.fn>;
33
+ }
34
+
35
+ describe('G9: Multi-Tracker Support', () => {
36
+ describe('GitHubIssueTracker', () => {
37
+ it('getIssue parses gh CLI output', async () => {
38
+ const payload = JSON.stringify({
39
+ number: 42,
40
+ title: 'Add dark mode',
41
+ body: 'Users want dark mode.',
42
+ state: 'OPEN',
43
+ url: 'https://github.com/org/repo/issues/42'
44
+ });
45
+ const runner = makeGhRunner(payload);
46
+ const tracker = new GitHubIssueTracker({ repo: 'org/repo' }, runner);
47
+
48
+ const issue = await tracker.getIssue('42');
49
+ expect(issue.id).toBe('42');
50
+ expect(issue.title).toBe('Add dark mode');
51
+ expect(issue.status).toBe('open');
52
+ expect(issue.url).toContain('issues/42');
53
+ });
54
+
55
+ it('getIssue returns empty fallback when gh is not available', async () => {
56
+ const runner = makeGhRunner('', 127);
57
+ const tracker = new GitHubIssueTracker({}, runner);
58
+
59
+ const issue = await tracker.getIssue('99');
60
+ expect(issue.id).toBe('99');
61
+ expect(issue.title).toBe('');
62
+ expect(issue.status).toBe('unknown');
63
+ });
64
+
65
+ it('updateIssueStatus and addComment route through gh CLI args', async () => {
66
+ const runner = makeGhRunner('');
67
+ const tracker = new GitHubIssueTracker({ repo: 'org/repo' }, runner);
68
+
69
+ await tracker.updateIssueStatus('42', 'merged');
70
+ await tracker.addComment('42', 'AOP: status -> merged');
71
+
72
+ expect(runner).toHaveBeenCalledWith(expect.arrayContaining(['issue', 'edit', '42', '--state', 'closed']));
73
+ expect(runner).toHaveBeenCalledWith(
74
+ expect.arrayContaining(['issue', 'comment', '42', '--body', 'AOP: status -> merged'])
75
+ );
76
+ });
77
+
78
+ it('GIVEN_no_ghRunner_injected_WHEN_getIssue_called_THEN_returns_fallback_issue', async () => {
79
+ const tracker = new GitHubIssueTracker({});
80
+ const issue = await tracker.getIssue('999');
81
+ expect(issue.id).toBe('999');
82
+ expect(issue.status).toBe('unknown');
83
+ });
84
+ });
85
+
86
+ describe('LinearIssueTracker', () => {
87
+ it('GIVEN_linear_issue_exists_WHEN_getIssue_called_THEN_parses_graphql_response', async () => {
88
+ const httpRunner = makeHttpRunner([
89
+ {
90
+ body: {
91
+ data: {
92
+ issueByIdentifier: {
93
+ id: 'lin_uuid_1',
94
+ identifier: 'LIN-123',
95
+ title: 'Ship feature flag',
96
+ description: 'Implement rollout toggle',
97
+ url: 'https://linear.app/acme/issue/LIN-123',
98
+ state: { name: 'In Progress' }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ]);
104
+ const tracker = new LinearIssueTracker(
105
+ {
106
+ token: 'linear-token',
107
+ base_url: 'https://api.linear.app/graphql'
108
+ },
109
+ httpRunner
110
+ );
111
+
112
+ const issue = await tracker.getIssue('LIN-123');
113
+
114
+ expect(issue).toMatchObject({
115
+ id: 'LIN-123',
116
+ title: 'Ship feature flag',
117
+ body: 'Implement rollout toggle',
118
+ status: 'in progress',
119
+ url: 'https://linear.app/acme/issue/LIN-123'
120
+ });
121
+ expect(httpRunner).toHaveBeenCalledTimes(1);
122
+ expect(httpRunner).toHaveBeenCalledWith(
123
+ 'https://api.linear.app/graphql',
124
+ expect.objectContaining({
125
+ method: 'POST',
126
+ headers: expect.objectContaining({
127
+ Authorization: 'Bearer linear-token'
128
+ })
129
+ })
130
+ );
131
+ });
132
+
133
+ it('GIVEN_state_id_mapping_WHEN_updateIssueStatus_called_THEN_issues_graphql_issueUpdate_mutation', async () => {
134
+ const httpRunner = makeHttpRunner([
135
+ {
136
+ body: {
137
+ data: {
138
+ issueByIdentifier: {
139
+ id: 'lin_uuid_2',
140
+ identifier: 'LIN-200',
141
+ title: 'Refactor queue',
142
+ description: '',
143
+ url: 'https://linear.app/acme/issue/LIN-200',
144
+ state: { name: 'Todo' }
145
+ }
146
+ }
147
+ }
148
+ },
149
+ {
150
+ body: {
151
+ data: {
152
+ issueUpdate: {
153
+ success: true
154
+ }
155
+ }
156
+ }
157
+ }
158
+ ]);
159
+ const tracker = new LinearIssueTracker(
160
+ {
161
+ state_id_building: 'state_in_progress'
162
+ },
163
+ httpRunner
164
+ );
165
+
166
+ await tracker.updateIssueStatus('LIN-200', 'building');
167
+
168
+ expect(httpRunner).toHaveBeenCalledTimes(2);
169
+ const secondCallBody = JSON.parse(String(httpRunner.mock.calls[1]?.[1]?.body));
170
+ expect(secondCallBody.query).toContain('issueUpdate');
171
+ expect(secondCallBody.variables).toMatchObject({
172
+ id: 'lin_uuid_2',
173
+ stateId: 'state_in_progress'
174
+ });
175
+ });
176
+
177
+ it('GIVEN_issue_exists_WHEN_addComment_called_THEN_posts_commentCreate_mutation', async () => {
178
+ const httpRunner = makeHttpRunner([
179
+ {
180
+ body: {
181
+ data: {
182
+ issueByIdentifier: {
183
+ id: 'lin_uuid_3',
184
+ identifier: 'LIN-300',
185
+ title: 'Fix race condition',
186
+ description: '',
187
+ url: 'https://linear.app/acme/issue/LIN-300',
188
+ state: { name: 'Todo' }
189
+ }
190
+ }
191
+ }
192
+ },
193
+ {
194
+ body: {
195
+ data: {
196
+ commentCreate: {
197
+ success: true
198
+ }
199
+ }
200
+ }
201
+ }
202
+ ]);
203
+ const tracker = new LinearIssueTracker({}, httpRunner);
204
+
205
+ await tracker.addComment('LIN-300', 'AOP: feature status changed to `qa`');
206
+
207
+ expect(httpRunner).toHaveBeenCalledTimes(2);
208
+ const secondCallBody = JSON.parse(String(httpRunner.mock.calls[1]?.[1]?.body));
209
+ expect(secondCallBody.query).toContain('commentCreate');
210
+ expect(secondCallBody.variables).toMatchObject({
211
+ issueId: 'lin_uuid_3',
212
+ body: 'AOP: feature status changed to `qa`'
213
+ });
214
+ });
215
+
216
+ it('GIVEN_linear_api_failure_WHEN_getIssue_called_THEN_returns_unknown_fallback', async () => {
217
+ const httpRunner = makeHttpRunner([{ ok: false, status: 500, body: '' }]);
218
+ const tracker = new LinearIssueTracker({}, httpRunner);
219
+
220
+ const issue = await tracker.getIssue('LIN-500');
221
+ expect(issue).toEqual({
222
+ id: 'LIN-500',
223
+ title: '',
224
+ body: '',
225
+ status: 'unknown',
226
+ url: ''
227
+ });
228
+ });
229
+ });
230
+
231
+ describe('JiraIssueTracker', () => {
232
+ it('GIVEN_jira_issue_exists_WHEN_getIssue_called_THEN_parses_rest_fields', async () => {
233
+ const httpRunner = makeHttpRunner([
234
+ {
235
+ body: {
236
+ key: 'PROJ-42',
237
+ fields: {
238
+ summary: 'Fix flaky pipeline',
239
+ description: 'Investigate and stabilize',
240
+ status: { name: 'In Progress' }
241
+ }
242
+ }
243
+ }
244
+ ]);
245
+ const tracker = new JiraIssueTracker(
246
+ {
247
+ base_url: 'https://jira.example.com',
248
+ email: 'dev@example.com',
249
+ token: 'jira-token'
250
+ },
251
+ httpRunner
252
+ );
253
+
254
+ const issue = await tracker.getIssue('PROJ-42');
255
+ expect(issue).toMatchObject({
256
+ id: 'PROJ-42',
257
+ title: 'Fix flaky pipeline',
258
+ body: 'Investigate and stabilize',
259
+ status: 'in progress',
260
+ url: 'https://jira.example.com/browse/PROJ-42'
261
+ });
262
+ expect(httpRunner).toHaveBeenCalledWith(
263
+ 'https://jira.example.com/rest/api/2/issue/PROJ-42?fields=summary,description,status',
264
+ expect.objectContaining({
265
+ method: 'GET',
266
+ headers: expect.objectContaining({
267
+ Authorization: expect.stringContaining('Basic ')
268
+ })
269
+ })
270
+ );
271
+ });
272
+
273
+ it('GIVEN_matching_transition_WHEN_updateIssueStatus_called_THEN_posts_transition_update', async () => {
274
+ const httpRunner = makeHttpRunner([
275
+ {
276
+ body: {
277
+ transitions: [
278
+ { id: '11', name: 'To Do' },
279
+ { id: '22', name: 'In Progress' },
280
+ { id: '33', name: 'Done' }
281
+ ]
282
+ }
283
+ },
284
+ {
285
+ body: {}
286
+ }
287
+ ]);
288
+ const tracker = new JiraIssueTracker(
289
+ {
290
+ base_url: 'https://jira.example.com',
291
+ transition_building: 'In Progress'
292
+ },
293
+ httpRunner
294
+ );
295
+
296
+ await tracker.updateIssueStatus('PROJ-7', 'building');
297
+
298
+ expect(httpRunner).toHaveBeenCalledTimes(2);
299
+ expect(httpRunner).toHaveBeenNthCalledWith(
300
+ 2,
301
+ 'https://jira.example.com/rest/api/2/issue/PROJ-7/transitions',
302
+ expect.objectContaining({
303
+ method: 'POST',
304
+ body: JSON.stringify({ transition: { id: '22' } })
305
+ })
306
+ );
307
+ });
308
+
309
+ it('GIVEN_comment_WHEN_addComment_called_THEN_posts_comment_payload', async () => {
310
+ const httpRunner = makeHttpRunner([{ body: {} }]);
311
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com' }, httpRunner);
312
+
313
+ await tracker.addComment('PROJ-8', 'AOP: feature status changed to `merged`');
314
+
315
+ expect(httpRunner).toHaveBeenCalledWith(
316
+ 'https://jira.example.com/rest/api/2/issue/PROJ-8/comment',
317
+ expect.objectContaining({
318
+ method: 'POST',
319
+ body: JSON.stringify({ body: 'AOP: feature status changed to `merged`' })
320
+ })
321
+ );
322
+ });
323
+ });
324
+
325
+ describe('createIssueTracker factory', () => {
326
+ it('returns tracker instances for known types and honors enabled=false', () => {
327
+ const github = createIssueTracker({ type: 'github', config: { repo: 'o/r' } });
328
+ const linear = createIssueTracker({ type: 'linear' });
329
+ const jira = createIssueTracker({ type: 'jira' });
330
+ const disabled = createIssueTracker({ type: 'github', enabled: false });
331
+
332
+ expect(github).toBeInstanceOf(GitHubIssueTracker);
333
+ expect(linear).toBeInstanceOf(LinearIssueTracker);
334
+ expect(jira).toBeInstanceOf(JiraIssueTracker);
335
+ expect(disabled).toBeUndefined();
336
+ });
337
+
338
+ it('returns undefined for unknown type and undefined config', () => {
339
+ expect(createIssueTracker({ type: 'unknown-tracker' })).toBeUndefined();
340
+ expect(createIssueTracker(undefined)).toBeUndefined();
341
+ });
342
+ });
343
+ });
344
+
345
+ describe('issue-tracker-service branch coverage', () => {
346
+ describe('createGhRunner', () => {
347
+ it('GIVEN_function_provided_WHEN_createGhRunner_called_THEN_returns_that_function', async () => {
348
+ const { createGhRunner } = await import('../src/application/services/issue-tracker-service.js');
349
+ const custom = vi.fn(async () => ({ stdout: 'ok', exitCode: 0 }));
350
+ const runner = createGhRunner(custom as GhRunner);
351
+ expect(runner).toBe(custom);
352
+ });
353
+ });
354
+
355
+ describe('GitHubIssueTracker', () => {
356
+ it('GIVEN_gh_not_found_WHEN_getIssue_called_THEN_returns_fallback', async () => {
357
+ const runner: GhRunner = async () => ({ stdout: '', exitCode: 127 });
358
+ const tracker = new GitHubIssueTracker({}, runner);
359
+ const issue = await tracker.getIssue('123');
360
+ expect(issue.id).toBe('123');
361
+ expect(issue.url).toBe('');
362
+ });
363
+
364
+ it('GIVEN_invalid_json_WHEN_getIssue_called_THEN_returns_fallback', async () => {
365
+ const runner: GhRunner = async () => ({ stdout: '', exitCode: 0 });
366
+ const tracker = new GitHubIssueTracker({}, runner);
367
+ const issue = await tracker.getIssue('456');
368
+ expect(issue.id).toBe('456');
369
+ });
370
+ });
371
+
372
+ describe('LinearIssueTracker', () => {
373
+ it('GIVEN_status_blocked_WHEN_updateIssueStatus_called_THEN_maps_to_blocked', async () => {
374
+ const calls: Array<[string, string, string]> = [];
375
+ const runner: HttpRunner = async (_url, init) => {
376
+ const body = JSON.parse(init.body as string) as Record<string, unknown>;
377
+ const vars = body['variables'] as Record<string, unknown> | undefined;
378
+ calls.push([body['query'] as string, vars?.['issueId'] as string, vars?.['stateId'] as string]);
379
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
380
+ };
381
+ const tracker = new LinearIssueTracker({ token: 'tok', state_blocked: 'state_b' }, runner);
382
+ await tracker.updateIssueStatus('LIN-1', 'blocked');
383
+ expect(calls.length).toBeGreaterThan(0);
384
+ });
385
+
386
+ it('GIVEN_http_failure_WHEN_getIssue_called_THEN_returns_fallback_unknown', async () => {
387
+ const runner: HttpRunner = async () => { throw new Error('network error'); };
388
+ const tracker = new LinearIssueTracker({ token: 'tok' }, runner as never);
389
+ const issue = await tracker.getIssue('LIN-999');
390
+ expect(issue.id).toBe('LIN-999');
391
+ });
392
+ });
393
+
394
+ describe('JiraIssueTracker', () => {
395
+ it('GIVEN_status_planning_WHEN_updateIssueStatus_called_THEN_maps_to_to_do_transition', async () => {
396
+ const calls: Array<unknown> = [];
397
+ const runner: HttpRunner = async (_url, init) => {
398
+ calls.push(JSON.parse(init.body as string));
399
+ return { status: 204, ok: true, body: '' };
400
+ };
401
+ // Need to stub getTransitions first (GET /transitions returns match)
402
+ let callCount = 0;
403
+ const mockRunner: HttpRunner = async (url, init) => {
404
+ callCount += 1;
405
+ if (url.includes('transitions') && (!init.method || init.method === 'GET')) {
406
+ return {
407
+ status: 200, ok: true, body: JSON.stringify({
408
+ transitions: [{ id: 't1', name: 'To Do' }]
409
+ })
410
+ };
411
+ }
412
+ return runner(url, init);
413
+ };
414
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok', email: 'me@example.com' }, mockRunner);
415
+ await tracker.updateIssueStatus('PROJ-1', 'planning');
416
+ expect(callCount).toBeGreaterThan(0);
417
+ });
418
+ });
419
+ });
420
+
421
+ describe('JiraIssueTracker additional branch coverage', () => {
422
+ it('GIVEN_requestJson_returns_null_WHEN_getIssue_called_THEN_returns_fallback', async () => {
423
+ // Return non-ok response → requestJson returns null
424
+ const runner: HttpRunner = async () => ({ status: 404, ok: false, body: '' });
425
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
426
+ const issue = await tracker.getIssue('PROJ-999');
427
+ expect(issue.id).toBe('PROJ-999');
428
+ expect(issue.status).toBe('unknown');
429
+ });
430
+
431
+ it('GIVEN_no_matching_transition_WHEN_updateIssueStatus_called_THEN_returns_without_posting', async () => {
432
+ const calls: string[] = [];
433
+ const runner: HttpRunner = async (url, init) => {
434
+ calls.push(`${String(init.method ?? 'GET')} ${url}`);
435
+ // Only GET for transitions, no matching transition name
436
+ return {
437
+ status: 200, ok: true, body: JSON.stringify({ transitions: [{ id: 'x', name: 'To Do' }] })
438
+ };
439
+ };
440
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
441
+ // 'building' maps to 'in progress' but transitions only have 'To Do'
442
+ await tracker.updateIssueStatus('PROJ-1', 'building');
443
+ // Only GET should have been called (no POST)
444
+ expect(calls.every((c) => c.startsWith('GET'))).toBe(true);
445
+ });
446
+
447
+ it('GIVEN_no_base_url_WHEN_requestJson_called_THEN_returns_null_and_getIssue_returns_fallback', async () => {
448
+ const runner: HttpRunner = async () => ({ status: 200, ok: true, body: '{}' });
449
+ const tracker = new JiraIssueTracker({}, runner); // no base_url
450
+ const issue = await tracker.getIssue('PROJ-0');
451
+ expect(issue.status).toBe('unknown');
452
+ });
453
+
454
+ it('GIVEN_token_but_no_email_WHEN_authHeader_called_via_getIssue_THEN_uses_bearer_token', async () => {
455
+ let capturedAuth: string | undefined;
456
+ const runner: HttpRunner = async (_, init) => {
457
+ capturedAuth = (init.headers as Record<string, string>)?.['Authorization'];
458
+ return { status: 200, ok: true, body: JSON.stringify({ key: 'PROJ-1', fields: { summary: 'Test', status: { name: 'Open' } } }) };
459
+ };
460
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'mytoken' }, runner);
461
+ await tracker.getIssue('PROJ-1');
462
+ expect(capturedAuth).toBe('Bearer mytoken');
463
+ });
464
+ });
465
+
466
+ describe('issue-tracker-service additional branch coverage', () => {
467
+ it('GIVEN_jira_object_description_WHEN_toJiraDescription_called_THEN_returns_json_stringified', async () => {
468
+ // toJiraDescription is called internally when updating an issue that has an object description field
469
+ const runner: HttpRunner = async () => ({
470
+ status: 200,
471
+ ok: true,
472
+ body: JSON.stringify({
473
+ key: 'PROJ-1',
474
+ fields: { summary: 'Test', description: { type: 'doc', content: [] }, status: { name: 'Open' } }
475
+ })
476
+ });
477
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
478
+ // getIssue passes the description through toJiraDescription when calling addComment
479
+ const issue = await tracker.getIssue('PROJ-1');
480
+ // description field (object) should be stringified
481
+ expect(typeof issue.title).toBe('string');
482
+ });
483
+
484
+ it('GIVEN_jira_config_transition_override_WHEN_updateIssueStatus_THEN_uses_explicit_transition', async () => {
485
+ const calls: string[] = [];
486
+ const runner: HttpRunner = async (url, init) => {
487
+ calls.push(`${init.method} ${url}`);
488
+ if (init.method === 'GET') {
489
+ return {
490
+ status: 200,
491
+ ok: true,
492
+ body: JSON.stringify({ transitions: [{ id: '99', name: 'Custom Done' }] })
493
+ };
494
+ }
495
+ return { status: 204, ok: true, body: '' };
496
+ };
497
+ // config has explicit transition for 'merged' → 'custom done'
498
+ const tracker = new JiraIssueTracker(
499
+ { base_url: 'https://jira.example.com', token: 'tok', transition_merged: 'custom done' },
500
+ runner
501
+ );
502
+ await tracker.updateIssueStatus('PROJ-2', 'merged');
503
+ // POST should have been called since 'custom done' matches 'Custom Done' (case-insensitive)
504
+ expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
505
+ });
506
+
507
+ it('GIVEN_linear_planning_status_WHEN_updateIssueStatus_THEN_maps_to_backlog', async () => {
508
+ const bodies: string[] = [];
509
+ const runner: HttpRunner = async (_, init) => {
510
+ bodies.push(init.body ?? '');
511
+ // First call: resolveIssueNode → return a valid issue
512
+ if (bodies.length === 1) {
513
+ return {
514
+ status: 200, ok: true,
515
+ body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-1', identifier: 'LIN-1', title: 'T', description: '', url: '', state: { name: 'Backlog' } } } })
516
+ };
517
+ }
518
+ // Second call: mutation
519
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
520
+ };
521
+ // Provide state_id_backlog so resolveStateId returns non-null → mutation is called
522
+ const tracker = new LinearIssueTracker({ token: 'tok', state_id_backlog: 'state-backlog-id' }, runner);
523
+ await tracker.updateIssueStatus('LIN-1', 'planning');
524
+ // mapDefaultLinearStatus('planning') → 'backlog' → state_id_backlog → mutation called
525
+ expect(bodies.length).toBe(2);
526
+ expect(bodies[1]).toContain('issueUpdate');
527
+ });
528
+
529
+ it('GIVEN_linear_qa_status_WHEN_updateIssueStatus_THEN_maps_to_in_review', async () => {
530
+ const bodies: string[] = [];
531
+ const runner: HttpRunner = async (_, init) => {
532
+ bodies.push(init.body ?? '');
533
+ if (bodies.length === 1) {
534
+ return {
535
+ status: 200, ok: true,
536
+ body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-2', identifier: 'LIN-2', title: 'T', description: '', url: '', state: { name: 'In Review' } } } })
537
+ };
538
+ }
539
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
540
+ };
541
+ // Provide state_id_in_review so resolveStateId returns non-null → mutation is called
542
+ const tracker = new LinearIssueTracker({ token: 'tok', state_id_in_review: 'state-inreview-id' }, runner);
543
+ await tracker.updateIssueStatus('LIN-2', 'qa');
544
+ // mapDefaultLinearStatus('qa') → 'in_review' → state_id_in_review → mutation called
545
+ expect(bodies.length).toBe(2);
546
+ expect(bodies[1]).toContain('issueUpdate');
547
+ });
548
+
549
+ it('GIVEN_jira_building_status_WHEN_updateIssueStatus_THEN_maps_to_in_progress', async () => {
550
+ const calls: string[] = [];
551
+ const runner: HttpRunner = async (url, init) => {
552
+ calls.push(`${init.method} ${url}`);
553
+ if (init.method === 'GET') {
554
+ return {
555
+ status: 200,
556
+ ok: true,
557
+ body: JSON.stringify({ transitions: [{ id: '5', name: 'In Progress' }] })
558
+ };
559
+ }
560
+ return { status: 204, ok: true, body: '' };
561
+ };
562
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
563
+ await tracker.updateIssueStatus('PROJ-3', 'building');
564
+ // mapDefaultJiraTransition('building') → 'in progress' → matches 'In Progress'
565
+ expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
566
+ });
567
+
568
+ it('GIVEN_jira_planning_status_WHEN_updateIssueStatus_THEN_maps_to_to_do', async () => {
569
+ const calls: string[] = [];
570
+ const runner: HttpRunner = async (url, init) => {
571
+ calls.push(`${init.method} ${url}`);
572
+ if (init.method === 'GET') {
573
+ return {
574
+ status: 200,
575
+ ok: true,
576
+ body: JSON.stringify({ transitions: [{ id: '1', name: 'To Do' }] })
577
+ };
578
+ }
579
+ return { status: 204, ok: true, body: '' };
580
+ };
581
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
582
+ await tracker.updateIssueStatus('PROJ-4', 'planning');
583
+ // mapDefaultJiraTransition('planning') → 'to do' → matches 'To Do'
584
+ expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
585
+ });
586
+
587
+ it('GIVEN_jira_blocked_status_WHEN_updateIssueStatus_THEN_maps_to_blocked', async () => {
588
+ const calls: string[] = [];
589
+ const runner: HttpRunner = async (url, init) => {
590
+ calls.push(`${init.method} ${url}`);
591
+ if (init.method === 'GET') {
592
+ return {
593
+ status: 200,
594
+ ok: true,
595
+ body: JSON.stringify({ transitions: [{ id: '7', name: 'Blocked' }] })
596
+ };
597
+ }
598
+ return { status: 204, ok: true, body: '' };
599
+ };
600
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
601
+ await tracker.updateIssueStatus('PROJ-5', 'blocked');
602
+ expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
603
+ });
604
+ });
605
+
606
+ describe('Issue tracker additional branch coverage', () => {
607
+ it('GIVEN_createHttpRunner_called_without_fn_WHEN_fetch_throws_THEN_returns_zero_status', async () => {
608
+ const { createHttpRunner } = await import('../src/application/services/issue-tracker-service.js');
609
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network error'));
610
+ const runner = createHttpRunner();
611
+ const result = await runner('https://example.com', { method: 'GET' });
612
+ expect(result.status).toBe(0);
613
+ expect(result.ok).toBe(false);
614
+ expect(result.body).toBe('');
615
+ fetchSpy.mockRestore();
616
+ });
617
+
618
+ it('GIVEN_linear_graphql_response_with_errors_array_WHEN_getIssue_THEN_returns_fallback', async () => {
619
+ const runner: HttpRunner = async () => ({
620
+ status: 200,
621
+ ok: true,
622
+ body: JSON.stringify({ data: {}, errors: [{ message: 'GraphQL error' }] })
623
+ });
624
+ const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
625
+ const issue = await tracker.getIssue('LIN-99');
626
+ expect(issue.id).toBe('LIN-99');
627
+ expect(issue.title).toBe('');
628
+ expect(issue.status).toBe('unknown');
629
+ });
630
+
631
+ it('GIVEN_linear_response_with_non_object_parsed_WHEN_getIssue_THEN_returns_fallback', async () => {
632
+ const runner: HttpRunner = async () => ({
633
+ status: 200,
634
+ ok: true,
635
+ body: JSON.stringify({ data: null })
636
+ });
637
+ const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
638
+ const issue = await tracker.getIssue('LIN-100');
639
+ expect(issue.status).toBe('unknown');
640
+ });
641
+
642
+ it('GIVEN_linear_with_unknown_status_WHEN_updateIssueStatus_THEN_uses_in_progress_default', async () => {
643
+ const bodies: string[] = [];
644
+ const runner: HttpRunner = async (_, init) => {
645
+ bodies.push(init.body ?? '');
646
+ if (bodies.length === 1) {
647
+ return {
648
+ status: 200, ok: true,
649
+ body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-3', identifier: 'LIN-3', title: 'T', description: '', url: '', state: null } } })
650
+ };
651
+ }
652
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
653
+ };
654
+ // Provide state_id_in_progress to cover the 'in_progress' default mapping for unknown status
655
+ const tracker = new LinearIssueTracker({ token: 'tok', state_id_in_progress: 'state-inprogress-id' }, runner);
656
+ await tracker.updateIssueStatus('LIN-3', 'some_random_unknown_status');
657
+ // mapDefaultLinearStatus returns 'in_progress' → state_id_in_progress → mutation called
658
+ expect(bodies.length).toBe(2);
659
+ });
660
+
661
+ it('GIVEN_linear_resolveStateId_no_explicit_no_default_WHEN_updateIssueStatus_THEN_no_mutation', async () => {
662
+ let callCount = 0;
663
+ const runner: HttpRunner = async () => {
664
+ callCount += 1;
665
+ if (callCount === 1) {
666
+ return {
667
+ status: 200, ok: true,
668
+ body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-4', identifier: 'LIN-4', title: 'T', description: '', url: '', state: null } } })
669
+ };
670
+ }
671
+ return { status: 200, ok: true, body: JSON.stringify({ data: {} }) };
672
+ };
673
+ // No state_id_* keys → resolveStateId returns null → no mutation
674
+ const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
675
+ await tracker.updateIssueStatus('LIN-4', 'some_status');
676
+ // Only resolveIssueNode call (2 queries: by identifier + by id), no mutation
677
+ expect(callCount).toBeLessThanOrEqual(3);
678
+ });
679
+
680
+ it('GIVEN_jira_token_only_no_email_WHEN_requestJson_called_THEN_uses_bearer_auth', async () => {
681
+ const calls: { auth?: string }[] = [];
682
+ const runner: HttpRunner = async (_, init) => {
683
+ calls.push({ auth: (init.headers as Record<string, string>)['Authorization'] });
684
+ return { status: 200, ok: true, body: JSON.stringify({ key: 'PROJ-1', fields: { summary: 'Test', description: 'Body', status: { name: 'Open' } } }) };
685
+ };
686
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'my-token' }, runner);
687
+ const issue = await tracker.getIssue('PROJ-1');
688
+ expect(issue.title).toBe('Test');
689
+ expect(calls[0].auth).toMatch(/^Bearer /);
690
+ });
691
+
692
+ it('GIVEN_jira_no_base_url_WHEN_getIssue_called_THEN_returns_fallback', async () => {
693
+ const runner: HttpRunner = vi.fn(async () => ({ status: 200, ok: true, body: '' }));
694
+ const tracker = new JiraIssueTracker({}, runner);
695
+ const issue = await tracker.getIssue('PROJ-1');
696
+ expect(issue.title).toBe('');
697
+ expect(issue.status).toBe('unknown');
698
+ });
699
+
700
+ it('GIVEN_jira_no_matching_transition_WHEN_updateIssueStatus_THEN_no_post_made', async () => {
701
+ const calls: string[] = [];
702
+ const runner: HttpRunner = async (_url, init) => {
703
+ calls.push(init.method ?? 'GET');
704
+ return { status: 200, ok: true, body: JSON.stringify({ transitions: [{ id: '1', name: 'Open' }] }) };
705
+ };
706
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
707
+ await tracker.updateIssueStatus('PROJ-1', 'merged'); // mapDefaultJiraTransition → 'done', no matching transition
708
+ expect(calls.filter((m) => m === 'POST')).toHaveLength(0);
709
+ });
710
+
711
+ it('GIVEN_jira_no_auth_WHEN_requestJson_called_THEN_no_authorization_header', async () => {
712
+ const calls: { hasAuth: boolean }[] = [];
713
+ const runner: HttpRunner = async (_, init) => {
714
+ calls.push({ hasAuth: 'Authorization' in (init.headers as Record<string, string>) });
715
+ return { status: 200, ok: true, body: JSON.stringify({ key: 'PROJ-1', fields: { summary: '', description: null, status: { name: 'Open' } } }) };
716
+ };
717
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com' }, runner);
718
+ await tracker.getIssue('PROJ-1');
719
+ expect(calls[0].hasAuth).toBe(false);
720
+ });
721
+
722
+ it('GIVEN_linear_http_response_not_ok_WHEN_graphQl_called_THEN_returns_null_and_fallback', async () => {
723
+ const runner: HttpRunner = async () => ({ status: 500, ok: false, body: '' });
724
+ const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
725
+ const issue = await tracker.getIssue('LIN-500');
726
+ expect(issue.status).toBe('unknown');
727
+ });
728
+
729
+ it('GIVEN_jira_description_is_object_WHEN_getIssue_called_THEN_returns_json_string', async () => {
730
+ const runner: HttpRunner = async () => ({
731
+ status: 200, ok: true,
732
+ body: JSON.stringify({
733
+ key: 'PROJ-2',
734
+ fields: {
735
+ summary: 'Test',
736
+ description: { type: 'doc', content: [] },
737
+ status: { name: 'In Progress' }
738
+ }
739
+ })
740
+ });
741
+ const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
742
+ const issue = await tracker.getIssue('PROJ-2');
743
+ expect(issue.body).toContain('doc');
744
+ });
745
+ });
746
+
747
+ describe('LinearIssueTracker addComment and updateIssueStatus guard branches', () => {
748
+ it('GIVEN_issue_not_found_WHEN_addComment_called_THEN_returns_without_posting', async () => {
749
+ // resolveIssueNode returns null → !issue?.id guard returns early (lines 314-315)
750
+ const runner: HttpRunner = async () => ({
751
+ status: 200,
752
+ ok: true,
753
+ body: JSON.stringify({ data: { issueByIdentifier: null } })
754
+ });
755
+ // Should not throw and should not call graphql mutation
756
+ let callCount = 0;
757
+ const countingRunner: HttpRunner = async (url, init) => {
758
+ callCount++;
759
+ return runner(url, init);
760
+ };
761
+ const tracker2 = new LinearIssueTracker({}, countingRunner);
762
+ await tracker2.addComment('LIN-999', 'some comment');
763
+ // Only the resolveIssueNode queries are called, no mutation
764
+ expect(callCount).toBeLessThanOrEqual(2);
765
+ });
766
+
767
+ it('GIVEN_resolved_issue_with_no_id_field_WHEN_addComment_called_THEN_returns_early', async () => {
768
+ // resolveIssueNode returns issue node but without id → !issue?.id guard
769
+ const runner: HttpRunner = async () => ({
770
+ status: 200,
771
+ ok: true,
772
+ body: JSON.stringify({ data: { issueByIdentifier: { identifier: 'LIN-1', title: 'T', description: '', url: '' } } })
773
+ });
774
+ const tracker = new LinearIssueTracker({}, runner);
775
+ await expect(tracker.addComment('LIN-1', 'comment')).resolves.toBeUndefined();
776
+ });
777
+
778
+ it('GIVEN_issue_not_found_WHEN_updateIssueStatus_called_THEN_returns_without_mutation', async () => {
779
+ const runner: HttpRunner = async () => ({
780
+ status: 200,
781
+ ok: true,
782
+ body: JSON.stringify({ data: { issueByIdentifier: null } })
783
+ });
784
+ const tracker = new LinearIssueTracker({ token: 'tok', state_id_building: 'state-id' }, runner);
785
+ await expect(tracker.updateIssueStatus('LIN-999', 'building')).resolves.toBeUndefined();
786
+ });
787
+ });
788
+
789
+ describe('LinearIssueTracker graphQl invalid body branch', () => {
790
+ it('GIVEN_response_body_is_invalid_json_WHEN_graphQl_called_THEN_returns_null_fallback', async () => {
791
+ // Returns ok:true but body is not valid JSON → tryParseJson returns null → lines 218-219 covered
792
+ const runner: HttpRunner = async () => ({
793
+ status: 200,
794
+ ok: true,
795
+ body: 'not valid json {{ response'
796
+ });
797
+ const tracker = new LinearIssueTracker({}, runner);
798
+ const issue = await tracker.getIssue('LIN-invalid-json');
799
+ expect(issue.status).toBe('unknown');
800
+ expect(issue.id).toBe('LIN-invalid-json');
801
+ });
802
+
803
+ it('GIVEN_response_body_is_empty_string_WHEN_graphQl_called_THEN_returns_null_fallback', async () => {
804
+ // tryParseJson returns null for empty string → lines 218-219 covered
805
+ const runner: HttpRunner = async () => ({
806
+ status: 200,
807
+ ok: true,
808
+ body: ''
809
+ });
810
+ const tracker = new LinearIssueTracker({}, runner);
811
+ const issue = await tracker.getIssue('LIN-empty-body');
812
+ expect(issue.status).toBe('unknown');
813
+ });
814
+ });
815
+
816
+ describe('LinearIssueTracker mapDefaultLinearStatus building branch', () => {
817
+ it('GIVEN_building_status_no_explicit_id_but_has_in_progress_WHEN_updateIssueStatus_THEN_uses_mapped_state', async () => {
818
+ // status='building' → normalizeAopStatus='building' → explicit (state_id_building) not in config
819
+ // → mapDefaultLinearStatus('building') → returns 'in_progress' → lines 168-169 covered
820
+ let callCount = 0;
821
+ const runner: HttpRunner = async () => {
822
+ callCount++;
823
+ if (callCount === 1) {
824
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-build', identifier: 'LIN-B', title: 'T', description: '', url: '', state: null } } }) };
825
+ }
826
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
827
+ };
828
+ // No state_id_building but has state_id_in_progress → mapDefaultLinearStatus('building') → 'in_progress'
829
+ const tracker = new LinearIssueTracker({ token: 'tok', state_id_in_progress: 'state-ip-id' }, runner);
830
+ await tracker.updateIssueStatus('LIN-B', 'building');
831
+ // Mutation should be called (callCount > 2 if resolveIssueNode needs 2 calls)
832
+ expect(callCount).toBeGreaterThan(1);
833
+ });
834
+ });
835
+
836
+ describe('LinearIssueTracker mapDefaultLinearStatus OR branches and lowerCaseStatus', () => {
837
+ it('GIVEN_status_closed_WHEN_updateIssueStatus_THEN_uses_done_default_mapping', async () => {
838
+ // 'closed' → normalized='closed' → line 155: merged||closed - 'merged' is FALSE, 'closed' is TRUE (Branch 0 covered)
839
+ let callCount = 0;
840
+ const runner: HttpRunner = async () => {
841
+ callCount++;
842
+ if (callCount === 1) {
843
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-closed', identifier: 'LIN-C', title: 'T', description: '', url: '', state: null } } }) };
844
+ }
845
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
846
+ };
847
+ const tracker = new LinearIssueTracker({ token: 'tok', state_id_done: 'state-done-id' }, runner);
848
+ await tracker.updateIssueStatus('LIN-C', 'closed');
849
+ expect(callCount).toBeGreaterThan(1);
850
+ });
851
+
852
+ it('GIVEN_status_failed_WHEN_updateIssueStatus_THEN_uses_blocked_default_mapping', async () => {
853
+ // 'failed' → normalized='failed' → line 158: blocked||failed - 'blocked' is FALSE, 'failed' is TRUE (Branch 0 covered)
854
+ let callCount = 0;
855
+ const runner: HttpRunner = async () => {
856
+ callCount++;
857
+ if (callCount === 1) {
858
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-failed', identifier: 'LIN-F', title: 'T', description: '', url: '', state: null } } }) };
859
+ }
860
+ return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
861
+ };
862
+ const tracker = new LinearIssueTracker({ token: 'tok', state_id_blocked: 'state-blocked-id' }, runner);
863
+ await tracker.updateIssueStatus('LIN-F', 'failed');
864
+ expect(callCount).toBeGreaterThan(1);
865
+ });
866
+
867
+ it('GIVEN_issue_with_null_state_WHEN_getIssue_THEN_status_is_unknown', async () => {
868
+ // null state → state?.name = undefined → ?? '' → lowerCaseStatus('') → '' is falsy → 'unknown' (line 146 FALSE branch + line 291)
869
+ const runner: HttpRunner = async () => ({
870
+ status: 200,
871
+ ok: true,
872
+ body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-nostate', identifier: 'LIN-NS', title: 'No State', description: 'desc', url: 'https://x', state: null } } })
873
+ });
874
+ const tracker = new LinearIssueTracker({}, runner);
875
+ const issue = await tracker.getIssue('LIN-NS');
876
+ expect(issue.status).toBe('unknown');
877
+ });
878
+
879
+ it('GIVEN_issue_without_identifier_WHEN_getIssue_THEN_falls_back_to_id_or_issueId', async () => {
880
+ // issue.identifier is falsy → falls back to issue.id (line 288 Block 100 Branch 0 covered)
881
+ const runner: HttpRunner = async () => ({
882
+ status: 200,
883
+ ok: true,
884
+ body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-raw-id', identifier: '', title: 'T', description: 'desc', url: 'https://x', state: { name: 'In Progress' } } } })
885
+ });
886
+ const tracker = new LinearIssueTracker({}, runner);
887
+ const issue = await tracker.getIssue('LIN-NOID');
888
+ expect(issue.id).toBe('lin-raw-id');
889
+ });
890
+ });