orbital-command 0.2.0 → 1.0.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 (431) hide show
  1. package/README.md +67 -42
  2. package/bin/commands/config.js +19 -0
  3. package/bin/commands/events.js +40 -0
  4. package/bin/commands/launch.js +126 -0
  5. package/bin/commands/manifest.js +283 -0
  6. package/bin/commands/registry.js +104 -0
  7. package/bin/commands/update.js +24 -0
  8. package/bin/lib/helpers.js +229 -0
  9. package/bin/orbital.js +147 -319
  10. package/dist/assets/Landing-CfQdHR0N.js +11 -0
  11. package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
  12. package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
  13. package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
  14. package/dist/assets/Settings-DLcZwbCT.js +12 -0
  15. package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
  16. package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
  17. package/dist/assets/arrow-down-DVPp6_qp.js +6 -0
  18. package/dist/assets/bot-NFaJBDn_.js +6 -0
  19. package/dist/assets/charts-LGLb8hyU.js +68 -0
  20. package/dist/assets/circle-x-IsFCkBZu.js +6 -0
  21. package/dist/assets/file-text-J1cebZXF.js +6 -0
  22. package/dist/assets/globe-WzeyHsUc.js +6 -0
  23. package/dist/assets/index-BdJ57EhC.css +1 -0
  24. package/dist/assets/index-o4ScMAuR.js +349 -0
  25. package/dist/assets/key-CKR8JJSj.js +6 -0
  26. package/dist/assets/minus-CHBsJyjp.js +6 -0
  27. package/dist/assets/radio-xqZaR-Uk.js +6 -0
  28. package/dist/assets/rocket-D_xvvNG6.js +6 -0
  29. package/dist/assets/shield-TdB1yv_a.js +6 -0
  30. package/dist/assets/ui-BmsSg9jU.js +53 -0
  31. package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
  32. package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
  33. package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
  34. package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
  35. package/dist/assets/zap-C9wqYMpl.js +6 -0
  36. package/dist/favicon.svg +1 -0
  37. package/dist/index.html +6 -5
  38. package/dist/server/server/__tests__/data-routes.test.js +126 -0
  39. package/dist/server/server/__tests__/helpers/db.js +17 -0
  40. package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
  41. package/dist/server/server/__tests__/scope-routes.test.js +138 -0
  42. package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
  43. package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
  44. package/dist/server/server/config-migrator.js +135 -0
  45. package/dist/server/server/config.js +51 -7
  46. package/dist/server/server/database.js +21 -28
  47. package/dist/server/server/global-config.js +143 -0
  48. package/dist/server/server/index.js +118 -276
  49. package/dist/server/server/init.js +243 -225
  50. package/dist/server/server/launch.js +29 -0
  51. package/dist/server/server/manifest-types.js +8 -0
  52. package/dist/server/server/manifest.js +454 -0
  53. package/dist/server/server/migrate-legacy.js +229 -0
  54. package/dist/server/server/parsers/event-parser.js +4 -1
  55. package/dist/server/server/parsers/event-parser.test.js +117 -0
  56. package/dist/server/server/parsers/scope-parser.js +74 -28
  57. package/dist/server/server/parsers/scope-parser.test.js +230 -0
  58. package/dist/server/server/project-context.js +265 -0
  59. package/dist/server/server/project-emitter.js +41 -0
  60. package/dist/server/server/project-manager.js +297 -0
  61. package/dist/server/server/routes/aggregate-routes.js +871 -0
  62. package/dist/server/server/routes/config-routes.js +41 -90
  63. package/dist/server/server/routes/data-routes.js +25 -123
  64. package/dist/server/server/routes/dispatch-routes.js +37 -15
  65. package/dist/server/server/routes/git-routes.js +74 -0
  66. package/dist/server/server/routes/manifest-routes.js +319 -0
  67. package/dist/server/server/routes/scope-routes.js +45 -28
  68. package/dist/server/server/routes/sync-routes.js +134 -0
  69. package/dist/server/server/routes/version-routes.js +1 -15
  70. package/dist/server/server/routes/workflow-routes.js +9 -3
  71. package/dist/server/server/schema.js +3 -0
  72. package/dist/server/server/services/batch-orchestrator.js +41 -17
  73. package/dist/server/server/services/claude-session-service.js +17 -14
  74. package/dist/server/server/services/config-service.js +10 -1
  75. package/dist/server/server/services/deploy-service.test.js +119 -0
  76. package/dist/server/server/services/event-service.js +64 -1
  77. package/dist/server/server/services/event-service.test.js +191 -0
  78. package/dist/server/server/services/gate-service.test.js +105 -0
  79. package/dist/server/server/services/git-service.js +108 -4
  80. package/dist/server/server/services/github-service.js +110 -2
  81. package/dist/server/server/services/readiness-service.test.js +190 -0
  82. package/dist/server/server/services/scope-cache.js +5 -1
  83. package/dist/server/server/services/scope-cache.test.js +142 -0
  84. package/dist/server/server/services/scope-service.js +222 -131
  85. package/dist/server/server/services/scope-service.test.js +137 -0
  86. package/dist/server/server/services/sprint-orchestrator.js +29 -15
  87. package/dist/server/server/services/sprint-service.js +23 -3
  88. package/dist/server/server/services/sprint-service.test.js +238 -0
  89. package/dist/server/server/services/sync-service.js +434 -0
  90. package/dist/server/server/services/sync-types.js +2 -0
  91. package/dist/server/server/services/workflow-service.js +26 -5
  92. package/dist/server/server/services/workflow-service.test.js +159 -0
  93. package/dist/server/server/settings-sync.js +284 -0
  94. package/dist/server/server/uninstall.js +195 -0
  95. package/dist/server/server/update-planner.js +279 -0
  96. package/dist/server/server/update.js +212 -0
  97. package/dist/server/server/utils/cc-hooks-parser.js +3 -0
  98. package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
  99. package/dist/server/server/utils/dispatch-utils.js +83 -24
  100. package/dist/server/server/utils/dispatch-utils.test.js +182 -0
  101. package/dist/server/server/utils/flag-builder.js +54 -0
  102. package/dist/server/server/utils/json-fields.js +14 -0
  103. package/dist/server/server/utils/json-fields.test.js +73 -0
  104. package/dist/server/server/utils/logger.js +37 -3
  105. package/dist/server/server/utils/package-info.js +30 -0
  106. package/dist/server/server/utils/route-helpers.js +47 -0
  107. package/dist/server/server/utils/route-helpers.test.js +115 -0
  108. package/dist/server/server/utils/terminal-launcher.js +79 -25
  109. package/dist/server/server/utils/worktree-manager.js +13 -4
  110. package/dist/server/server/validator.js +230 -0
  111. package/dist/server/server/watchers/event-watcher.js +28 -13
  112. package/dist/server/server/watchers/global-watcher.js +63 -0
  113. package/dist/server/server/watchers/scope-watcher.js +27 -12
  114. package/dist/server/server/wizard/config-editor.js +237 -0
  115. package/dist/server/server/wizard/detect.js +96 -0
  116. package/dist/server/server/wizard/doctor.js +115 -0
  117. package/dist/server/server/wizard/index.js +340 -0
  118. package/dist/server/server/wizard/phases/confirm.js +39 -0
  119. package/dist/server/server/wizard/phases/project-setup.js +90 -0
  120. package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
  121. package/dist/server/server/wizard/phases/welcome.js +32 -0
  122. package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
  123. package/dist/server/server/wizard/types.js +29 -0
  124. package/dist/server/server/wizard/ui.js +73 -0
  125. package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
  126. package/dist/server/shared/api-types.js +80 -1
  127. package/dist/server/shared/default-workflow.json +65 -0
  128. package/dist/server/shared/onboarding-tour.test.js +81 -0
  129. package/dist/server/shared/project-colors.js +24 -0
  130. package/dist/server/shared/workflow-config.test.js +84 -0
  131. package/dist/server/shared/workflow-engine.js +1 -1
  132. package/dist/server/shared/workflow-engine.test.js +302 -0
  133. package/dist/server/shared/workflow-normalizer.js +101 -0
  134. package/dist/server/shared/workflow-normalizer.test.js +100 -0
  135. package/dist/server/src/components/onboarding/tour-steps.js +84 -0
  136. package/package.json +34 -29
  137. package/schemas/orbital.config.schema.json +2 -5
  138. package/scripts/postinstall.js +18 -6
  139. package/scripts/release.sh +53 -0
  140. package/server/__tests__/data-routes.test.ts +151 -0
  141. package/server/__tests__/helpers/db.ts +19 -0
  142. package/server/__tests__/helpers/mock-emitter.ts +10 -0
  143. package/server/__tests__/scope-routes.test.ts +158 -0
  144. package/server/__tests__/sprint-routes.test.ts +118 -0
  145. package/server/__tests__/workflow-routes.test.ts +120 -0
  146. package/server/config-migrator.ts +160 -0
  147. package/server/config.ts +64 -12
  148. package/server/database.ts +22 -31
  149. package/server/global-config.ts +204 -0
  150. package/server/index.ts +139 -316
  151. package/server/init.ts +266 -234
  152. package/server/launch.ts +32 -0
  153. package/server/manifest-types.ts +145 -0
  154. package/server/manifest.ts +494 -0
  155. package/server/migrate-legacy.ts +290 -0
  156. package/server/parsers/event-parser.test.ts +135 -0
  157. package/server/parsers/event-parser.ts +4 -1
  158. package/server/parsers/scope-parser.test.ts +270 -0
  159. package/server/parsers/scope-parser.ts +79 -31
  160. package/server/project-context.ts +325 -0
  161. package/server/project-emitter.ts +50 -0
  162. package/server/project-manager.ts +368 -0
  163. package/server/routes/aggregate-routes.ts +968 -0
  164. package/server/routes/config-routes.ts +43 -85
  165. package/server/routes/data-routes.ts +34 -156
  166. package/server/routes/dispatch-routes.ts +46 -17
  167. package/server/routes/git-routes.ts +77 -0
  168. package/server/routes/manifest-routes.ts +388 -0
  169. package/server/routes/scope-routes.ts +39 -30
  170. package/server/routes/sync-routes.ts +175 -0
  171. package/server/routes/version-routes.ts +1 -16
  172. package/server/routes/workflow-routes.ts +9 -3
  173. package/server/schema.ts +3 -0
  174. package/server/services/batch-orchestrator.ts +41 -17
  175. package/server/services/claude-session-service.ts +16 -14
  176. package/server/services/config-service.ts +10 -1
  177. package/server/services/deploy-service.test.ts +145 -0
  178. package/server/services/deploy-service.ts +2 -2
  179. package/server/services/event-service.test.ts +242 -0
  180. package/server/services/event-service.ts +92 -3
  181. package/server/services/gate-service.test.ts +131 -0
  182. package/server/services/gate-service.ts +2 -2
  183. package/server/services/git-service.ts +137 -4
  184. package/server/services/github-service.ts +120 -2
  185. package/server/services/readiness-service.test.ts +217 -0
  186. package/server/services/scope-cache.test.ts +167 -0
  187. package/server/services/scope-cache.ts +4 -1
  188. package/server/services/scope-service.test.ts +169 -0
  189. package/server/services/scope-service.ts +224 -130
  190. package/server/services/sprint-orchestrator.ts +30 -15
  191. package/server/services/sprint-service.test.ts +271 -0
  192. package/server/services/sprint-service.ts +29 -5
  193. package/server/services/sync-service.ts +482 -0
  194. package/server/services/sync-types.ts +77 -0
  195. package/server/services/workflow-service.test.ts +190 -0
  196. package/server/services/workflow-service.ts +29 -9
  197. package/server/settings-sync.ts +359 -0
  198. package/server/uninstall.ts +214 -0
  199. package/server/update-planner.ts +346 -0
  200. package/server/update.ts +263 -0
  201. package/server/utils/cc-hooks-parser.test.ts +96 -0
  202. package/server/utils/cc-hooks-parser.ts +4 -0
  203. package/server/utils/dispatch-utils.test.ts +245 -0
  204. package/server/utils/dispatch-utils.ts +102 -30
  205. package/server/utils/flag-builder.ts +56 -0
  206. package/server/utils/json-fields.test.ts +83 -0
  207. package/server/utils/json-fields.ts +14 -0
  208. package/server/utils/logger.ts +40 -3
  209. package/server/utils/package-info.ts +32 -0
  210. package/server/utils/route-helpers.test.ts +144 -0
  211. package/server/utils/route-helpers.ts +50 -0
  212. package/server/utils/terminal-launcher.ts +85 -25
  213. package/server/utils/worktree-manager.ts +9 -4
  214. package/server/validator.ts +270 -0
  215. package/server/watchers/event-watcher.ts +24 -12
  216. package/server/watchers/global-watcher.ts +77 -0
  217. package/server/watchers/scope-watcher.ts +21 -9
  218. package/server/wizard/config-editor.ts +248 -0
  219. package/server/wizard/detect.ts +104 -0
  220. package/server/wizard/doctor.ts +114 -0
  221. package/server/wizard/index.ts +438 -0
  222. package/server/wizard/phases/confirm.ts +45 -0
  223. package/server/wizard/phases/project-setup.ts +106 -0
  224. package/server/wizard/phases/setup-wizard.ts +78 -0
  225. package/server/wizard/phases/welcome.ts +39 -0
  226. package/server/wizard/phases/workflow-setup.ts +28 -0
  227. package/server/wizard/types.ts +56 -0
  228. package/server/wizard/ui.ts +92 -0
  229. package/shared/__fixtures__/workflow-configs.ts +80 -0
  230. package/shared/api-types.ts +106 -0
  231. package/shared/onboarding-tour.test.ts +94 -0
  232. package/shared/project-colors.ts +24 -0
  233. package/shared/workflow-config.test.ts +111 -0
  234. package/shared/workflow-config.ts +7 -0
  235. package/shared/workflow-engine.test.ts +388 -0
  236. package/shared/workflow-engine.ts +1 -1
  237. package/shared/workflow-normalizer.test.ts +119 -0
  238. package/shared/workflow-normalizer.ts +118 -0
  239. package/templates/agents/QUICK-REFERENCE.md +1 -0
  240. package/templates/agents/README.md +1 -0
  241. package/templates/agents/SKILL-TRIGGERS.md +11 -0
  242. package/templates/agents/green-team/deep-dive.md +361 -0
  243. package/templates/hooks/end-session.sh +4 -1
  244. package/templates/hooks/init-session.sh +1 -0
  245. package/templates/hooks/orbital-emit.sh +2 -2
  246. package/templates/hooks/orbital-report-deploy.sh +4 -4
  247. package/templates/hooks/orbital-report-gates.sh +4 -4
  248. package/templates/hooks/orbital-scope-update.sh +1 -1
  249. package/templates/hooks/scope-commit-logger.sh +2 -2
  250. package/templates/hooks/scope-create-cleanup.sh +2 -2
  251. package/templates/hooks/scope-create-gate.sh +2 -5
  252. package/templates/hooks/scope-gate.sh +4 -6
  253. package/templates/hooks/scope-helpers.sh +28 -1
  254. package/templates/hooks/scope-lifecycle-gate.sh +14 -5
  255. package/templates/hooks/scope-prepare.sh +67 -12
  256. package/templates/hooks/scope-transition.sh +14 -6
  257. package/templates/hooks/time-tracker.sh +2 -5
  258. package/templates/migrations/renames.json +1 -0
  259. package/templates/orbital.config.json +8 -6
  260. package/{shared/default-workflow.json → templates/presets/default.json} +65 -0
  261. package/templates/presets/development.json +4 -4
  262. package/templates/presets/gitflow.json +7 -0
  263. package/templates/prompts/README.md +23 -0
  264. package/templates/prompts/deep-dive-audit.md +94 -0
  265. package/templates/quick/rules.md +56 -5
  266. package/templates/settings-hooks.json +1 -1
  267. package/templates/skills/git-commit/SKILL.md +27 -7
  268. package/templates/skills/git-dev/SKILL.md +13 -4
  269. package/templates/skills/git-main/SKILL.md +13 -3
  270. package/templates/skills/git-production/SKILL.md +9 -2
  271. package/templates/skills/git-staging/SKILL.md +11 -3
  272. package/templates/skills/scope-create/SKILL.md +17 -3
  273. package/templates/skills/scope-fix-review/SKILL.md +14 -7
  274. package/templates/skills/scope-implement/SKILL.md +15 -4
  275. package/templates/skills/scope-post-review/SKILL.md +77 -7
  276. package/templates/skills/scope-pre-review/SKILL.md +11 -4
  277. package/templates/skills/scope-verify/SKILL.md +5 -3
  278. package/templates/skills/test-code-review/SKILL.md +41 -33
  279. package/templates/skills/test-scaffold/SKILL.md +222 -0
  280. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
  281. package/dist/assets/charts-D__PA1zp.js +0 -72
  282. package/dist/assets/index-D1G6i0nS.css +0 -1
  283. package/dist/assets/index-DpItvKpf.js +0 -419
  284. package/dist/assets/ui-BvF022GT.js +0 -53
  285. package/index.html +0 -15
  286. package/postcss.config.js +0 -6
  287. package/src/App.tsx +0 -33
  288. package/src/components/AgentBadge.tsx +0 -40
  289. package/src/components/BatchPreflightModal.tsx +0 -115
  290. package/src/components/CardDisplayToggle.tsx +0 -74
  291. package/src/components/ColumnHeaderActions.tsx +0 -55
  292. package/src/components/ColumnMenu.tsx +0 -99
  293. package/src/components/DeployHistory.tsx +0 -141
  294. package/src/components/DispatchModal.tsx +0 -164
  295. package/src/components/DispatchPopover.tsx +0 -139
  296. package/src/components/DragOverlay.tsx +0 -25
  297. package/src/components/DriftSidebar.tsx +0 -140
  298. package/src/components/EnvironmentStrip.tsx +0 -88
  299. package/src/components/ErrorBoundary.tsx +0 -62
  300. package/src/components/FilterChip.tsx +0 -105
  301. package/src/components/GateIndicator.tsx +0 -33
  302. package/src/components/IdeaDetailModal.tsx +0 -190
  303. package/src/components/IdeaFormDialog.tsx +0 -113
  304. package/src/components/KanbanColumn.tsx +0 -201
  305. package/src/components/MarkdownRenderer.tsx +0 -114
  306. package/src/components/NeonGrid.tsx +0 -128
  307. package/src/components/PromotionQueue.tsx +0 -89
  308. package/src/components/ScopeCard.tsx +0 -234
  309. package/src/components/ScopeDetailModal.tsx +0 -255
  310. package/src/components/ScopeFilterBar.tsx +0 -152
  311. package/src/components/SearchInput.tsx +0 -102
  312. package/src/components/SessionPanel.tsx +0 -335
  313. package/src/components/SprintContainer.tsx +0 -303
  314. package/src/components/SprintDependencyDialog.tsx +0 -78
  315. package/src/components/SprintPreflightModal.tsx +0 -138
  316. package/src/components/StatusBar.tsx +0 -168
  317. package/src/components/SwimCell.tsx +0 -67
  318. package/src/components/SwimLaneRow.tsx +0 -94
  319. package/src/components/SwimlaneBoardView.tsx +0 -108
  320. package/src/components/VersionBadge.tsx +0 -139
  321. package/src/components/ViewModeSelector.tsx +0 -114
  322. package/src/components/config/AgentChip.tsx +0 -53
  323. package/src/components/config/AgentCreateDialog.tsx +0 -321
  324. package/src/components/config/AgentEditor.tsx +0 -175
  325. package/src/components/config/DirectoryTree.tsx +0 -582
  326. package/src/components/config/FileEditor.tsx +0 -550
  327. package/src/components/config/HookChip.tsx +0 -50
  328. package/src/components/config/StageCard.tsx +0 -198
  329. package/src/components/config/TransitionZone.tsx +0 -173
  330. package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
  331. package/src/components/config/WorkflowPipeline.tsx +0 -161
  332. package/src/components/source-control/BranchList.tsx +0 -93
  333. package/src/components/source-control/BranchPanel.tsx +0 -105
  334. package/src/components/source-control/CommitLog.tsx +0 -100
  335. package/src/components/source-control/CommitRow.tsx +0 -47
  336. package/src/components/source-control/GitHubPanel.tsx +0 -110
  337. package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
  338. package/src/components/source-control/GitOverviewBar.tsx +0 -101
  339. package/src/components/source-control/PullRequestList.tsx +0 -69
  340. package/src/components/source-control/WorktreeList.tsx +0 -80
  341. package/src/components/ui/badge.tsx +0 -41
  342. package/src/components/ui/button.tsx +0 -55
  343. package/src/components/ui/card.tsx +0 -78
  344. package/src/components/ui/dialog.tsx +0 -94
  345. package/src/components/ui/popover.tsx +0 -33
  346. package/src/components/ui/scroll-area.tsx +0 -54
  347. package/src/components/ui/separator.tsx +0 -28
  348. package/src/components/ui/tabs.tsx +0 -52
  349. package/src/components/ui/toggle-switch.tsx +0 -35
  350. package/src/components/ui/tooltip.tsx +0 -27
  351. package/src/components/workflow/AddEdgeDialog.tsx +0 -217
  352. package/src/components/workflow/AddListDialog.tsx +0 -201
  353. package/src/components/workflow/ChecklistEditor.tsx +0 -239
  354. package/src/components/workflow/CommandPrefixManager.tsx +0 -118
  355. package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
  356. package/src/components/workflow/DirectionSelector.tsx +0 -133
  357. package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
  358. package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
  359. package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
  360. package/src/components/workflow/EditToolbar.tsx +0 -138
  361. package/src/components/workflow/HookDetailPanel.tsx +0 -250
  362. package/src/components/workflow/HookExecutionLog.tsx +0 -24
  363. package/src/components/workflow/HookSourceModal.tsx +0 -129
  364. package/src/components/workflow/HooksDashboard.tsx +0 -363
  365. package/src/components/workflow/ListPropertyEditor.tsx +0 -251
  366. package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
  367. package/src/components/workflow/MovementRulesPanel.tsx +0 -188
  368. package/src/components/workflow/NodeDetailPanel.tsx +0 -245
  369. package/src/components/workflow/PresetSelector.tsx +0 -414
  370. package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
  371. package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
  372. package/src/components/workflow/WorkflowNode.tsx +0 -147
  373. package/src/components/workflow/graphLayout.ts +0 -186
  374. package/src/components/workflow/mergeHooks.ts +0 -85
  375. package/src/components/workflow/useEditHistory.ts +0 -88
  376. package/src/components/workflow/useWorkflowEditor.ts +0 -262
  377. package/src/components/workflow/validateConfig.ts +0 -70
  378. package/src/hooks/useActiveDispatches.ts +0 -198
  379. package/src/hooks/useBoardSettings.ts +0 -170
  380. package/src/hooks/useCardDisplay.ts +0 -57
  381. package/src/hooks/useCcHooks.ts +0 -24
  382. package/src/hooks/useConfigTree.ts +0 -51
  383. package/src/hooks/useEnforcementRules.ts +0 -46
  384. package/src/hooks/useEvents.ts +0 -59
  385. package/src/hooks/useFileEditor.ts +0 -165
  386. package/src/hooks/useGates.ts +0 -57
  387. package/src/hooks/useIdeaActions.ts +0 -53
  388. package/src/hooks/useKanbanDnd.ts +0 -410
  389. package/src/hooks/useOrbitalConfig.ts +0 -54
  390. package/src/hooks/usePipeline.ts +0 -47
  391. package/src/hooks/usePipelineData.ts +0 -338
  392. package/src/hooks/useReconnect.ts +0 -25
  393. package/src/hooks/useScopeFilters.ts +0 -125
  394. package/src/hooks/useScopeSessions.ts +0 -44
  395. package/src/hooks/useScopes.ts +0 -67
  396. package/src/hooks/useSearch.ts +0 -67
  397. package/src/hooks/useSettings.tsx +0 -187
  398. package/src/hooks/useSocket.ts +0 -25
  399. package/src/hooks/useSourceControl.ts +0 -105
  400. package/src/hooks/useSprintPreflight.ts +0 -55
  401. package/src/hooks/useSprints.ts +0 -154
  402. package/src/hooks/useStatusBarHighlight.ts +0 -18
  403. package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
  404. package/src/hooks/useTheme.ts +0 -9
  405. package/src/hooks/useTransitionReadiness.ts +0 -53
  406. package/src/hooks/useVersion.ts +0 -155
  407. package/src/hooks/useViolations.ts +0 -65
  408. package/src/hooks/useWorkflow.tsx +0 -125
  409. package/src/hooks/useZoomModifier.ts +0 -19
  410. package/src/index.css +0 -797
  411. package/src/layouts/DashboardLayout.tsx +0 -113
  412. package/src/lib/collisionDetection.ts +0 -20
  413. package/src/lib/scope-fields.ts +0 -61
  414. package/src/lib/swimlane.ts +0 -146
  415. package/src/lib/utils.ts +0 -15
  416. package/src/main.tsx +0 -19
  417. package/src/socket.ts +0 -11
  418. package/src/types/index.ts +0 -497
  419. package/src/views/AgentFeed.tsx +0 -339
  420. package/src/views/DeployPipeline.tsx +0 -59
  421. package/src/views/EnforcementView.tsx +0 -378
  422. package/src/views/PrimitivesConfig.tsx +0 -500
  423. package/src/views/QualityGates.tsx +0 -1012
  424. package/src/views/ScopeBoard.tsx +0 -454
  425. package/src/views/SessionTimeline.tsx +0 -516
  426. package/src/views/Settings.tsx +0 -183
  427. package/src/views/SourceControl.tsx +0 -95
  428. package/src/views/WorkflowVisualizer.tsx +0 -382
  429. package/tailwind.config.js +0 -161
  430. package/tsconfig.json +0 -25
  431. package/vite.config.ts +0 -38
@@ -0,0 +1,871 @@
1
+ import { Router } from 'express';
2
+ import path from 'path';
3
+ import { launchInTerminal } from '../utils/terminal-launcher.js';
4
+ import { buildClaudeFlags } from '../utils/flag-builder.js';
5
+ import { DEFAULT_DISPATCH_FLAGS, DEFAULT_DISPATCH_CONFIG, validateDispatchFlags, validateDispatchConfig } from '../../shared/api-types.js';
6
+ import { getClaudeSessions, getSessionStats } from '../services/claude-session-service.js';
7
+ import { getActiveScopeIds, getAbandonedScopeIds } from '../utils/dispatch-utils.js';
8
+ import { ConfigService, isValidPrimitiveType } from '../services/config-service.js';
9
+ import { GLOBAL_PRIMITIVES_DIR } from '../global-config.js';
10
+ import { getHookEnforcement } from '../../shared/workflow-config.js';
11
+ import { parseCcHooks } from '../utils/cc-hooks-parser.js';
12
+ import { createLogger } from '../utils/logger.js';
13
+ import { parseJsonFields } from '../utils/json-fields.js';
14
+ import { loadManifest, refreshFileStatuses, summarizeManifest } from '../manifest.js';
15
+ import { getPackageVersion } from '../utils/package-info.js';
16
+ import { runUpdate } from '../init.js';
17
+ import { loadGlobalConfig, saveGlobalConfig, } from '../global-config.js';
18
+ const log = createLogger('aggregate');
19
+ export function createAggregateRoutes({ projectManager, io, syncService }) {
20
+ const router = Router();
21
+ // ─── Aggregate: Scopes & Events ──────────────────────────
22
+ router.get('/aggregate/scopes', (_req, res) => {
23
+ const allScopes = [];
24
+ for (const [projectId, ctx] of projectManager.getAllContexts()) {
25
+ for (const scope of ctx.scopeService.getAll()) {
26
+ allScopes.push({ ...scope, project_id: projectId });
27
+ }
28
+ }
29
+ res.json(allScopes);
30
+ });
31
+ router.get('/aggregate/sprints', (_req, res) => {
32
+ const allSprints = [];
33
+ for (const [projectId, ctx] of projectManager.getAllContexts()) {
34
+ for (const sprint of ctx.sprintService.getAll()) {
35
+ allSprints.push({ ...sprint, project_id: projectId });
36
+ }
37
+ }
38
+ res.json(allSprints);
39
+ });
40
+ router.get('/aggregate/events', (req, res) => {
41
+ const limit = Number(req.query.limit) || 50;
42
+ const allEvents = [];
43
+ for (const [projectId, ctx] of projectManager.getAllContexts()) {
44
+ const events = ctx.db.prepare(`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`).all(limit);
45
+ for (const event of events) {
46
+ allEvents.push({ ...event, project_id: projectId });
47
+ }
48
+ }
49
+ // Sort by timestamp descending across all projects
50
+ allEvents.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)));
51
+ res.json(allEvents.slice(0, limit));
52
+ });
53
+ // Aggregate sessions across all projects
54
+ router.get('/aggregate/sessions', (_req, res) => {
55
+ const allRows = [];
56
+ for (const [projectId, ctx] of projectManager.getAllContexts()) {
57
+ const rows = ctx.db.prepare('SELECT * FROM sessions ORDER BY started_at DESC').all();
58
+ for (const row of rows) {
59
+ allRows.push({ ...parseJsonFields(row), project_id: projectId });
60
+ }
61
+ }
62
+ // Deduplicate by claude_session_id, aggregate scope_ids and actions
63
+ const seen = new Map();
64
+ const scopeMap = new Map();
65
+ const actionMap = new Map();
66
+ for (const row of allRows) {
67
+ const key = row.claude_session_id ?? row.id;
68
+ if (!seen.has(key)) {
69
+ seen.set(key, row);
70
+ scopeMap.set(key, []);
71
+ actionMap.set(key, []);
72
+ }
73
+ const sid = row.scope_id;
74
+ if (sid != null) {
75
+ const arr = scopeMap.get(key);
76
+ if (!arr.includes(sid))
77
+ arr.push(sid);
78
+ }
79
+ const action = row.action;
80
+ if (action) {
81
+ const actions = actionMap.get(key);
82
+ if (!actions.includes(action))
83
+ actions.push(action);
84
+ }
85
+ }
86
+ const results = [...seen.values()].map((row) => {
87
+ const key = row.claude_session_id ?? row.id;
88
+ return { ...row, scope_ids: scopeMap.get(key) ?? [], actions: actionMap.get(key) ?? [] };
89
+ });
90
+ // Sort by started_at descending across all projects
91
+ results.sort((a, b) => String(b.started_at ?? '').localeCompare(String(a.started_at ?? '')));
92
+ res.json(results.slice(0, 50));
93
+ });
94
+ router.get('/aggregate/sessions/:id/content', async (req, res) => {
95
+ const sessionId = req.params.id;
96
+ // Find the session across all project databases
97
+ let session;
98
+ let matchedProjectRoot;
99
+ for (const [, ctx] of projectManager.getAllContexts()) {
100
+ const row = ctx.db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
101
+ if (row) {
102
+ session = parseJsonFields(row);
103
+ matchedProjectRoot = ctx.config.projectRoot;
104
+ break;
105
+ }
106
+ }
107
+ if (!session || !matchedProjectRoot) {
108
+ res.status(404).json({ error: 'Session not found' });
109
+ return;
110
+ }
111
+ let content = '';
112
+ let meta = null;
113
+ let stats = null;
114
+ if (session.claude_session_id && typeof session.claude_session_id === 'string') {
115
+ const claudeSessions = await getClaudeSessions(undefined, matchedProjectRoot);
116
+ const match = claudeSessions.find(s => s.id === session.claude_session_id);
117
+ if (match) {
118
+ meta = {
119
+ slug: match.slug,
120
+ branch: match.branch,
121
+ fileSize: match.fileSize,
122
+ summary: match.summary,
123
+ startedAt: match.startedAt,
124
+ lastActiveAt: match.lastActiveAt,
125
+ };
126
+ }
127
+ stats = getSessionStats(session.claude_session_id, matchedProjectRoot);
128
+ }
129
+ if (!content) {
130
+ const parts = [];
131
+ if (session.summary)
132
+ parts.push(`# ${session.summary}\n`);
133
+ const discoveries = Array.isArray(session.discoveries) ? session.discoveries : [];
134
+ if (discoveries.length > 0) {
135
+ parts.push('## Completed\n');
136
+ for (const d of discoveries)
137
+ parts.push(`- ${d}`);
138
+ parts.push('');
139
+ }
140
+ const nextSteps = Array.isArray(session.next_steps) ? session.next_steps : [];
141
+ if (nextSteps.length > 0) {
142
+ parts.push('## Next Steps\n');
143
+ for (const n of nextSteps)
144
+ parts.push(`- ${n}`);
145
+ }
146
+ content = parts.join('\n');
147
+ }
148
+ res.json({
149
+ id: session.id,
150
+ content,
151
+ claude_session_id: session.claude_session_id ?? null,
152
+ meta,
153
+ stats,
154
+ });
155
+ });
156
+ router.post('/aggregate/sessions/:id/resume', async (req, res) => {
157
+ const sessionId = req.params.id;
158
+ const { claude_session_id } = req.body;
159
+ if (!claude_session_id || !/^[0-9a-f-]{36}$/i.test(claude_session_id)) {
160
+ res.status(400).json({ error: 'Valid claude_session_id (UUID) required' });
161
+ return;
162
+ }
163
+ // Find the session's project root and config
164
+ let matchedProjectRoot;
165
+ let matchedConfig;
166
+ for (const [, ctx] of projectManager.getAllContexts()) {
167
+ const row = ctx.db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
168
+ if (row) {
169
+ matchedProjectRoot = ctx.config.projectRoot;
170
+ matchedConfig = ctx.config;
171
+ break;
172
+ }
173
+ }
174
+ if (!matchedProjectRoot || !matchedConfig) {
175
+ res.status(404).json({ error: 'Session not found' });
176
+ return;
177
+ }
178
+ const flagsStr = buildClaudeFlags(matchedConfig.claude.dispatchFlags);
179
+ const resumeCmd = `cd '${matchedProjectRoot}' && claude ${flagsStr} --resume '${claude_session_id}'`;
180
+ try {
181
+ await launchInTerminal(resumeCmd);
182
+ res.json({ ok: true, session_id: claude_session_id });
183
+ }
184
+ catch (err) {
185
+ log.error('Terminal launch failed', { error: String(err) });
186
+ res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
187
+ }
188
+ });
189
+ // ─── Aggregate: Enforcement & Gates ──────────────────────
190
+ router.get('/aggregate/events/violations/summary', (_req, res) => {
191
+ try {
192
+ const mergedByRule = new Map();
193
+ const mergedByFile = new Map();
194
+ let allOverrides = [];
195
+ let totalViolations = 0;
196
+ let totalOverrides = 0;
197
+ for (const [, ctx] of projectManager.getAllContexts()) {
198
+ const byRule = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
199
+ FROM events WHERE type = 'VIOLATION' GROUP BY rule ORDER BY count DESC`).all();
200
+ for (const r of byRule) {
201
+ const existing = mergedByRule.get(r.rule);
202
+ if (existing) {
203
+ existing.count += r.count;
204
+ if (r.last_seen > existing.last_seen)
205
+ existing.last_seen = r.last_seen;
206
+ }
207
+ else {
208
+ mergedByRule.set(r.rule, { ...r });
209
+ }
210
+ }
211
+ const byFile = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.file') as file, COUNT(*) as count FROM events
212
+ WHERE type = 'VIOLATION' AND JSON_EXTRACT(data, '$.file') IS NOT NULL AND JSON_EXTRACT(data, '$.file') != ''
213
+ GROUP BY file ORDER BY count DESC LIMIT 20`).all();
214
+ for (const f of byFile) {
215
+ const existing = mergedByFile.get(f.file);
216
+ if (existing) {
217
+ existing.count += f.count;
218
+ }
219
+ else {
220
+ mergedByFile.set(f.file, { ...f });
221
+ }
222
+ }
223
+ const overrides = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, JSON_EXTRACT(data, '$.reason') as reason, timestamp as date
224
+ FROM events WHERE type = 'OVERRIDE' ORDER BY timestamp DESC LIMIT 50`).all();
225
+ allOverrides = allOverrides.concat(overrides);
226
+ const tv = ctx.db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'VIOLATION'`).get();
227
+ const to = ctx.db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'OVERRIDE'`).get();
228
+ totalViolations += tv.count;
229
+ totalOverrides += to.count;
230
+ }
231
+ const byRule = [...mergedByRule.values()].sort((a, b) => b.count - a.count);
232
+ const byFile = [...mergedByFile.values()].sort((a, b) => b.count - a.count).slice(0, 20);
233
+ allOverrides.sort((a, b) => b.date.localeCompare(a.date));
234
+ res.json({ byRule, byFile, overrides: allOverrides.slice(0, 50), totalViolations, totalOverrides });
235
+ }
236
+ catch (err) {
237
+ log.error('Violations summary failed', { error: String(err) });
238
+ res.status(500).json({ error: 'Failed to aggregate violations summary' });
239
+ }
240
+ });
241
+ router.get('/aggregate/enforcement/rules', (_req, res) => {
242
+ try {
243
+ const hookMap = new Map();
244
+ const summary = { guards: 0, gates: 0, lifecycle: 0, observers: 0 };
245
+ const edgeIdSet = new Set();
246
+ let totalEdges = 0;
247
+ for (const [, ctx] of projectManager.getAllContexts()) {
248
+ const allHooks = ctx.workflowEngine.getAllHooks();
249
+ const allEdges = ctx.workflowEngine.getAllEdges();
250
+ // Build edge map for this project
251
+ const hookEdgeMap = new Map();
252
+ for (const edge of allEdges) {
253
+ const edgeKey = `${edge.from}->${edge.to}`;
254
+ if (!edgeIdSet.has(edgeKey)) {
255
+ edgeIdSet.add(edgeKey);
256
+ totalEdges++;
257
+ }
258
+ for (const hookId of edge.hooks ?? []) {
259
+ if (!hookEdgeMap.has(hookId))
260
+ hookEdgeMap.set(hookId, []);
261
+ hookEdgeMap.get(hookId).push({ from: edge.from, to: edge.to, label: edge.label });
262
+ }
263
+ }
264
+ // Query stats from this project's DB
265
+ const violationStats = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
266
+ FROM events WHERE type = 'VIOLATION' GROUP BY rule`).all();
267
+ const overrideStats = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
268
+ FROM events WHERE type = 'OVERRIDE' GROUP BY rule`).all();
269
+ const violationMap = new Map(violationStats.map((v) => [v.rule, v]));
270
+ const overrideMap = new Map(overrideStats.map((o) => [o.rule, o]));
271
+ for (const hook of allHooks) {
272
+ const existing = hookMap.get(hook.id);
273
+ const projViolations = violationMap.get(hook.id)?.count ?? 0;
274
+ const projOverrides = overrideMap.get(hook.id)?.count ?? 0;
275
+ const projLastTriggered = violationMap.get(hook.id)?.last_seen ?? null;
276
+ if (existing) {
277
+ // Sum stats across projects
278
+ existing.stats.violations += projViolations;
279
+ existing.stats.overrides += projOverrides;
280
+ if (projLastTriggered && (!existing.stats.last_triggered || projLastTriggered > existing.stats.last_triggered)) {
281
+ existing.stats.last_triggered = projLastTriggered;
282
+ }
283
+ // Union edges
284
+ const existingEdgeKeys = new Set(existing.edges.map((e) => `${e.from}->${e.to}`));
285
+ for (const edge of hookEdgeMap.get(hook.id) ?? []) {
286
+ if (!existingEdgeKeys.has(`${edge.from}->${edge.to}`)) {
287
+ existing.edges.push(edge);
288
+ }
289
+ }
290
+ }
291
+ else {
292
+ // First time seeing this hook — count it in summary
293
+ if (hook.category === 'guard')
294
+ summary.guards++;
295
+ else if (hook.category === 'gate')
296
+ summary.gates++;
297
+ else if (hook.category === 'lifecycle')
298
+ summary.lifecycle++;
299
+ else if (hook.category === 'observer')
300
+ summary.observers++;
301
+ hookMap.set(hook.id, {
302
+ hook,
303
+ enforcement: getHookEnforcement(hook),
304
+ edges: hookEdgeMap.get(hook.id) ?? [],
305
+ stats: {
306
+ violations: projViolations,
307
+ overrides: projOverrides,
308
+ last_triggered: projLastTriggered,
309
+ },
310
+ });
311
+ }
312
+ }
313
+ }
314
+ res.json({ summary, rules: [...hookMap.values()], totalEdges });
315
+ }
316
+ catch (err) {
317
+ log.error('Enforcement rules failed', { error: String(err) });
318
+ res.status(500).json({ error: 'Failed to aggregate enforcement rules' });
319
+ }
320
+ });
321
+ // GET /aggregate/workflow/claude-hooks — union of CC hooks across all projects
322
+ router.get('/aggregate/workflow/claude-hooks', (_req, res) => {
323
+ try {
324
+ const allHooks = [];
325
+ for (const [, ctx] of projectManager.getAllContexts()) {
326
+ const settingsPath = path.join(ctx.config.projectRoot, '.claude/settings.local.json');
327
+ allHooks.push(...parseCcHooks(settingsPath));
328
+ }
329
+ res.json({ success: true, data: allHooks });
330
+ }
331
+ catch (err) {
332
+ log.error('Aggregate claude-hooks failed', { error: String(err) });
333
+ res.status(500).json({ success: false, error: String(err) });
334
+ }
335
+ });
336
+ router.get('/aggregate/events/violations/trend', (req, res) => {
337
+ try {
338
+ const days = Number(req.query.days) || 30;
339
+ const merged = new Map();
340
+ for (const [, ctx] of projectManager.getAllContexts()) {
341
+ const trend = ctx.db.prepare(`SELECT date(timestamp) as day, JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
342
+ FROM events WHERE type = 'VIOLATION' AND timestamp >= datetime('now', ? || ' days')
343
+ GROUP BY day, rule ORDER BY day ASC`).all(`-${days}`);
344
+ for (const t of trend) {
345
+ const key = `${t.day}:${t.rule}`;
346
+ const existing = merged.get(key);
347
+ if (existing) {
348
+ existing.count += t.count;
349
+ }
350
+ else {
351
+ merged.set(key, { ...t });
352
+ }
353
+ }
354
+ }
355
+ const result = [...merged.values()].sort((a, b) => a.day.localeCompare(b.day));
356
+ res.json(result);
357
+ }
358
+ catch (err) {
359
+ log.error('Violation trends failed', { error: String(err) });
360
+ res.status(500).json({ error: 'Failed to aggregate violation trends' });
361
+ }
362
+ });
363
+ router.get('/aggregate/gates', (req, res) => {
364
+ try {
365
+ const scopeId = req.query.scope_id;
366
+ const filterProjectId = req.query.project_id;
367
+ const mergedGates = new Map();
368
+ for (const [projectId, ctx] of projectManager.getAllContexts()) {
369
+ if (filterProjectId && projectId !== filterProjectId)
370
+ continue;
371
+ const gates = scopeId
372
+ ? ctx.gateService.getLatestForScope(Number(scopeId))
373
+ : ctx.gateService.getLatestRun();
374
+ for (const gate of gates) {
375
+ const existing = mergedGates.get(gate.gate_name);
376
+ if (!existing || gate.run_at > existing.run_at) {
377
+ mergedGates.set(gate.gate_name, { ...gate, project_id: projectId });
378
+ }
379
+ }
380
+ }
381
+ res.json([...mergedGates.values()]);
382
+ }
383
+ catch (err) {
384
+ log.error('Gates aggregation failed', { error: String(err) });
385
+ res.status(500).json({ error: 'Failed to aggregate gates' });
386
+ }
387
+ });
388
+ router.get('/aggregate/gates/stats', (_req, res) => {
389
+ try {
390
+ const merged = new Map();
391
+ for (const [, ctx] of projectManager.getAllContexts()) {
392
+ const stats = ctx.gateService.getStats();
393
+ for (const s of stats) {
394
+ const existing = merged.get(s.gate_name);
395
+ if (existing) {
396
+ existing.total += s.total;
397
+ existing.passed += s.passed;
398
+ existing.failed += s.failed;
399
+ }
400
+ else {
401
+ merged.set(s.gate_name, { ...s });
402
+ }
403
+ }
404
+ }
405
+ res.json([...merged.values()]);
406
+ }
407
+ catch (err) {
408
+ log.error('Gate stats failed', { error: String(err) });
409
+ res.status(500).json({ error: 'Failed to aggregate gate stats' });
410
+ }
411
+ });
412
+ // ─── Aggregate: Git & GitHub ───────────────────────────────
413
+ router.get('/aggregate/git/overview', async (_req, res) => {
414
+ try {
415
+ const projects = projectManager.getProjectList();
416
+ const results = await Promise.allSettled(projects.filter(p => p.enabled && p.status === 'active').map(async (proj) => {
417
+ const ctx = projectManager.getContext(proj.id);
418
+ if (!ctx)
419
+ throw new Error('Project offline');
420
+ const config = ctx.workflowEngine.getConfig();
421
+ const overview = await ctx.gitService.getOverview(config.branchingMode ?? 'trunk');
422
+ return {
423
+ projectId: proj.id,
424
+ projectName: proj.name,
425
+ projectColor: proj.color,
426
+ status: 'ok',
427
+ overview,
428
+ };
429
+ }));
430
+ const overviews = results.map((r, i) => {
431
+ if (r.status === 'fulfilled')
432
+ return r.value;
433
+ const proj = projects.filter(p => p.enabled && p.status === 'active')[i];
434
+ return {
435
+ projectId: proj.id,
436
+ projectName: proj.name,
437
+ projectColor: proj.color,
438
+ status: 'error',
439
+ error: String(r.reason),
440
+ };
441
+ });
442
+ res.json(overviews);
443
+ }
444
+ catch (err) {
445
+ log.error('Git overviews failed', { error: String(err) });
446
+ res.status(500).json({ error: 'Failed to aggregate git overviews' });
447
+ }
448
+ });
449
+ router.get('/aggregate/git/commits', async (req, res) => {
450
+ try {
451
+ const limit = Number(req.query.limit) || 50;
452
+ const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
453
+ const results = await Promise.allSettled(projects.map(async (proj) => {
454
+ const ctx = projectManager.getContext(proj.id);
455
+ if (!ctx)
456
+ return [];
457
+ const commits = await ctx.gitService.getCommits({ limit });
458
+ return commits.map(c => ({
459
+ ...c,
460
+ project_id: proj.id,
461
+ projectName: proj.name,
462
+ projectColor: proj.color,
463
+ }));
464
+ }));
465
+ const allCommits = [];
466
+ for (const r of results) {
467
+ if (r.status === 'fulfilled')
468
+ allCommits.push(...r.value);
469
+ }
470
+ allCommits.sort((a, b) => String(b.date).localeCompare(String(a.date)));
471
+ res.json(allCommits.slice(0, limit));
472
+ }
473
+ catch (err) {
474
+ log.error('Commits aggregation failed', { error: String(err) });
475
+ res.status(500).json({ error: 'Failed to aggregate commits' });
476
+ }
477
+ });
478
+ router.get('/aggregate/github/prs', async (_req, res) => {
479
+ try {
480
+ const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
481
+ const results = await Promise.allSettled(projects.map(async (proj) => {
482
+ const ctx = projectManager.getContext(proj.id);
483
+ if (!ctx)
484
+ return [];
485
+ const prs = await ctx.githubService.getOpenPRs();
486
+ return prs.map(pr => ({
487
+ ...pr,
488
+ project_id: proj.id,
489
+ projectName: proj.name,
490
+ projectColor: proj.color,
491
+ }));
492
+ }));
493
+ const allPrs = [];
494
+ for (const r of results) {
495
+ if (r.status === 'fulfilled')
496
+ allPrs.push(...r.value);
497
+ }
498
+ allPrs.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
499
+ res.json(allPrs);
500
+ }
501
+ catch (err) {
502
+ log.error('PRs aggregation failed', { error: String(err) });
503
+ res.status(500).json({ error: 'Failed to aggregate PRs' });
504
+ }
505
+ });
506
+ router.get('/aggregate/git/health', async (_req, res) => {
507
+ try {
508
+ const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
509
+ const results = await Promise.allSettled(projects.map(async (proj) => {
510
+ const ctx = projectManager.getContext(proj.id);
511
+ if (!ctx)
512
+ throw new Error('offline');
513
+ const branches = await ctx.gitService.getBranches();
514
+ const config = ctx.workflowEngine.getConfig();
515
+ const listsWithBranch = config.lists.filter(l => l.gitBranch).sort((a, b) => a.order - b.order);
516
+ const driftPairs = [];
517
+ for (let i = 0; i < listsWithBranch.length - 1; i++) {
518
+ driftPairs.push({ from: listsWithBranch[i].gitBranch, to: listsWithBranch[i + 1].gitBranch });
519
+ }
520
+ const drift = driftPairs.length > 0 ? await ctx.gitService.getDrift(driftPairs) : [];
521
+ const maxDrift = Math.max(0, ...drift.map(d => d.count));
522
+ const staleBranches = branches.filter(b => b.isStale && !b.isRemote);
523
+ return {
524
+ projectId: proj.id,
525
+ projectName: proj.name,
526
+ projectColor: proj.color,
527
+ branchCount: branches.filter(b => !b.isRemote).length,
528
+ staleBranchCount: staleBranches.length,
529
+ featureBranchCount: branches.filter(b => !b.isRemote && /(?:feat|fix|scope)[/-]/.test(b.name)).length,
530
+ maxDriftSeverity: maxDrift === 0 ? 'clean' : maxDrift <= 5 ? 'low' : maxDrift <= 20 ? 'moderate' : 'high',
531
+ };
532
+ }));
533
+ const health = [];
534
+ for (const r of results) {
535
+ if (r.status === 'fulfilled')
536
+ health.push(r.value);
537
+ }
538
+ res.json(health);
539
+ }
540
+ catch (err) {
541
+ log.error('Branch health failed', { error: String(err) });
542
+ res.status(500).json({ error: 'Failed to aggregate branch health' });
543
+ }
544
+ });
545
+ router.get('/aggregate/git/activity', async (req, res) => {
546
+ try {
547
+ const days = Number(req.query.days) || 30;
548
+ const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
549
+ const results = await Promise.allSettled(projects.map(async (proj) => {
550
+ const ctx = projectManager.getContext(proj.id);
551
+ if (!ctx)
552
+ return { projectId: proj.id, series: [] };
553
+ const series = await ctx.gitService.getActivitySeries(days);
554
+ return { projectId: proj.id, projectName: proj.name, projectColor: proj.color, series };
555
+ }));
556
+ const activity = [];
557
+ for (const r of results) {
558
+ if (r.status === 'fulfilled')
559
+ activity.push(r.value);
560
+ }
561
+ res.json(activity);
562
+ }
563
+ catch (err) {
564
+ log.error('Activity aggregation failed', { error: String(err) });
565
+ res.status(500).json({ error: 'Failed to aggregate activity' });
566
+ }
567
+ });
568
+ router.get('/aggregate/scopes/:id/readiness', (req, res) => {
569
+ const scopeId = Number(req.params.id);
570
+ const projectId = req.query.project_id;
571
+ for (const [pid, ctx] of projectManager.getAllContexts()) {
572
+ if (projectId && pid !== projectId)
573
+ continue;
574
+ const scope = ctx.scopeService.getById(scopeId);
575
+ if (scope) {
576
+ const readiness = ctx.readinessService.getReadiness(scopeId);
577
+ if (readiness) {
578
+ res.json(readiness);
579
+ return;
580
+ }
581
+ }
582
+ }
583
+ res.status(404).json({ error: 'Scope not found in any project' });
584
+ });
585
+ router.get('/aggregate/dispatch/active-scopes', (_req, res) => {
586
+ const allActive = [];
587
+ const seenActive = new Set();
588
+ const allAbandoned = [];
589
+ const seenAbandoned = new Set();
590
+ for (const [projectId, ctx] of projectManager.getAllContexts()) {
591
+ const activeIds = getActiveScopeIds(ctx.db, ctx.scopeService, ctx.workflowEngine);
592
+ for (const id of activeIds) {
593
+ const key = `${projectId}::${id}`;
594
+ if (!seenActive.has(key)) {
595
+ seenActive.add(key);
596
+ allActive.push({ scope_id: id, project_id: projectId });
597
+ }
598
+ }
599
+ const abandoned = getAbandonedScopeIds(ctx.db, ctx.scopeService, ctx.workflowEngine, activeIds);
600
+ for (const entry of abandoned) {
601
+ const key = `${projectId}::${entry.scope_id}`;
602
+ if (!seenAbandoned.has(key)) {
603
+ seenAbandoned.add(key);
604
+ allAbandoned.push({ ...entry, project_id: projectId });
605
+ }
606
+ }
607
+ }
608
+ res.json({ scope_ids: allActive, abandoned_scopes: allAbandoned });
609
+ });
610
+ router.get('/aggregate/dispatch/active', (req, res) => {
611
+ const scopeId = Number(req.query.scope_id);
612
+ if (isNaN(scopeId) || scopeId <= 0) {
613
+ res.status(400).json({ error: 'Valid scope_id query param required' });
614
+ return;
615
+ }
616
+ for (const [, ctx] of projectManager.getAllContexts()) {
617
+ const scope = ctx.scopeService.getById(scopeId);
618
+ if (!scope)
619
+ continue;
620
+ const active = ctx.db.prepare(`SELECT id, timestamp, JSON_EXTRACT(data, '$.command') as command
621
+ FROM events
622
+ WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
623
+ ORDER BY timestamp DESC LIMIT 1`).get(scopeId);
624
+ res.json({ active: active ?? null });
625
+ return;
626
+ }
627
+ res.json({ active: null });
628
+ });
629
+ // ─── Aggregate: Manifest Health ────────────────────────────
630
+ router.get('/aggregate/manifest/status', (_req, res) => {
631
+ try {
632
+ const projects = projectManager.getProjectList().filter(p => p.enabled);
633
+ const pkgVersion = getPackageVersion();
634
+ const projectOverviews = projects.map((proj) => {
635
+ const ctx = projectManager.getContext(proj.id);
636
+ if (!ctx) {
637
+ return {
638
+ projectId: proj.id,
639
+ projectName: proj.name,
640
+ projectColor: proj.color,
641
+ status: 'error',
642
+ manifest: null,
643
+ error: 'Project offline',
644
+ };
645
+ }
646
+ try {
647
+ const manifest = loadManifest(ctx.config.projectRoot);
648
+ if (!manifest) {
649
+ return {
650
+ projectId: proj.id,
651
+ projectName: proj.name,
652
+ projectColor: proj.color,
653
+ status: 'no-manifest',
654
+ manifest: null,
655
+ };
656
+ }
657
+ const claudeDir = path.join(ctx.config.projectRoot, '.claude');
658
+ refreshFileStatuses(manifest, claudeDir);
659
+ const summary = summarizeManifest(manifest);
660
+ return {
661
+ projectId: proj.id,
662
+ projectName: proj.name,
663
+ projectColor: proj.color,
664
+ status: 'ok',
665
+ manifest: {
666
+ exists: true,
667
+ packageVersion: pkgVersion,
668
+ installedVersion: manifest.packageVersion,
669
+ needsUpdate: manifest.packageVersion !== pkgVersion,
670
+ preset: manifest.preset,
671
+ files: summary,
672
+ lastUpdated: manifest.updatedAt,
673
+ },
674
+ };
675
+ }
676
+ catch (err) {
677
+ return {
678
+ projectId: proj.id,
679
+ projectName: proj.name,
680
+ projectColor: proj.color,
681
+ status: 'error',
682
+ manifest: null,
683
+ error: String(err),
684
+ };
685
+ }
686
+ });
687
+ const projectsUpToDate = projectOverviews.filter(p => p.status === 'ok' && !p.manifest?.needsUpdate).length;
688
+ const projectsOutdated = projectOverviews.filter(p => p.status === 'ok' && p.manifest?.needsUpdate).length;
689
+ const noManifest = projectOverviews.filter(p => p.status === 'no-manifest').length;
690
+ const totalOutdated = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.outdated ?? 0), 0);
691
+ const totalModified = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.modified ?? 0), 0);
692
+ const totalPinned = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.pinned ?? 0), 0);
693
+ const totalMissing = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.missing ?? 0), 0);
694
+ const totalSynced = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.synced ?? 0), 0);
695
+ const totalUserOwned = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.userOwned ?? 0), 0);
696
+ res.json({
697
+ total: projects.length,
698
+ projectsUpToDate,
699
+ projectsOutdated,
700
+ noManifest,
701
+ totalOutdated,
702
+ totalModified,
703
+ totalPinned,
704
+ totalMissing,
705
+ totalSynced,
706
+ totalUserOwned,
707
+ projects: projectOverviews,
708
+ });
709
+ }
710
+ catch (err) {
711
+ log.error('Manifest status failed', { error: String(err) });
712
+ res.status(500).json({ error: 'Failed to aggregate manifest status' });
713
+ }
714
+ });
715
+ router.post('/aggregate/manifest/update-all', (_req, res) => {
716
+ try {
717
+ const projects = projectManager.getProjectList().filter(p => p.enabled);
718
+ const pkgVersion = getPackageVersion();
719
+ const results = [];
720
+ for (const proj of projects) {
721
+ const ctx = projectManager.getContext(proj.id);
722
+ if (!ctx) {
723
+ results.push({ projectId: proj.id, success: false, error: 'Project offline' });
724
+ continue;
725
+ }
726
+ const manifest = loadManifest(ctx.config.projectRoot);
727
+ if (!manifest)
728
+ continue; // uninitialized — skip
729
+ // Refresh statuses and check if there's anything to update
730
+ const claudeDir = path.join(ctx.config.projectRoot, '.claude');
731
+ refreshFileStatuses(manifest, claudeDir);
732
+ const manifestSummary = summarizeManifest(manifest);
733
+ if (manifest.packageVersion === pkgVersion && manifestSummary.outdated === 0 && manifestSummary.missing === 0) {
734
+ continue; // fully up to date
735
+ }
736
+ try {
737
+ runUpdate(ctx.config.projectRoot, { dryRun: false });
738
+ ctx.emitter.emit('manifest:changed', { action: 'updated' });
739
+ results.push({ projectId: proj.id, success: true });
740
+ }
741
+ catch (err) {
742
+ results.push({ projectId: proj.id, success: false, error: String(err) });
743
+ }
744
+ }
745
+ res.json({ success: true, results });
746
+ }
747
+ catch (err) {
748
+ log.error('Update all projects failed', { error: String(err) });
749
+ res.status(500).json({ error: 'Failed to update all projects' });
750
+ }
751
+ });
752
+ // ─── Global: Dispatch Config ────────────────────────────────
753
+ // Dispatch settings are global — stored in ~/.orbital/config.json.
754
+ // Changes propagate to all active projects' in-memory config.
755
+ router.get('/aggregate/config/dispatch-flags', (_req, res) => {
756
+ const global = loadGlobalConfig();
757
+ res.json({ success: true, data: global.dispatchFlags ?? DEFAULT_DISPATCH_FLAGS });
758
+ });
759
+ router.put('/aggregate/config/dispatch-flags', (req, res) => {
760
+ const updates = req.body;
761
+ const error = validateDispatchFlags(updates);
762
+ if (error) {
763
+ res.status(400).json({ success: false, error });
764
+ return;
765
+ }
766
+ const global = loadGlobalConfig();
767
+ const merged = { ...(global.dispatchFlags ?? DEFAULT_DISPATCH_FLAGS), ...updates };
768
+ global.dispatchFlags = merged;
769
+ saveGlobalConfig(global);
770
+ for (const [, ctx] of projectManager.getAllContexts()) {
771
+ ctx.config.claude.dispatchFlags = merged;
772
+ }
773
+ res.json({ success: true, data: merged });
774
+ });
775
+ router.get('/aggregate/config/dispatch-settings', (_req, res) => {
776
+ const global = loadGlobalConfig();
777
+ res.json({
778
+ success: true,
779
+ data: { ...(global.dispatch ?? DEFAULT_DISPATCH_CONFIG), terminalAdapter: global.terminalAdapter ?? 'auto' },
780
+ });
781
+ });
782
+ router.put('/aggregate/config/dispatch-settings', (req, res) => {
783
+ const { terminalAdapter, ...dispatchUpdates } = req.body;
784
+ const error = validateDispatchConfig({ ...dispatchUpdates, terminalAdapter });
785
+ if (error) {
786
+ res.status(400).json({ success: false, error });
787
+ return;
788
+ }
789
+ const global = loadGlobalConfig();
790
+ const mergedDispatch = { ...(global.dispatch ?? DEFAULT_DISPATCH_CONFIG), ...dispatchUpdates };
791
+ global.dispatch = mergedDispatch;
792
+ if (terminalAdapter)
793
+ global.terminalAdapter = terminalAdapter;
794
+ saveGlobalConfig(global);
795
+ for (const [, ctx] of projectManager.getAllContexts()) {
796
+ ctx.config.dispatch = mergedDispatch;
797
+ if (terminalAdapter)
798
+ ctx.config.terminal.adapter = terminalAdapter;
799
+ }
800
+ res.json({ success: true, data: { ...mergedDispatch, terminalAdapter: global.terminalAdapter ?? 'auto' } });
801
+ });
802
+ // ─── Aggregate: Config Primitives (Global) ────────────────
803
+ // In aggregate mode, config reads/writes target ~/.orbital/primitives/
804
+ // Writes propagate to all synced (non-overridden) projects via SyncService.
805
+ const globalConfigService = new ConfigService(GLOBAL_PRIMITIVES_DIR);
806
+ router.get('/aggregate/config/:type/tree', (req, res) => {
807
+ const type = req.params.type;
808
+ if (!isValidPrimitiveType(type)) {
809
+ res.status(400).json({ success: false, error: `Invalid type "${type}". Must be one of: agents, skills, hooks` });
810
+ return;
811
+ }
812
+ try {
813
+ const basePath = path.join(GLOBAL_PRIMITIVES_DIR, type);
814
+ const tree = globalConfigService.scanDirectory(basePath);
815
+ res.json({ success: true, data: tree });
816
+ }
817
+ catch (err) {
818
+ log.error('Config tree read failed', { type, error: String(err) });
819
+ res.status(500).json({ success: false, error: 'Failed to read global config tree' });
820
+ }
821
+ });
822
+ router.get('/aggregate/config/:type/file', (req, res) => {
823
+ const type = req.params.type;
824
+ if (!isValidPrimitiveType(type)) {
825
+ res.status(400).json({ success: false, error: `Invalid type "${type}". Must be one of: agents, skills, hooks` });
826
+ return;
827
+ }
828
+ const filePath = req.query.path;
829
+ if (!filePath) {
830
+ res.status(400).json({ success: false, error: 'path query parameter is required' });
831
+ return;
832
+ }
833
+ try {
834
+ const basePath = path.join(GLOBAL_PRIMITIVES_DIR, type);
835
+ const content = globalConfigService.readFile(basePath, filePath);
836
+ res.json({ success: true, data: { path: filePath, content } });
837
+ }
838
+ catch (err) {
839
+ const msg = err instanceof Error ? err.message : String(err);
840
+ const status = msg.includes('traversal') ? 403 : msg.includes('ENOENT') || msg.includes('not found') ? 404 : 500;
841
+ res.status(status).json({ success: false, error: msg });
842
+ }
843
+ });
844
+ router.put('/aggregate/config/:type/file', (req, res) => {
845
+ const type = req.params.type;
846
+ if (!isValidPrimitiveType(type)) {
847
+ res.status(400).json({ success: false, error: `Invalid type "${type}". Must be one of: agents, skills, hooks` });
848
+ return;
849
+ }
850
+ const { path: filePath, content } = req.body;
851
+ if (!filePath || content === undefined) {
852
+ res.status(400).json({ success: false, error: 'path and content are required' });
853
+ return;
854
+ }
855
+ try {
856
+ const basePath = path.join(GLOBAL_PRIMITIVES_DIR, type);
857
+ globalConfigService.writeFile(basePath, filePath, content);
858
+ // Propagate to all synced projects
859
+ const relativePath = path.join(type, filePath);
860
+ const result = syncService.propagateGlobalChange(relativePath);
861
+ io.emit(`config:${type}:changed`, { action: 'updated', path: filePath, global: true });
862
+ res.json({ success: true, propagation: result });
863
+ }
864
+ catch (err) {
865
+ const msg = err instanceof Error ? err.message : String(err);
866
+ const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : 500;
867
+ res.status(status).json({ success: false, error: msg });
868
+ }
869
+ });
870
+ return router;
871
+ }