orbital-command 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/bin/orbital.js +676 -53
  2. package/dist/assets/PrimitivesConfig-CrmQXYh4.js +32 -0
  3. package/dist/assets/QualityGates-BbasOsF3.js +21 -0
  4. package/dist/assets/SessionTimeline-CGeJsVvy.js +1 -0
  5. package/dist/assets/Settings-oiM496mc.js +12 -0
  6. package/dist/assets/SourceControl-B1fP2nJL.js +41 -0
  7. package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +74 -0
  8. package/dist/assets/arrow-down-CPy85_J6.js +6 -0
  9. package/dist/assets/charts-DbDg0Psc.js +68 -0
  10. package/dist/assets/circle-x-Cwz6ZQDV.js +6 -0
  11. package/dist/assets/file-text-C46Xr65c.js +6 -0
  12. package/dist/assets/formatDistanceToNow-BMqsSP44.js +1 -0
  13. package/dist/assets/globe-Cn2yNZUD.js +6 -0
  14. package/dist/assets/index-Aj4sV8Al.css +1 -0
  15. package/dist/assets/index-Bc9dK3MW.js +354 -0
  16. package/dist/assets/key-OPaNTWJ5.js +6 -0
  17. package/dist/assets/minus-GMsbpKym.js +6 -0
  18. package/dist/assets/shield-DwAFkDYI.js +6 -0
  19. package/dist/assets/ui-BmsSg9jU.js +53 -0
  20. package/dist/assets/useWorkflowEditor-BJkTX_NR.js +16 -0
  21. package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
  22. package/dist/assets/zap-DfbUoOty.js +11 -0
  23. package/dist/favicon.svg +1 -0
  24. package/dist/index.html +6 -5
  25. package/dist/server/server/__tests__/data-routes.test.js +124 -0
  26. package/dist/server/server/__tests__/helpers/db.js +17 -0
  27. package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
  28. package/dist/server/server/__tests__/scope-routes.test.js +137 -0
  29. package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
  30. package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
  31. package/dist/server/server/config-migrator.js +138 -0
  32. package/dist/server/server/config.js +17 -2
  33. package/dist/server/server/database.js +27 -12
  34. package/dist/server/server/global-config.js +143 -0
  35. package/dist/server/server/index.js +882 -252
  36. package/dist/server/server/init.js +579 -194
  37. package/dist/server/server/launch.js +29 -0
  38. package/dist/server/server/manifest-types.js +8 -0
  39. package/dist/server/server/manifest.js +454 -0
  40. package/dist/server/server/migrate-legacy.js +229 -0
  41. package/dist/server/server/parsers/event-parser.test.js +117 -0
  42. package/dist/server/server/parsers/scope-parser.js +74 -28
  43. package/dist/server/server/parsers/scope-parser.test.js +230 -0
  44. package/dist/server/server/project-context.js +255 -0
  45. package/dist/server/server/project-emitter.js +41 -0
  46. package/dist/server/server/project-manager.js +297 -0
  47. package/dist/server/server/routes/config-routes.js +1 -3
  48. package/dist/server/server/routes/data-routes.js +22 -110
  49. package/dist/server/server/routes/dispatch-routes.js +15 -9
  50. package/dist/server/server/routes/git-routes.js +74 -0
  51. package/dist/server/server/routes/manifest-routes.js +319 -0
  52. package/dist/server/server/routes/scope-routes.js +37 -23
  53. package/dist/server/server/routes/sync-routes.js +134 -0
  54. package/dist/server/server/routes/version-routes.js +1 -15
  55. package/dist/server/server/routes/workflow-routes.js +9 -3
  56. package/dist/server/server/schema.js +2 -0
  57. package/dist/server/server/services/batch-orchestrator.js +26 -16
  58. package/dist/server/server/services/claude-session-service.js +17 -14
  59. package/dist/server/server/services/deploy-service.test.js +119 -0
  60. package/dist/server/server/services/event-service.js +64 -1
  61. package/dist/server/server/services/event-service.test.js +191 -0
  62. package/dist/server/server/services/gate-service.test.js +105 -0
  63. package/dist/server/server/services/git-service.js +108 -4
  64. package/dist/server/server/services/github-service.js +110 -2
  65. package/dist/server/server/services/readiness-service.test.js +190 -0
  66. package/dist/server/server/services/scope-cache.js +5 -1
  67. package/dist/server/server/services/scope-cache.test.js +142 -0
  68. package/dist/server/server/services/scope-service.js +217 -126
  69. package/dist/server/server/services/scope-service.test.js +137 -0
  70. package/dist/server/server/services/sprint-orchestrator.js +7 -6
  71. package/dist/server/server/services/sprint-service.js +21 -1
  72. package/dist/server/server/services/sprint-service.test.js +238 -0
  73. package/dist/server/server/services/sync-service.js +434 -0
  74. package/dist/server/server/services/sync-types.js +2 -0
  75. package/dist/server/server/services/telemetry-service.js +143 -0
  76. package/dist/server/server/services/workflow-service.js +26 -5
  77. package/dist/server/server/services/workflow-service.test.js +159 -0
  78. package/dist/server/server/settings-sync.js +284 -0
  79. package/dist/server/server/update-planner.js +279 -0
  80. package/dist/server/server/utils/cc-hooks-parser.js +3 -0
  81. package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
  82. package/dist/server/server/utils/dispatch-utils.js +77 -20
  83. package/dist/server/server/utils/dispatch-utils.test.js +182 -0
  84. package/dist/server/server/utils/logger.js +37 -3
  85. package/dist/server/server/utils/package-info.js +30 -0
  86. package/dist/server/server/utils/route-helpers.js +10 -0
  87. package/dist/server/server/utils/terminal-launcher.js +79 -25
  88. package/dist/server/server/utils/worktree-manager.js +13 -4
  89. package/dist/server/server/validator.js +230 -0
  90. package/dist/server/server/watchers/global-watcher.js +63 -0
  91. package/dist/server/server/watchers/scope-watcher.js +27 -12
  92. package/dist/server/server/wizard/config-editor.js +237 -0
  93. package/dist/server/server/wizard/detect.js +96 -0
  94. package/dist/server/server/wizard/doctor.js +115 -0
  95. package/dist/server/server/wizard/index.js +155 -0
  96. package/dist/server/server/wizard/phases/confirm.js +39 -0
  97. package/dist/server/server/wizard/phases/project-setup.js +90 -0
  98. package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
  99. package/dist/server/server/wizard/phases/welcome.js +35 -0
  100. package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
  101. package/dist/server/server/wizard/types.js +29 -0
  102. package/dist/server/server/wizard/ui.js +74 -0
  103. package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
  104. package/dist/server/shared/default-workflow.json +65 -0
  105. package/dist/server/shared/onboarding-tour.test.js +81 -0
  106. package/dist/server/shared/project-colors.js +24 -0
  107. package/dist/server/shared/workflow-config.test.js +84 -0
  108. package/dist/server/shared/workflow-engine.test.js +302 -0
  109. package/dist/server/shared/workflow-normalizer.js +101 -0
  110. package/dist/server/shared/workflow-normalizer.test.js +100 -0
  111. package/dist/server/src/components/onboarding/tour-steps.js +84 -0
  112. package/package.json +20 -15
  113. package/schemas/orbital.config.schema.json +16 -1
  114. package/scripts/postinstall.js +55 -7
  115. package/server/__tests__/data-routes.test.ts +149 -0
  116. package/server/__tests__/helpers/db.ts +19 -0
  117. package/server/__tests__/helpers/mock-emitter.ts +10 -0
  118. package/server/__tests__/scope-routes.test.ts +157 -0
  119. package/server/__tests__/sprint-routes.test.ts +118 -0
  120. package/server/__tests__/workflow-routes.test.ts +120 -0
  121. package/server/config-migrator.ts +163 -0
  122. package/server/config.ts +26 -2
  123. package/server/database.ts +35 -18
  124. package/server/global-config.ts +200 -0
  125. package/server/index.ts +975 -287
  126. package/server/init.ts +625 -182
  127. package/server/launch.ts +32 -0
  128. package/server/manifest-types.ts +145 -0
  129. package/server/manifest.ts +494 -0
  130. package/server/migrate-legacy.ts +290 -0
  131. package/server/parsers/event-parser.test.ts +135 -0
  132. package/server/parsers/scope-parser.test.ts +270 -0
  133. package/server/parsers/scope-parser.ts +79 -31
  134. package/server/project-context.ts +309 -0
  135. package/server/project-emitter.ts +50 -0
  136. package/server/project-manager.ts +369 -0
  137. package/server/routes/config-routes.ts +3 -5
  138. package/server/routes/data-routes.ts +28 -141
  139. package/server/routes/dispatch-routes.ts +19 -11
  140. package/server/routes/git-routes.ts +77 -0
  141. package/server/routes/manifest-routes.ts +388 -0
  142. package/server/routes/scope-routes.ts +29 -25
  143. package/server/routes/sync-routes.ts +175 -0
  144. package/server/routes/version-routes.ts +1 -16
  145. package/server/routes/workflow-routes.ts +9 -3
  146. package/server/schema.ts +2 -0
  147. package/server/services/batch-orchestrator.ts +24 -16
  148. package/server/services/claude-session-service.ts +16 -14
  149. package/server/services/deploy-service.test.ts +145 -0
  150. package/server/services/deploy-service.ts +2 -2
  151. package/server/services/event-service.test.ts +242 -0
  152. package/server/services/event-service.ts +92 -3
  153. package/server/services/gate-service.test.ts +131 -0
  154. package/server/services/gate-service.ts +2 -2
  155. package/server/services/git-service.ts +137 -4
  156. package/server/services/github-service.ts +120 -2
  157. package/server/services/readiness-service.test.ts +217 -0
  158. package/server/services/scope-cache.test.ts +167 -0
  159. package/server/services/scope-cache.ts +4 -1
  160. package/server/services/scope-service.test.ts +169 -0
  161. package/server/services/scope-service.ts +220 -126
  162. package/server/services/sprint-orchestrator.ts +7 -7
  163. package/server/services/sprint-service.test.ts +271 -0
  164. package/server/services/sprint-service.ts +27 -3
  165. package/server/services/sync-service.ts +482 -0
  166. package/server/services/sync-types.ts +77 -0
  167. package/server/services/telemetry-service.ts +195 -0
  168. package/server/services/workflow-service.test.ts +190 -0
  169. package/server/services/workflow-service.ts +29 -9
  170. package/server/settings-sync.ts +359 -0
  171. package/server/update-planner.ts +346 -0
  172. package/server/utils/cc-hooks-parser.test.ts +96 -0
  173. package/server/utils/cc-hooks-parser.ts +4 -0
  174. package/server/utils/dispatch-utils.test.ts +245 -0
  175. package/server/utils/dispatch-utils.ts +97 -27
  176. package/server/utils/logger.ts +40 -3
  177. package/server/utils/package-info.ts +32 -0
  178. package/server/utils/route-helpers.ts +12 -0
  179. package/server/utils/terminal-launcher.ts +85 -25
  180. package/server/utils/worktree-manager.ts +9 -4
  181. package/server/validator.ts +270 -0
  182. package/server/watchers/global-watcher.ts +77 -0
  183. package/server/watchers/scope-watcher.ts +21 -9
  184. package/server/wizard/config-editor.ts +248 -0
  185. package/server/wizard/detect.ts +104 -0
  186. package/server/wizard/doctor.ts +114 -0
  187. package/server/wizard/index.ts +187 -0
  188. package/server/wizard/phases/confirm.ts +45 -0
  189. package/server/wizard/phases/project-setup.ts +106 -0
  190. package/server/wizard/phases/setup-wizard.ts +78 -0
  191. package/server/wizard/phases/welcome.ts +43 -0
  192. package/server/wizard/phases/workflow-setup.ts +28 -0
  193. package/server/wizard/types.ts +56 -0
  194. package/server/wizard/ui.ts +93 -0
  195. package/shared/__fixtures__/workflow-configs.ts +80 -0
  196. package/shared/default-workflow.json +65 -0
  197. package/shared/onboarding-tour.test.ts +94 -0
  198. package/shared/project-colors.ts +24 -0
  199. package/shared/workflow-config.test.ts +111 -0
  200. package/shared/workflow-config.ts +7 -0
  201. package/shared/workflow-engine.test.ts +388 -0
  202. package/shared/workflow-normalizer.test.ts +119 -0
  203. package/shared/workflow-normalizer.ts +118 -0
  204. package/templates/hooks/end-session.sh +3 -1
  205. package/templates/hooks/orbital-emit.sh +2 -2
  206. package/templates/hooks/orbital-report-deploy.sh +4 -4
  207. package/templates/hooks/orbital-report-gates.sh +4 -4
  208. package/templates/hooks/orbital-scope-update.sh +1 -1
  209. package/templates/hooks/scope-create-cleanup.sh +2 -2
  210. package/templates/hooks/scope-create-gate.sh +0 -1
  211. package/templates/hooks/scope-helpers.sh +18 -0
  212. package/templates/hooks/scope-prepare.sh +66 -11
  213. package/templates/migrations/renames.json +1 -0
  214. package/templates/orbital.config.json +7 -2
  215. package/templates/settings-hooks.json +1 -1
  216. package/templates/skills/git-commit/SKILL.md +9 -4
  217. package/templates/skills/git-dev/SKILL.md +8 -3
  218. package/templates/skills/git-main/SKILL.md +8 -2
  219. package/templates/skills/git-production/SKILL.md +6 -2
  220. package/templates/skills/git-staging/SKILL.md +8 -3
  221. package/templates/skills/scope-create/SKILL.md +17 -3
  222. package/templates/skills/scope-fix-review/SKILL.md +6 -3
  223. package/templates/skills/scope-implement/SKILL.md +4 -1
  224. package/templates/skills/scope-post-review/SKILL.md +63 -5
  225. package/templates/skills/scope-pre-review/SKILL.md +5 -2
  226. package/templates/skills/scope-verify/SKILL.md +5 -3
  227. package/templates/skills/test-code-review/SKILL.md +41 -33
  228. package/templates/skills/test-scaffold/SKILL.md +222 -0
  229. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
  230. package/dist/assets/charts-D__PA1zp.js +0 -72
  231. package/dist/assets/index-D1G6i0nS.css +0 -1
  232. package/dist/assets/index-DpItvKpf.js +0 -419
  233. package/dist/assets/ui-BvF022GT.js +0 -53
  234. package/index.html +0 -15
  235. package/postcss.config.js +0 -6
  236. package/src/App.tsx +0 -33
  237. package/src/components/AgentBadge.tsx +0 -40
  238. package/src/components/BatchPreflightModal.tsx +0 -115
  239. package/src/components/CardDisplayToggle.tsx +0 -74
  240. package/src/components/ColumnHeaderActions.tsx +0 -55
  241. package/src/components/ColumnMenu.tsx +0 -99
  242. package/src/components/DeployHistory.tsx +0 -141
  243. package/src/components/DispatchModal.tsx +0 -164
  244. package/src/components/DispatchPopover.tsx +0 -139
  245. package/src/components/DragOverlay.tsx +0 -25
  246. package/src/components/DriftSidebar.tsx +0 -140
  247. package/src/components/EnvironmentStrip.tsx +0 -88
  248. package/src/components/ErrorBoundary.tsx +0 -62
  249. package/src/components/FilterChip.tsx +0 -105
  250. package/src/components/GateIndicator.tsx +0 -33
  251. package/src/components/IdeaDetailModal.tsx +0 -190
  252. package/src/components/IdeaFormDialog.tsx +0 -113
  253. package/src/components/KanbanColumn.tsx +0 -201
  254. package/src/components/MarkdownRenderer.tsx +0 -114
  255. package/src/components/NeonGrid.tsx +0 -128
  256. package/src/components/PromotionQueue.tsx +0 -89
  257. package/src/components/ScopeCard.tsx +0 -234
  258. package/src/components/ScopeDetailModal.tsx +0 -255
  259. package/src/components/ScopeFilterBar.tsx +0 -152
  260. package/src/components/SearchInput.tsx +0 -102
  261. package/src/components/SessionPanel.tsx +0 -335
  262. package/src/components/SprintContainer.tsx +0 -303
  263. package/src/components/SprintDependencyDialog.tsx +0 -78
  264. package/src/components/SprintPreflightModal.tsx +0 -138
  265. package/src/components/StatusBar.tsx +0 -168
  266. package/src/components/SwimCell.tsx +0 -67
  267. package/src/components/SwimLaneRow.tsx +0 -94
  268. package/src/components/SwimlaneBoardView.tsx +0 -108
  269. package/src/components/VersionBadge.tsx +0 -139
  270. package/src/components/ViewModeSelector.tsx +0 -114
  271. package/src/components/config/AgentChip.tsx +0 -53
  272. package/src/components/config/AgentCreateDialog.tsx +0 -321
  273. package/src/components/config/AgentEditor.tsx +0 -175
  274. package/src/components/config/DirectoryTree.tsx +0 -582
  275. package/src/components/config/FileEditor.tsx +0 -550
  276. package/src/components/config/HookChip.tsx +0 -50
  277. package/src/components/config/StageCard.tsx +0 -198
  278. package/src/components/config/TransitionZone.tsx +0 -173
  279. package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
  280. package/src/components/config/WorkflowPipeline.tsx +0 -161
  281. package/src/components/source-control/BranchList.tsx +0 -93
  282. package/src/components/source-control/BranchPanel.tsx +0 -105
  283. package/src/components/source-control/CommitLog.tsx +0 -100
  284. package/src/components/source-control/CommitRow.tsx +0 -47
  285. package/src/components/source-control/GitHubPanel.tsx +0 -110
  286. package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
  287. package/src/components/source-control/GitOverviewBar.tsx +0 -101
  288. package/src/components/source-control/PullRequestList.tsx +0 -69
  289. package/src/components/source-control/WorktreeList.tsx +0 -80
  290. package/src/components/ui/badge.tsx +0 -41
  291. package/src/components/ui/button.tsx +0 -55
  292. package/src/components/ui/card.tsx +0 -78
  293. package/src/components/ui/dialog.tsx +0 -94
  294. package/src/components/ui/popover.tsx +0 -33
  295. package/src/components/ui/scroll-area.tsx +0 -54
  296. package/src/components/ui/separator.tsx +0 -28
  297. package/src/components/ui/tabs.tsx +0 -52
  298. package/src/components/ui/toggle-switch.tsx +0 -35
  299. package/src/components/ui/tooltip.tsx +0 -27
  300. package/src/components/workflow/AddEdgeDialog.tsx +0 -217
  301. package/src/components/workflow/AddListDialog.tsx +0 -201
  302. package/src/components/workflow/ChecklistEditor.tsx +0 -239
  303. package/src/components/workflow/CommandPrefixManager.tsx +0 -118
  304. package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
  305. package/src/components/workflow/DirectionSelector.tsx +0 -133
  306. package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
  307. package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
  308. package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
  309. package/src/components/workflow/EditToolbar.tsx +0 -138
  310. package/src/components/workflow/HookDetailPanel.tsx +0 -250
  311. package/src/components/workflow/HookExecutionLog.tsx +0 -24
  312. package/src/components/workflow/HookSourceModal.tsx +0 -129
  313. package/src/components/workflow/HooksDashboard.tsx +0 -363
  314. package/src/components/workflow/ListPropertyEditor.tsx +0 -251
  315. package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
  316. package/src/components/workflow/MovementRulesPanel.tsx +0 -188
  317. package/src/components/workflow/NodeDetailPanel.tsx +0 -245
  318. package/src/components/workflow/PresetSelector.tsx +0 -414
  319. package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
  320. package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
  321. package/src/components/workflow/WorkflowNode.tsx +0 -147
  322. package/src/components/workflow/graphLayout.ts +0 -186
  323. package/src/components/workflow/mergeHooks.ts +0 -85
  324. package/src/components/workflow/useEditHistory.ts +0 -88
  325. package/src/components/workflow/useWorkflowEditor.ts +0 -262
  326. package/src/components/workflow/validateConfig.ts +0 -70
  327. package/src/hooks/useActiveDispatches.ts +0 -198
  328. package/src/hooks/useBoardSettings.ts +0 -170
  329. package/src/hooks/useCardDisplay.ts +0 -57
  330. package/src/hooks/useCcHooks.ts +0 -24
  331. package/src/hooks/useConfigTree.ts +0 -51
  332. package/src/hooks/useEnforcementRules.ts +0 -46
  333. package/src/hooks/useEvents.ts +0 -59
  334. package/src/hooks/useFileEditor.ts +0 -165
  335. package/src/hooks/useGates.ts +0 -57
  336. package/src/hooks/useIdeaActions.ts +0 -53
  337. package/src/hooks/useKanbanDnd.ts +0 -410
  338. package/src/hooks/useOrbitalConfig.ts +0 -54
  339. package/src/hooks/usePipeline.ts +0 -47
  340. package/src/hooks/usePipelineData.ts +0 -338
  341. package/src/hooks/useReconnect.ts +0 -25
  342. package/src/hooks/useScopeFilters.ts +0 -125
  343. package/src/hooks/useScopeSessions.ts +0 -44
  344. package/src/hooks/useScopes.ts +0 -67
  345. package/src/hooks/useSearch.ts +0 -67
  346. package/src/hooks/useSettings.tsx +0 -187
  347. package/src/hooks/useSocket.ts +0 -25
  348. package/src/hooks/useSourceControl.ts +0 -105
  349. package/src/hooks/useSprintPreflight.ts +0 -55
  350. package/src/hooks/useSprints.ts +0 -154
  351. package/src/hooks/useStatusBarHighlight.ts +0 -18
  352. package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
  353. package/src/hooks/useTheme.ts +0 -9
  354. package/src/hooks/useTransitionReadiness.ts +0 -53
  355. package/src/hooks/useVersion.ts +0 -155
  356. package/src/hooks/useViolations.ts +0 -65
  357. package/src/hooks/useWorkflow.tsx +0 -125
  358. package/src/hooks/useZoomModifier.ts +0 -19
  359. package/src/index.css +0 -797
  360. package/src/layouts/DashboardLayout.tsx +0 -113
  361. package/src/lib/collisionDetection.ts +0 -20
  362. package/src/lib/scope-fields.ts +0 -61
  363. package/src/lib/swimlane.ts +0 -146
  364. package/src/lib/utils.ts +0 -15
  365. package/src/main.tsx +0 -19
  366. package/src/socket.ts +0 -11
  367. package/src/types/index.ts +0 -497
  368. package/src/views/AgentFeed.tsx +0 -339
  369. package/src/views/DeployPipeline.tsx +0 -59
  370. package/src/views/EnforcementView.tsx +0 -378
  371. package/src/views/PrimitivesConfig.tsx +0 -500
  372. package/src/views/QualityGates.tsx +0 -1012
  373. package/src/views/ScopeBoard.tsx +0 -454
  374. package/src/views/SessionTimeline.tsx +0 -516
  375. package/src/views/Settings.tsx +0 -183
  376. package/src/views/SourceControl.tsx +0 -95
  377. package/src/views/WorkflowVisualizer.tsx +0 -382
  378. package/tailwind.config.js +0 -161
  379. package/tsconfig.json +0 -25
  380. package/vite.config.ts +0 -49
@@ -0,0 +1,297 @@
1
+ import fs from 'fs';
2
+ import { Router } from 'express';
3
+ import { ProjectEmitter } from './project-emitter.js';
4
+ import { createProjectContext } from './project-context.js';
5
+ import { loadGlobalConfig, registerProject, unregisterProject, updateProject, findProject, } from './global-config.js';
6
+ import { createScopeRoutes } from './routes/scope-routes.js';
7
+ import { createDataRoutes } from './routes/data-routes.js';
8
+ import { createDispatchRoutes } from './routes/dispatch-routes.js';
9
+ import { createSprintRoutes } from './routes/sprint-routes.js';
10
+ import { createWorkflowRoutes } from './routes/workflow-routes.js';
11
+ import { createConfigRoutes } from './routes/config-routes.js';
12
+ import { createGitRoutes } from './routes/git-routes.js';
13
+ import { createManifestRoutes } from './routes/manifest-routes.js';
14
+ import { createTelemetryRoutes } from './services/telemetry-service.js';
15
+ import { TEMPLATES_DIR } from './init.js';
16
+ import { getPackageVersion } from './utils/package-info.js';
17
+ import { resolveActiveDispatchesForScope } from './utils/dispatch-utils.js';
18
+ import { createLogger } from './utils/logger.js';
19
+ const log = createLogger('project-manager');
20
+ // ─── Manager ────────────────────────────────────────────────
21
+ export class ProjectManager {
22
+ io;
23
+ contexts = new Map();
24
+ routers = new Map();
25
+ healthCheckInterval = null;
26
+ constructor(io) {
27
+ this.io = io;
28
+ }
29
+ // ─── Initialization ─────────────────────────────────────
30
+ /** Initialize all enabled projects from the registry. */
31
+ async initializeAll() {
32
+ const config = loadGlobalConfig();
33
+ const enabledProjects = config.projects.filter(p => p.enabled);
34
+ log.info('Initializing projects', { count: enabledProjects.length });
35
+ for (const reg of enabledProjects) {
36
+ try {
37
+ await this.initializeProject(reg);
38
+ }
39
+ catch (err) {
40
+ log.error('Failed to initialize project', {
41
+ id: reg.id,
42
+ path: reg.path,
43
+ error: String(err),
44
+ });
45
+ }
46
+ }
47
+ // Start periodic health checks
48
+ this.healthCheckInterval = setInterval(() => this.checkHealth(), 60_000);
49
+ }
50
+ /** Initialize a single project from its registration. */
51
+ async initializeProject(reg) {
52
+ // Verify directory exists
53
+ if (!fs.existsSync(reg.path)) {
54
+ log.warn('Project directory not found, marking offline', { id: reg.id, path: reg.path });
55
+ // Store a placeholder context to track status
56
+ return null;
57
+ }
58
+ const emitter = new ProjectEmitter(this.io, reg.id);
59
+ try {
60
+ const ctx = await createProjectContext(reg.id, reg.path, emitter);
61
+ this.contexts.set(reg.id, ctx);
62
+ // Build and cache the project's router
63
+ const router = this.buildProjectRouter(ctx);
64
+ this.routers.set(reg.id, router);
65
+ log.info('Project initialized', {
66
+ id: reg.id,
67
+ name: reg.name,
68
+ scopeCount: ctx.scopeService.getAll().length,
69
+ });
70
+ return ctx;
71
+ }
72
+ catch (err) {
73
+ log.error('Project initialization failed', { id: reg.id, error: String(err) });
74
+ return null;
75
+ }
76
+ }
77
+ // ─── Context Access ─────────────────────────────────────
78
+ /** Get a project context by ID. */
79
+ getContext(id) {
80
+ return this.contexts.get(id);
81
+ }
82
+ /** Get all active contexts. */
83
+ getAllContexts() {
84
+ return this.contexts;
85
+ }
86
+ /** Get the router for a project. */
87
+ getRouter(id) {
88
+ return this.routers.get(id);
89
+ }
90
+ /** Get all project routers. */
91
+ getAllRouters() {
92
+ return this.routers;
93
+ }
94
+ // ─── Project List ───────────────────────────────────────
95
+ /** Get summary of all registered projects with live status. */
96
+ getProjectList(options) {
97
+ const config = loadGlobalConfig();
98
+ return config.projects.map(reg => {
99
+ const ctx = this.contexts.get(reg.id);
100
+ const summary = {
101
+ id: reg.id,
102
+ name: reg.name,
103
+ path: reg.path,
104
+ color: reg.color,
105
+ status: ctx?.status ?? 'offline',
106
+ enabled: reg.enabled,
107
+ scopeCount: ctx ? ctx.scopeService.getAll().length : 0,
108
+ error: ctx?.error,
109
+ };
110
+ if (options?.includeWorkflow && ctx) {
111
+ summary.workflow = ctx.workflowService.getActive();
112
+ }
113
+ return summary;
114
+ });
115
+ }
116
+ // ─── Registration ───────────────────────────────────────
117
+ /** Register and initialize a new project. */
118
+ async addProject(projectRoot, options) {
119
+ const reg = registerProject(projectRoot, options);
120
+ const ctx = await this.initializeProject(reg);
121
+ // Notify all clients
122
+ this.io.emit('project:registered', {
123
+ id: reg.id,
124
+ name: reg.name,
125
+ path: reg.path,
126
+ color: reg.color,
127
+ });
128
+ return {
129
+ id: reg.id,
130
+ name: reg.name,
131
+ path: reg.path,
132
+ color: reg.color,
133
+ status: ctx?.status ?? 'offline',
134
+ enabled: reg.enabled,
135
+ scopeCount: ctx ? ctx.scopeService.getAll().length : 0,
136
+ };
137
+ }
138
+ /** Unregister a project and shut down its context. */
139
+ async removeProject(idOrPath) {
140
+ // Find the registration before removing
141
+ const config = loadGlobalConfig();
142
+ const reg = config.projects.find(p => p.id === idOrPath || p.path === idOrPath);
143
+ if (!reg)
144
+ return false;
145
+ // Shut down context
146
+ await this.shutdownProject(reg.id);
147
+ // Remove from registry
148
+ unregisterProject(idOrPath);
149
+ // Notify clients
150
+ this.io.emit('project:unregistered', { id: reg.id });
151
+ return true;
152
+ }
153
+ /** Update project metadata (name, color, enabled). */
154
+ async updateProject(id, updates) {
155
+ const result = updateProject(id, updates);
156
+ if (!result)
157
+ return null;
158
+ // If disabling, shut down the context
159
+ if (updates.enabled === false) {
160
+ await this.shutdownProject(id);
161
+ }
162
+ // If enabling, initialize the context
163
+ if (updates.enabled === true && !this.contexts.has(id)) {
164
+ const reg = findProject(id);
165
+ if (reg)
166
+ await this.initializeProject(reg);
167
+ }
168
+ // Notify clients of metadata change
169
+ this.io.emit('project:updated', { id, ...updates });
170
+ return result;
171
+ }
172
+ // ─── Lifecycle ──────────────────────────────────────────
173
+ /** Shut down a single project's context. */
174
+ async shutdownProject(id) {
175
+ const ctx = this.contexts.get(id);
176
+ if (ctx) {
177
+ await ctx.shutdown();
178
+ this.contexts.delete(id);
179
+ this.routers.delete(id);
180
+ log.info('Project shut down', { id });
181
+ }
182
+ }
183
+ /** Shut down all projects and stop health checks. */
184
+ async shutdownAll() {
185
+ if (this.healthCheckInterval) {
186
+ clearInterval(this.healthCheckInterval);
187
+ this.healthCheckInterval = null;
188
+ }
189
+ const shutdowns = [...this.contexts.entries()].map(([id, ctx]) => ctx.shutdown().catch(err => log.error('Error shutting down project', { id, error: String(err) })));
190
+ await Promise.all(shutdowns);
191
+ this.contexts.clear();
192
+ this.routers.clear();
193
+ log.info('All projects shut down');
194
+ }
195
+ // ─── Health Checks ──────────────────────────────────────
196
+ /** Periodic health check — detect projects that have gone offline or come back. */
197
+ async checkHealth() {
198
+ const config = loadGlobalConfig();
199
+ for (const reg of config.projects) {
200
+ if (!reg.enabled)
201
+ continue;
202
+ const ctx = this.contexts.get(reg.id);
203
+ const dirExists = fs.existsSync(reg.path);
204
+ if (ctx && !dirExists) {
205
+ // Project went offline
206
+ log.warn('Project directory disappeared', { id: reg.id, path: reg.path });
207
+ await this.shutdownProject(reg.id);
208
+ this.io.emit('project:status:changed', { id: reg.id, status: 'offline' });
209
+ }
210
+ else if (!ctx && dirExists) {
211
+ // Project came back online (or failed to initialize previously — retry)
212
+ log.info('Attempting to initialize project', { id: reg.id, path: reg.path });
213
+ try {
214
+ await this.initializeProject(reg);
215
+ this.io.emit('project:status:changed', { id: reg.id, status: 'active' });
216
+ }
217
+ catch (err) {
218
+ log.warn('Project initialization retry failed', { id: reg.id, error: String(err) });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ // ─── Route Building ─────────────────────────────────────
224
+ /** Build an Express Router with all per-project routes for a context. */
225
+ /** Build an Express Router with all per-project routes.
226
+ * Note: createVersionRoutes is intentionally omitted — version/update endpoints
227
+ * are global (they update the Orbital Command package), not per-project. */
228
+ buildProjectRouter(ctx) {
229
+ const router = Router();
230
+ const { db, emitter, config, scopeService, eventService, gateService, deployService, sprintService, sprintOrchestrator, batchOrchestrator, readinessService, workflowService, workflowEngine, gitService, githubService, telemetryService } = ctx;
231
+ // Scope status inference function (same logic as index.ts)
232
+ function inferScopeStatus(eventType, scopeId, data) {
233
+ if (scopeId == null)
234
+ return;
235
+ const id = Number(scopeId);
236
+ if (isNaN(id) || id <= 0)
237
+ return;
238
+ const current = scopeService.getById(id);
239
+ if (current?.status === 'icebox')
240
+ return;
241
+ const currentStatus = current?.status ?? '';
242
+ const result = workflowEngine.inferStatus(eventType, currentStatus, data);
243
+ if (result === null)
244
+ return;
245
+ if (typeof result === 'object' && 'dispatchResolution' in result) {
246
+ resolveActiveDispatchesForScope(db, emitter, id, result.resolution);
247
+ return;
248
+ }
249
+ scopeService.updateStatus(id, result, 'event');
250
+ }
251
+ // Project config endpoint
252
+ router.get('/config', (_req, res) => {
253
+ res.json({
254
+ projectName: config.projectName,
255
+ categories: config.categories,
256
+ agents: config.agents,
257
+ serverPort: config.serverPort,
258
+ clientPort: config.clientPort,
259
+ });
260
+ });
261
+ // Mount all route groups
262
+ router.use(createScopeRoutes({
263
+ db, io: emitter, scopeService, readinessService,
264
+ projectRoot: config.projectRoot, projectName: config.projectName,
265
+ engine: workflowEngine,
266
+ }));
267
+ router.use(createDataRoutes({
268
+ db, io: emitter, eventService, gateService, deployService, gitService,
269
+ engine: workflowEngine, projectRoot: config.projectRoot,
270
+ inferScopeStatus,
271
+ }));
272
+ router.use(createDispatchRoutes({
273
+ db, io: emitter, scopeService,
274
+ projectRoot: config.projectRoot, engine: workflowEngine,
275
+ }));
276
+ router.use(createSprintRoutes({
277
+ sprintService, sprintOrchestrator, batchOrchestrator,
278
+ }));
279
+ router.use(createWorkflowRoutes({
280
+ workflowService, projectRoot: config.projectRoot,
281
+ }));
282
+ router.use(createConfigRoutes({
283
+ projectRoot: config.projectRoot, workflowService, io: emitter,
284
+ }));
285
+ router.use(createGitRoutes({
286
+ gitService, githubService, engine: workflowEngine,
287
+ }));
288
+ router.use(createManifestRoutes({
289
+ projectRoot: config.projectRoot,
290
+ templatesDir: TEMPLATES_DIR,
291
+ packageVersion: getPackageVersion(),
292
+ io: emitter,
293
+ }));
294
+ router.use(createTelemetryRoutes({ telemetryService }));
295
+ return router;
296
+ }
297
+ }
@@ -1,5 +1,6 @@
1
1
  import { Router } from 'express';
2
2
  import { ConfigService, isValidPrimitiveType } from '../services/config-service.js';
3
+ import { errMsg } from '../utils/route-helpers.js';
3
4
  export function createConfigRoutes({ projectRoot, workflowService: _workflowService, io }) {
4
5
  const router = Router();
5
6
  const configService = new ConfigService(projectRoot);
@@ -158,6 +159,3 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
158
159
  });
159
160
  return router;
160
161
  }
161
- function errMsg(err) {
162
- return err instanceof Error ? err.message : String(err);
163
- }
@@ -5,6 +5,8 @@ import { promisify } from 'util';
5
5
  import { getHookEnforcement } from '../../shared/workflow-config.js';
6
6
  import { getClaudeSessions, getSessionStats } from '../services/claude-session-service.js';
7
7
  import { launchInTerminal } from '../utils/terminal-launcher.js';
8
+ import { createLogger } from '../utils/logger.js';
9
+ const log = createLogger('server');
8
10
  const execFileAsync = promisify(execFile);
9
11
  const JSON_FIELDS = ['tags', 'blocked_by', 'blocks', 'data', 'discoveries', 'next_steps', 'details'];
10
12
  function parseJsonFields(row) {
@@ -19,77 +21,14 @@ function parseJsonFields(row) {
19
21
  }
20
22
  return parsed;
21
23
  }
22
- function parseDriftCommits(raw) {
23
- if (!raw)
24
- return [];
25
- return raw.split('\n').map((line) => {
26
- const [sha, date, message, author] = line.split('|');
27
- return { sha, date, message: message ?? '', author: author ?? '' };
28
- });
29
- }
30
- function parseHead(raw) {
31
- const [sha, date, message] = raw.split('|');
32
- return { sha: sha ?? '', date: date ?? '', message: message ?? '' };
33
- }
34
- export function createDataRoutes({ db, io, gateService, deployService, engine, projectRoot, inferScopeStatus, }) {
24
+ export function createDataRoutes({ db, io, eventService, gateService, deployService, gitService, engine, projectRoot, inferScopeStatus, }) {
35
25
  const router = Router();
36
- // ─── Pipeline Drift (cached) ─────────────────────────────
37
- let driftCache = null;
38
- const DRIFT_CACHE_MS = 60_000;
39
- async function gitLog(args) {
40
- const { stdout } = await execFileAsync('git', args, { cwd: projectRoot });
41
- return stdout.trim();
42
- }
43
- async function computeDrift() {
44
- if (driftCache && Date.now() - driftCache.ts < DRIFT_CACHE_MS)
45
- return driftCache.data;
46
- const [devToStagingRaw, stagingToMainRaw, devHead, stagingHead, mainHead] = await Promise.all([
47
- gitLog(['log', 'origin/dev', '--not', 'origin/staging', '--reverse', '--format=%H|%aI|%s|%an']),
48
- gitLog(['log', 'origin/staging', '--not', 'origin/main', '--reverse', '--format=%H|%aI|%s|%an']),
49
- gitLog(['log', 'origin/dev', '-1', '--format=%H|%aI|%s']),
50
- gitLog(['log', 'origin/staging', '-1', '--format=%H|%aI|%s']),
51
- gitLog(['log', 'origin/main', '-1', '--format=%H|%aI|%s']),
52
- ]);
53
- const devToStaging = parseDriftCommits(devToStagingRaw);
54
- const stagingToMain = parseDriftCommits(stagingToMainRaw);
55
- const data = {
56
- devToStaging: {
57
- count: devToStaging.length,
58
- commits: devToStaging,
59
- oldestDate: devToStaging[0]?.date ?? null,
60
- },
61
- stagingToMain: {
62
- count: stagingToMain.length,
63
- commits: stagingToMain,
64
- oldestDate: stagingToMain[0]?.date ?? null,
65
- },
66
- heads: {
67
- dev: parseHead(devHead),
68
- staging: parseHead(stagingHead),
69
- main: parseHead(mainHead),
70
- },
71
- };
72
- driftCache = { data, ts: Date.now() };
73
- return data;
74
- }
75
26
  // ─── Event Routes ──────────────────────────────────────────
76
27
  router.get('/events', (req, res) => {
77
28
  const limit = Number(req.query.limit) || 50;
78
29
  const type = req.query.type;
79
- const scopeId = req.query.scope_id;
80
- let query = 'SELECT * FROM events WHERE 1=1';
81
- const params = [];
82
- if (type) {
83
- query += ' AND type = ?';
84
- params.push(type);
85
- }
86
- if (scopeId) {
87
- query += ' AND scope_id = ?';
88
- params.push(Number(scopeId));
89
- }
90
- query += ' ORDER BY timestamp DESC LIMIT ?';
91
- params.push(limit);
92
- const events = db.prepare(query).all(...params);
30
+ const scopeId = req.query.scope_id ? Number(req.query.scope_id) : undefined;
31
+ const events = eventService.getFiltered({ limit, type, scopeId });
93
32
  res.json(events.map(parseJsonFields));
94
33
  });
95
34
  router.post('/events', (req, res) => {
@@ -115,18 +54,10 @@ export function createDataRoutes({ db, io, gateService, deployService, engine, p
115
54
  // ─── Violations Summary ──────────────────────────────────
116
55
  router.get('/events/violations/summary', (_req, res) => {
117
56
  try {
118
- const byRule = db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
119
- FROM events WHERE type = 'VIOLATION' GROUP BY rule ORDER BY count DESC`).all();
120
- const byFile = db.prepare(`SELECT JSON_EXTRACT(data, '$.file') as file, COUNT(*) as count FROM events
121
- WHERE type = 'VIOLATION' AND JSON_EXTRACT(data, '$.file') IS NOT NULL AND JSON_EXTRACT(data, '$.file') != ''
122
- GROUP BY file ORDER BY count DESC LIMIT 20`).all();
123
- const overrides = db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, JSON_EXTRACT(data, '$.reason') as reason, timestamp as date
124
- FROM events WHERE type = 'OVERRIDE' ORDER BY timestamp DESC LIMIT 50`).all();
125
- const totalViolations = db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'VIOLATION'`).get();
126
- const totalOverrides = db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'OVERRIDE'`).get();
127
- res.json({ byRule, byFile, overrides, totalViolations: totalViolations.count, totalOverrides: totalOverrides.count });
128
- }
129
- catch {
57
+ res.json(eventService.getViolationSummary());
58
+ }
59
+ catch (err) {
60
+ log.error('Violations summary failed', { error: String(err) });
130
61
  res.status(500).json({ error: 'Failed to query violations summary' });
131
62
  }
132
63
  });
@@ -144,13 +75,7 @@ export function createDataRoutes({ db, io, gateService, deployService, engine, p
144
75
  hookEdgeMap.get(hookId).push({ from: edge.from, to: edge.to, label: edge.label });
145
76
  }
146
77
  }
147
- // Query violation and override stats per rule
148
- const violationStats = db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
149
- FROM events WHERE type = 'VIOLATION' GROUP BY rule`).all();
150
- const overrideStats = db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
151
- FROM events WHERE type = 'OVERRIDE' GROUP BY rule`).all();
152
- const violationMap = new Map(violationStats.map((v) => [v.rule, v]));
153
- const overrideMap = new Map(overrideStats.map((o) => [o.rule, o]));
78
+ const { violations: violationMap, overrides: overrideMap } = eventService.getViolationStatsByRule();
154
79
  // Build summary counts
155
80
  const summary = { guards: 0, gates: 0, lifecycle: 0, observers: 0 };
156
81
  for (const hook of allHooks) {
@@ -175,20 +100,19 @@ export function createDataRoutes({ db, io, gateService, deployService, engine, p
175
100
  }));
176
101
  res.json({ summary, rules, totalEdges: allEdges.length });
177
102
  }
178
- catch {
103
+ catch (err) {
104
+ log.error('Enforcement rules failed', { error: String(err) });
179
105
  res.status(500).json({ error: 'Failed to query enforcement rules' });
180
106
  }
181
107
  });
182
108
  // ─── Violation Trends ──────────────────────────────────────
183
109
  router.get('/events/violations/trend', (req, res) => {
184
110
  try {
185
- const days = Number(req.query.days) || 30;
186
- const trend = db.prepare(`SELECT date(timestamp) as day, JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
187
- FROM events WHERE type = 'VIOLATION' AND timestamp >= datetime('now', ? || ' days')
188
- GROUP BY day, rule ORDER BY day ASC`).all(`-${days}`);
189
- res.json(trend);
111
+ const days = Math.max(1, Math.min(365, Number(req.query.days) || 30));
112
+ res.json(eventService.getViolationTrend(days));
190
113
  }
191
- catch {
114
+ catch (err) {
115
+ log.error('Violation trends failed', { error: String(err) });
192
116
  res.status(500).json({ error: 'Failed to query violation trends' });
193
117
  }
194
118
  });
@@ -251,8 +175,7 @@ export function createDataRoutes({ db, io, gateService, deployService, engine, p
251
175
  });
252
176
  router.get('/pipeline/drift', async (_req, res) => {
253
177
  try {
254
- const drift = await computeDrift();
255
- res.json(drift);
178
+ res.json(await gitService.getPipelineDrift());
256
179
  }
257
180
  catch (err) {
258
181
  res.status(500).json({ error: 'Failed to compute drift', details: String(err) });
@@ -260,21 +183,10 @@ export function createDataRoutes({ db, io, gateService, deployService, engine, p
260
183
  });
261
184
  router.get('/deployments/frequency', (_req, res) => {
262
185
  try {
263
- const rows = db.prepare(`SELECT environment, strftime('%Y-W%W', started_at) as week, COUNT(*) as count
264
- FROM deployments WHERE started_at > datetime('now', '-56 days') GROUP BY environment, week ORDER BY week ASC`).all();
265
- const weekMap = new Map();
266
- for (const row of rows) {
267
- if (!weekMap.has(row.week))
268
- weekMap.set(row.week, { week: row.week, staging: 0, production: 0 });
269
- const entry = weekMap.get(row.week);
270
- if (row.environment === 'staging')
271
- entry.staging = row.count;
272
- if (row.environment === 'production')
273
- entry.production = row.count;
274
- }
275
- res.json([...weekMap.values()]);
186
+ res.json(eventService.getDeployFrequency());
276
187
  }
277
- catch {
188
+ catch (err) {
189
+ log.error('Deploy frequency query failed', { error: String(err) });
278
190
  res.status(500).json({ error: 'Failed to query deployment frequency' });
279
191
  }
280
192
  });
@@ -330,7 +242,7 @@ export function createDataRoutes({ db, io, gateService, deployService, engine, p
330
242
  let meta = null;
331
243
  let stats = null;
332
244
  if (parsed.claude_session_id && typeof parsed.claude_session_id === 'string') {
333
- const claudeSessions = await getClaudeSessions();
245
+ const claudeSessions = await getClaudeSessions(undefined, projectRoot);
334
246
  const match = claudeSessions.find(s => s.id === parsed.claude_session_id);
335
247
  if (match) {
336
248
  meta = {
@@ -342,7 +254,7 @@ export function createDataRoutes({ db, io, gateService, deployService, engine, p
342
254
  lastActiveAt: match.lastActiveAt,
343
255
  };
344
256
  }
345
- stats = getSessionStats(parsed.claude_session_id);
257
+ stats = getSessionStats(parsed.claude_session_id, projectRoot);
346
258
  }
347
259
  if (!content) {
348
260
  const parts = [];
@@ -1,5 +1,5 @@
1
1
  import { Router } from 'express';
2
- import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
2
+ import { launchInCategorizedTerminal, escapeForAnsiC, shellQuote, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
3
3
  import { resolveDispatchEvent, resolveAbandonedDispatchesForScope, getActiveScopeIds, getAbandonedScopeIds, linkPidToDispatch } from '../utils/dispatch-utils.js';
4
4
  import { createLogger } from '../utils/logger.js';
5
5
  const log = createLogger('dispatch');
@@ -69,20 +69,23 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
69
69
  // Launch in iTerm — interactive TUI mode (no -p) for full visibility
70
70
  const promptText = prompt ?? command;
71
71
  const escaped = escapeForAnsiC(promptText);
72
- const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions $'${escaped}'`;
72
+ const fullCmd = `cd '${shellQuote(projectRoot)}' && ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude --dangerously-skip-permissions $'${escaped}'`;
73
73
  try {
74
74
  await launchInCategorizedTerminal(command, fullCmd, sessionName);
75
75
  res.json({ ok: true, dispatch_id: eventId, scope_id: scope_id ?? null });
76
- // Fire-and-forget: discover session PID, link to dispatch, and rename
76
+ // Fire-and-forget: discover session PID, link to dispatch, and rename.
77
+ // If discovery fails, SESSION_START event handler will link via ORBITAL_DISPATCH_ID.
77
78
  discoverNewSession(projectRoot, beforePids)
78
79
  .then((session) => {
79
- if (!session)
80
+ if (!session) {
81
+ log.warn('PID discovery returned null — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId, scope_id });
80
82
  return;
83
+ }
81
84
  linkPidToDispatch(db, eventId, session.pid);
82
85
  if (sessionName)
83
86
  renameSession(projectRoot, session.sessionId, sessionName);
84
87
  })
85
- .catch(err => log.error('PID discovery failed', { error: err.message }));
88
+ .catch(err => log.warn('PID discovery failed — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId, error: err.message }));
86
89
  }
87
90
  catch (err) {
88
91
  if (scope_id != null && transition?.from) {
@@ -188,18 +191,21 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
188
191
  // Launch single CLI session
189
192
  const batchEscaped = escapeForAnsiC(command);
190
193
  const beforePids = snapshotSessionPids(projectRoot);
191
- const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions -p $'${batchEscaped}'`;
194
+ const fullCmd = `cd '${shellQuote(projectRoot)}' && ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude --dangerously-skip-permissions -p $'${batchEscaped}'`;
192
195
  try {
193
196
  await launchInCategorizedTerminal(command, fullCmd);
194
197
  res.json({ ok: true, dispatch_id: eventId, scope_ids });
195
- // Fire-and-forget: discover session PID and link to dispatch
198
+ // Fire-and-forget: discover session PID and link to dispatch.
199
+ // If discovery fails, SESSION_START event handler will link via ORBITAL_DISPATCH_ID.
196
200
  discoverNewSession(projectRoot, beforePids)
197
201
  .then((session) => {
198
- if (!session)
202
+ if (!session) {
203
+ log.warn('Batch PID discovery returned null — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId });
199
204
  return;
205
+ }
200
206
  linkPidToDispatch(db, eventId, session.pid);
201
207
  })
202
- .catch(err => log.error('Batch PID discovery failed', { error: err.message }));
208
+ .catch(err => log.warn('Batch PID discovery failed — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId, error: err.message }));
203
209
  }
204
210
  catch (err) {
205
211
  if (transition?.from) {
@@ -88,5 +88,79 @@ export function createGitRoutes({ gitService, githubService, engine }) {
88
88
  res.status(500).json({ error: 'Failed to get PRs', details: String(err) });
89
89
  }
90
90
  });
91
+ // ─── GitHub Auth Flow ──────────────────────────────────────
92
+ router.post('/github/connect', async (req, res) => {
93
+ try {
94
+ const { method, token } = req.body;
95
+ if (method === 'pat' && token) {
96
+ const result = await githubService.connectWithToken(token);
97
+ res.json(result);
98
+ }
99
+ else {
100
+ const result = await githubService.connectOAuth();
101
+ res.json(result);
102
+ }
103
+ }
104
+ catch (err) {
105
+ res.status(500).json({ error: 'Failed to connect', details: String(err) });
106
+ }
107
+ });
108
+ router.get('/github/auth-status', async (_req, res) => {
109
+ try {
110
+ const status = await githubService.getAuthStatus();
111
+ res.json(status);
112
+ }
113
+ catch (err) {
114
+ res.status(500).json({ error: 'Failed to check auth', details: String(err) });
115
+ }
116
+ });
117
+ router.post('/github/disconnect', async (_req, res) => {
118
+ try {
119
+ const result = await githubService.disconnect();
120
+ res.json(result);
121
+ }
122
+ catch (err) {
123
+ res.status(500).json({ error: 'Failed to disconnect', details: String(err) });
124
+ }
125
+ });
126
+ // ─── GitHub CI Checks ──────────────────────────────────────
127
+ router.get('/github/checks/:ref', async (req, res) => {
128
+ try {
129
+ const checks = await githubService.getCheckRuns(req.params.ref);
130
+ res.json(checks);
131
+ }
132
+ catch (err) {
133
+ res.status(500).json({ error: 'Failed to get checks', details: String(err) });
134
+ }
135
+ });
136
+ // ─── Git Health Metrics ────────────────────────────────────
137
+ router.get('/git/health', async (_req, res) => {
138
+ try {
139
+ // Get PR ages for health calculation
140
+ let prAges = [];
141
+ try {
142
+ const prs = await githubService.getOpenPRs();
143
+ const now = Date.now();
144
+ prAges = prs.map(pr => Math.round((now - new Date(pr.createdAt).getTime()) / (1000 * 60 * 60 * 24)));
145
+ }
146
+ catch { /* ok — GitHub may not be connected */ }
147
+ const health = await gitService.getHealthMetrics(prAges);
148
+ res.json(health);
149
+ }
150
+ catch (err) {
151
+ res.status(500).json({ error: 'Failed to get health metrics', details: String(err) });
152
+ }
153
+ });
154
+ // ─── Git Activity Series ───────────────────────────────────
155
+ router.get('/git/activity', async (req, res) => {
156
+ try {
157
+ const days = Number(req.query.days) || 30;
158
+ const activity = await gitService.getActivitySeries(days);
159
+ res.json(activity);
160
+ }
161
+ catch (err) {
162
+ res.status(500).json({ error: 'Failed to get activity', details: String(err) });
163
+ }
164
+ });
91
165
  return router;
92
166
  }