orbital-command 0.1.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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/bin/orbital.js +362 -0
  4. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
  5. package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
  6. package/dist/assets/charts-D__PA1zp.js +72 -0
  7. package/dist/assets/index-D1G6i0nS.css +1 -0
  8. package/dist/assets/index-DpItvKpf.js +419 -0
  9. package/dist/assets/ui-BvF022GT.js +53 -0
  10. package/dist/assets/vendor-Dzv9lrRc.js +59 -0
  11. package/dist/index.html +19 -0
  12. package/dist/scanner-sweep.png +0 -0
  13. package/dist/server/server/adapters/index.js +34 -0
  14. package/dist/server/server/adapters/iterm2-adapter.js +29 -0
  15. package/dist/server/server/adapters/subprocess-adapter.js +21 -0
  16. package/dist/server/server/adapters/terminal-adapter.js +1 -0
  17. package/dist/server/server/config.js +156 -0
  18. package/dist/server/server/database.js +90 -0
  19. package/dist/server/server/index.js +372 -0
  20. package/dist/server/server/init.js +811 -0
  21. package/dist/server/server/parsers/event-parser.js +64 -0
  22. package/dist/server/server/parsers/scope-parser.js +188 -0
  23. package/dist/server/server/routes/config-routes.js +163 -0
  24. package/dist/server/server/routes/data-routes.js +461 -0
  25. package/dist/server/server/routes/dispatch-routes.js +215 -0
  26. package/dist/server/server/routes/git-routes.js +92 -0
  27. package/dist/server/server/routes/scope-routes.js +215 -0
  28. package/dist/server/server/routes/sprint-routes.js +116 -0
  29. package/dist/server/server/routes/version-routes.js +130 -0
  30. package/dist/server/server/routes/workflow-routes.js +185 -0
  31. package/dist/server/server/schema.js +90 -0
  32. package/dist/server/server/services/batch-orchestrator.js +253 -0
  33. package/dist/server/server/services/claude-session-service.js +352 -0
  34. package/dist/server/server/services/config-service.js +132 -0
  35. package/dist/server/server/services/deploy-service.js +51 -0
  36. package/dist/server/server/services/event-service.js +63 -0
  37. package/dist/server/server/services/gate-service.js +83 -0
  38. package/dist/server/server/services/git-service.js +309 -0
  39. package/dist/server/server/services/github-service.js +145 -0
  40. package/dist/server/server/services/readiness-service.js +184 -0
  41. package/dist/server/server/services/scope-cache.js +72 -0
  42. package/dist/server/server/services/scope-service.js +424 -0
  43. package/dist/server/server/services/sprint-orchestrator.js +312 -0
  44. package/dist/server/server/services/sprint-service.js +293 -0
  45. package/dist/server/server/services/workflow-service.js +397 -0
  46. package/dist/server/server/utils/cc-hooks-parser.js +49 -0
  47. package/dist/server/server/utils/dispatch-utils.js +305 -0
  48. package/dist/server/server/utils/logger.js +86 -0
  49. package/dist/server/server/utils/terminal-launcher.js +388 -0
  50. package/dist/server/server/utils/worktree-manager.js +98 -0
  51. package/dist/server/server/watchers/event-watcher.js +81 -0
  52. package/dist/server/server/watchers/scope-watcher.js +33 -0
  53. package/dist/server/shared/api-types.js +5 -0
  54. package/dist/server/shared/default-workflow.json +616 -0
  55. package/dist/server/shared/workflow-config.js +44 -0
  56. package/dist/server/shared/workflow-engine.js +353 -0
  57. package/index.html +15 -0
  58. package/package.json +110 -0
  59. package/postcss.config.js +6 -0
  60. package/schemas/orbital.config.schema.json +83 -0
  61. package/scripts/postinstall.js +24 -0
  62. package/scripts/start.sh +20 -0
  63. package/server/adapters/index.ts +41 -0
  64. package/server/adapters/iterm2-adapter.ts +37 -0
  65. package/server/adapters/subprocess-adapter.ts +25 -0
  66. package/server/adapters/terminal-adapter.ts +24 -0
  67. package/server/config.ts +234 -0
  68. package/server/database.ts +107 -0
  69. package/server/index.ts +452 -0
  70. package/server/init.ts +891 -0
  71. package/server/parsers/event-parser.ts +74 -0
  72. package/server/parsers/scope-parser.ts +240 -0
  73. package/server/routes/config-routes.ts +182 -0
  74. package/server/routes/data-routes.ts +548 -0
  75. package/server/routes/dispatch-routes.ts +275 -0
  76. package/server/routes/git-routes.ts +112 -0
  77. package/server/routes/scope-routes.ts +262 -0
  78. package/server/routes/sprint-routes.ts +142 -0
  79. package/server/routes/version-routes.ts +156 -0
  80. package/server/routes/workflow-routes.ts +198 -0
  81. package/server/schema.ts +90 -0
  82. package/server/services/batch-orchestrator.ts +286 -0
  83. package/server/services/claude-session-service.ts +441 -0
  84. package/server/services/config-service.ts +151 -0
  85. package/server/services/deploy-service.ts +98 -0
  86. package/server/services/event-service.ts +98 -0
  87. package/server/services/gate-service.ts +126 -0
  88. package/server/services/git-service.ts +391 -0
  89. package/server/services/github-service.ts +183 -0
  90. package/server/services/readiness-service.ts +250 -0
  91. package/server/services/scope-cache.ts +81 -0
  92. package/server/services/scope-service.ts +476 -0
  93. package/server/services/sprint-orchestrator.ts +361 -0
  94. package/server/services/sprint-service.ts +415 -0
  95. package/server/services/workflow-service.ts +461 -0
  96. package/server/utils/cc-hooks-parser.ts +70 -0
  97. package/server/utils/dispatch-utils.ts +395 -0
  98. package/server/utils/logger.ts +109 -0
  99. package/server/utils/terminal-launcher.ts +462 -0
  100. package/server/utils/worktree-manager.ts +104 -0
  101. package/server/watchers/event-watcher.ts +100 -0
  102. package/server/watchers/scope-watcher.ts +38 -0
  103. package/shared/api-types.ts +20 -0
  104. package/shared/default-workflow.json +616 -0
  105. package/shared/workflow-config.ts +170 -0
  106. package/shared/workflow-engine.ts +427 -0
  107. package/src/App.tsx +33 -0
  108. package/src/components/AgentBadge.tsx +40 -0
  109. package/src/components/BatchPreflightModal.tsx +115 -0
  110. package/src/components/CardDisplayToggle.tsx +74 -0
  111. package/src/components/ColumnHeaderActions.tsx +55 -0
  112. package/src/components/ColumnMenu.tsx +99 -0
  113. package/src/components/DeployHistory.tsx +141 -0
  114. package/src/components/DispatchModal.tsx +164 -0
  115. package/src/components/DispatchPopover.tsx +139 -0
  116. package/src/components/DragOverlay.tsx +25 -0
  117. package/src/components/DriftSidebar.tsx +140 -0
  118. package/src/components/EnvironmentStrip.tsx +88 -0
  119. package/src/components/ErrorBoundary.tsx +62 -0
  120. package/src/components/FilterChip.tsx +105 -0
  121. package/src/components/GateIndicator.tsx +33 -0
  122. package/src/components/IdeaDetailModal.tsx +190 -0
  123. package/src/components/IdeaFormDialog.tsx +113 -0
  124. package/src/components/KanbanColumn.tsx +201 -0
  125. package/src/components/MarkdownRenderer.tsx +114 -0
  126. package/src/components/NeonGrid.tsx +128 -0
  127. package/src/components/PromotionQueue.tsx +89 -0
  128. package/src/components/ScopeCard.tsx +234 -0
  129. package/src/components/ScopeDetailModal.tsx +255 -0
  130. package/src/components/ScopeFilterBar.tsx +152 -0
  131. package/src/components/SearchInput.tsx +102 -0
  132. package/src/components/SessionPanel.tsx +335 -0
  133. package/src/components/SprintContainer.tsx +303 -0
  134. package/src/components/SprintDependencyDialog.tsx +78 -0
  135. package/src/components/SprintPreflightModal.tsx +138 -0
  136. package/src/components/StatusBar.tsx +168 -0
  137. package/src/components/SwimCell.tsx +67 -0
  138. package/src/components/SwimLaneRow.tsx +94 -0
  139. package/src/components/SwimlaneBoardView.tsx +108 -0
  140. package/src/components/VersionBadge.tsx +139 -0
  141. package/src/components/ViewModeSelector.tsx +114 -0
  142. package/src/components/config/AgentChip.tsx +53 -0
  143. package/src/components/config/AgentCreateDialog.tsx +321 -0
  144. package/src/components/config/AgentEditor.tsx +175 -0
  145. package/src/components/config/DirectoryTree.tsx +582 -0
  146. package/src/components/config/FileEditor.tsx +550 -0
  147. package/src/components/config/HookChip.tsx +50 -0
  148. package/src/components/config/StageCard.tsx +198 -0
  149. package/src/components/config/TransitionZone.tsx +173 -0
  150. package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
  151. package/src/components/config/WorkflowPipeline.tsx +161 -0
  152. package/src/components/source-control/BranchList.tsx +93 -0
  153. package/src/components/source-control/BranchPanel.tsx +105 -0
  154. package/src/components/source-control/CommitLog.tsx +100 -0
  155. package/src/components/source-control/CommitRow.tsx +47 -0
  156. package/src/components/source-control/GitHubPanel.tsx +110 -0
  157. package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
  158. package/src/components/source-control/GitOverviewBar.tsx +101 -0
  159. package/src/components/source-control/PullRequestList.tsx +69 -0
  160. package/src/components/source-control/WorktreeList.tsx +80 -0
  161. package/src/components/ui/badge.tsx +41 -0
  162. package/src/components/ui/button.tsx +55 -0
  163. package/src/components/ui/card.tsx +78 -0
  164. package/src/components/ui/dialog.tsx +94 -0
  165. package/src/components/ui/popover.tsx +33 -0
  166. package/src/components/ui/scroll-area.tsx +54 -0
  167. package/src/components/ui/separator.tsx +28 -0
  168. package/src/components/ui/tabs.tsx +52 -0
  169. package/src/components/ui/toggle-switch.tsx +35 -0
  170. package/src/components/ui/tooltip.tsx +27 -0
  171. package/src/components/workflow/AddEdgeDialog.tsx +217 -0
  172. package/src/components/workflow/AddListDialog.tsx +201 -0
  173. package/src/components/workflow/ChecklistEditor.tsx +239 -0
  174. package/src/components/workflow/CommandPrefixManager.tsx +118 -0
  175. package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
  176. package/src/components/workflow/DirectionSelector.tsx +133 -0
  177. package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
  178. package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
  179. package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
  180. package/src/components/workflow/EditToolbar.tsx +138 -0
  181. package/src/components/workflow/HookDetailPanel.tsx +250 -0
  182. package/src/components/workflow/HookExecutionLog.tsx +24 -0
  183. package/src/components/workflow/HookSourceModal.tsx +129 -0
  184. package/src/components/workflow/HooksDashboard.tsx +363 -0
  185. package/src/components/workflow/ListPropertyEditor.tsx +251 -0
  186. package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
  187. package/src/components/workflow/MovementRulesPanel.tsx +188 -0
  188. package/src/components/workflow/NodeDetailPanel.tsx +245 -0
  189. package/src/components/workflow/PresetSelector.tsx +414 -0
  190. package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
  191. package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
  192. package/src/components/workflow/WorkflowNode.tsx +147 -0
  193. package/src/components/workflow/graphLayout.ts +186 -0
  194. package/src/components/workflow/mergeHooks.ts +85 -0
  195. package/src/components/workflow/useEditHistory.ts +88 -0
  196. package/src/components/workflow/useWorkflowEditor.ts +262 -0
  197. package/src/components/workflow/validateConfig.ts +70 -0
  198. package/src/hooks/useActiveDispatches.ts +198 -0
  199. package/src/hooks/useBoardSettings.ts +170 -0
  200. package/src/hooks/useCardDisplay.ts +57 -0
  201. package/src/hooks/useCcHooks.ts +24 -0
  202. package/src/hooks/useConfigTree.ts +51 -0
  203. package/src/hooks/useEnforcementRules.ts +46 -0
  204. package/src/hooks/useEvents.ts +59 -0
  205. package/src/hooks/useFileEditor.ts +165 -0
  206. package/src/hooks/useGates.ts +57 -0
  207. package/src/hooks/useIdeaActions.ts +53 -0
  208. package/src/hooks/useKanbanDnd.ts +410 -0
  209. package/src/hooks/useOrbitalConfig.ts +54 -0
  210. package/src/hooks/usePipeline.ts +47 -0
  211. package/src/hooks/usePipelineData.ts +338 -0
  212. package/src/hooks/useReconnect.ts +25 -0
  213. package/src/hooks/useScopeFilters.ts +125 -0
  214. package/src/hooks/useScopeSessions.ts +44 -0
  215. package/src/hooks/useScopes.ts +67 -0
  216. package/src/hooks/useSearch.ts +67 -0
  217. package/src/hooks/useSettings.tsx +187 -0
  218. package/src/hooks/useSocket.ts +25 -0
  219. package/src/hooks/useSourceControl.ts +105 -0
  220. package/src/hooks/useSprintPreflight.ts +55 -0
  221. package/src/hooks/useSprints.ts +154 -0
  222. package/src/hooks/useStatusBarHighlight.ts +18 -0
  223. package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
  224. package/src/hooks/useTheme.ts +9 -0
  225. package/src/hooks/useTransitionReadiness.ts +53 -0
  226. package/src/hooks/useVersion.ts +155 -0
  227. package/src/hooks/useViolations.ts +65 -0
  228. package/src/hooks/useWorkflow.tsx +125 -0
  229. package/src/hooks/useZoomModifier.ts +19 -0
  230. package/src/index.css +797 -0
  231. package/src/layouts/DashboardLayout.tsx +113 -0
  232. package/src/lib/collisionDetection.ts +20 -0
  233. package/src/lib/scope-fields.ts +61 -0
  234. package/src/lib/swimlane.ts +146 -0
  235. package/src/lib/utils.ts +15 -0
  236. package/src/main.tsx +19 -0
  237. package/src/socket.ts +11 -0
  238. package/src/types/index.ts +497 -0
  239. package/src/views/AgentFeed.tsx +339 -0
  240. package/src/views/DeployPipeline.tsx +59 -0
  241. package/src/views/EnforcementView.tsx +378 -0
  242. package/src/views/PrimitivesConfig.tsx +500 -0
  243. package/src/views/QualityGates.tsx +1012 -0
  244. package/src/views/ScopeBoard.tsx +454 -0
  245. package/src/views/SessionTimeline.tsx +516 -0
  246. package/src/views/Settings.tsx +183 -0
  247. package/src/views/SourceControl.tsx +95 -0
  248. package/src/views/WorkflowVisualizer.tsx +382 -0
  249. package/tailwind.config.js +161 -0
  250. package/templates/agents/AUTO-INVOKE.md +180 -0
  251. package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
  252. package/templates/agents/QUICK-REFERENCE.md +122 -0
  253. package/templates/agents/README.md +188 -0
  254. package/templates/agents/SKILL-TRIGGERS.md +100 -0
  255. package/templates/agents/blue-team/frontend-designer.md +424 -0
  256. package/templates/agents/green-team/architect.md +526 -0
  257. package/templates/agents/green-team/rules-enforcer.md +131 -0
  258. package/templates/agents/red-team/attacker-learned.md +24 -0
  259. package/templates/agents/red-team/attacker.md +486 -0
  260. package/templates/agents/red-team/chaos.md +548 -0
  261. package/templates/agents/reference/component-registry.md +82 -0
  262. package/templates/agents/workflows/full-mode.md +218 -0
  263. package/templates/agents/workflows/quick-mode.md +118 -0
  264. package/templates/agents/workflows/security-mode.md +283 -0
  265. package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
  266. package/templates/config/agent-triggers.json +92 -0
  267. package/templates/hooks/agent-team-gate.sh +31 -0
  268. package/templates/hooks/agent-trigger.sh +97 -0
  269. package/templates/hooks/block-push.sh +66 -0
  270. package/templates/hooks/block-workarounds.sh +61 -0
  271. package/templates/hooks/blocker-check.sh +28 -0
  272. package/templates/hooks/completion-checklist.sh +28 -0
  273. package/templates/hooks/decision-capture.sh +15 -0
  274. package/templates/hooks/dependency-check.sh +27 -0
  275. package/templates/hooks/end-session.sh +31 -0
  276. package/templates/hooks/exploration-logger.sh +37 -0
  277. package/templates/hooks/files-changed-summary.sh +37 -0
  278. package/templates/hooks/get-session-id.sh +49 -0
  279. package/templates/hooks/git-commit-guard.sh +34 -0
  280. package/templates/hooks/init-session.sh +93 -0
  281. package/templates/hooks/orbital-emit.sh +79 -0
  282. package/templates/hooks/orbital-report-deploy.sh +78 -0
  283. package/templates/hooks/orbital-report-gates.sh +40 -0
  284. package/templates/hooks/orbital-report-violation.sh +36 -0
  285. package/templates/hooks/orbital-scope-update.sh +53 -0
  286. package/templates/hooks/phase-verify-reminder.sh +26 -0
  287. package/templates/hooks/review-gate-check.sh +82 -0
  288. package/templates/hooks/scope-commit-logger.sh +37 -0
  289. package/templates/hooks/scope-create-cleanup.sh +36 -0
  290. package/templates/hooks/scope-create-gate.sh +80 -0
  291. package/templates/hooks/scope-create-tracker.sh +17 -0
  292. package/templates/hooks/scope-file-sync.sh +53 -0
  293. package/templates/hooks/scope-gate.sh +35 -0
  294. package/templates/hooks/scope-helpers.sh +188 -0
  295. package/templates/hooks/scope-lifecycle-gate.sh +139 -0
  296. package/templates/hooks/scope-prepare.sh +244 -0
  297. package/templates/hooks/scope-transition.sh +172 -0
  298. package/templates/hooks/session-enforcer.sh +143 -0
  299. package/templates/hooks/time-tracker.sh +33 -0
  300. package/templates/lessons-learned.md +15 -0
  301. package/templates/orbital.config.json +35 -0
  302. package/templates/presets/development.json +42 -0
  303. package/templates/presets/gitflow.json +712 -0
  304. package/templates/presets/minimal.json +23 -0
  305. package/templates/quick/rules.md +218 -0
  306. package/templates/scopes/_template.md +255 -0
  307. package/templates/settings-hooks.json +98 -0
  308. package/templates/skills/git-commit/SKILL.md +85 -0
  309. package/templates/skills/git-dev/SKILL.md +99 -0
  310. package/templates/skills/git-hotfix/SKILL.md +223 -0
  311. package/templates/skills/git-main/SKILL.md +84 -0
  312. package/templates/skills/git-production/SKILL.md +165 -0
  313. package/templates/skills/git-staging/SKILL.md +112 -0
  314. package/templates/skills/scope-create/SKILL.md +81 -0
  315. package/templates/skills/scope-fix-review/SKILL.md +168 -0
  316. package/templates/skills/scope-implement/SKILL.md +110 -0
  317. package/templates/skills/scope-post-review/SKILL.md +144 -0
  318. package/templates/skills/scope-pre-review/SKILL.md +211 -0
  319. package/templates/skills/scope-verify/SKILL.md +201 -0
  320. package/templates/skills/session-init/SKILL.md +62 -0
  321. package/templates/skills/session-resume/SKILL.md +201 -0
  322. package/templates/skills/test-checks/SKILL.md +171 -0
  323. package/templates/skills/test-code-review/SKILL.md +252 -0
  324. package/tsconfig.json +25 -0
  325. package/vite.config.ts +38 -0
@@ -0,0 +1,185 @@
1
+ import { Router } from 'express';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { parseCcHooks } from '../utils/cc-hooks-parser.js';
5
+ export function createWorkflowRoutes({ workflowService, projectRoot }) {
6
+ const router = Router();
7
+ // GET /workflow — returns active config
8
+ router.get('/workflow', (_req, res) => {
9
+ try {
10
+ res.json({ success: true, data: workflowService.getActive() });
11
+ }
12
+ catch (err) {
13
+ res.status(500).json({ success: false, error: errMsg(err) });
14
+ }
15
+ });
16
+ // PUT /workflow — validate and update active config
17
+ router.put('/workflow', (req, res) => {
18
+ try {
19
+ const config = req.body;
20
+ const result = workflowService.updateActive(config);
21
+ if (!result.valid) {
22
+ res.status(400).json({ success: false, error: 'Validation failed', data: result });
23
+ return;
24
+ }
25
+ res.json({ success: true, data: result });
26
+ }
27
+ catch (err) {
28
+ res.status(500).json({ success: false, error: errMsg(err) });
29
+ }
30
+ });
31
+ // GET /workflow/presets — list all presets
32
+ router.get('/workflow/presets', (_req, res) => {
33
+ try {
34
+ res.json({ success: true, data: workflowService.listPresets() });
35
+ }
36
+ catch (err) {
37
+ res.status(500).json({ success: false, error: errMsg(err) });
38
+ }
39
+ });
40
+ // POST /workflow/presets — save current config as named preset
41
+ router.post('/workflow/presets', (req, res) => {
42
+ try {
43
+ const { name } = req.body;
44
+ if (!name) {
45
+ res.status(400).json({ success: false, error: 'name is required' });
46
+ return;
47
+ }
48
+ workflowService.savePreset(name);
49
+ res.json({ success: true });
50
+ }
51
+ catch (err) {
52
+ const msg = errMsg(err);
53
+ const status = msg.includes('Cannot overwrite') || msg.includes('must be') ? 400 : 500;
54
+ res.status(status).json({ success: false, error: msg });
55
+ }
56
+ });
57
+ // GET /workflow/presets/:name — get specific preset
58
+ router.get('/workflow/presets/:name', (req, res) => {
59
+ try {
60
+ const config = workflowService.getPreset(req.params.name);
61
+ res.json({ success: true, data: config });
62
+ }
63
+ catch (err) {
64
+ const msg = errMsg(err);
65
+ const status = msg.includes('not found') ? 404 : 500;
66
+ res.status(status).json({ success: false, error: msg });
67
+ }
68
+ });
69
+ // DELETE /workflow/presets/:name — delete preset
70
+ router.delete('/workflow/presets/:name', (req, res) => {
71
+ try {
72
+ workflowService.deletePreset(req.params.name);
73
+ res.json({ success: true });
74
+ }
75
+ catch (err) {
76
+ const msg = errMsg(err);
77
+ const status = msg.includes('Cannot delete') ? 400 : msg.includes('not found') ? 404 : 500;
78
+ res.status(status).json({ success: false, error: msg });
79
+ }
80
+ });
81
+ // GET /workflow/hooks — returns all hooks with edge mapping
82
+ router.get('/workflow/hooks', (_req, res) => {
83
+ try {
84
+ const engine = workflowService.getEngine();
85
+ const hooks = engine.getAllHooks();
86
+ const edgeHookMap = {};
87
+ for (const edge of engine.getAllEdges()) {
88
+ if (edge.hooks && edge.hooks.length > 0) {
89
+ edgeHookMap[`${edge.from}:${edge.to}`] = edge.hooks;
90
+ }
91
+ }
92
+ res.json({ success: true, data: { hooks, edgeHookMap } });
93
+ }
94
+ catch (err) {
95
+ res.status(500).json({ success: false, error: errMsg(err) });
96
+ }
97
+ });
98
+ // POST /workflow/preview — dry-run migration preview
99
+ router.post('/workflow/preview', (req, res) => {
100
+ try {
101
+ const config = req.body;
102
+ const plan = workflowService.previewMigration(config);
103
+ res.json({ success: true, data: plan });
104
+ }
105
+ catch (err) {
106
+ res.status(500).json({ success: false, error: errMsg(err) });
107
+ }
108
+ });
109
+ // GET /workflow/hooks/:id/source — read hook source file
110
+ router.get('/workflow/hooks/:id/source', async (req, res) => {
111
+ try {
112
+ const hookId = req.params.id;
113
+ const engine = workflowService.getEngine();
114
+ const hook = engine.getAllHooks().find((h) => h.id === hookId);
115
+ if (!hook) {
116
+ res.status(404).json({ success: false, error: `Hook '${hookId}' not found` });
117
+ return;
118
+ }
119
+ if (hook.target.includes('..')) {
120
+ res.status(400).json({ success: false, error: 'Invalid hook target path' });
121
+ return;
122
+ }
123
+ const filePath = path.resolve(projectRoot, hook.target);
124
+ const content = await readFile(filePath, 'utf-8');
125
+ const lineCount = content.split('\n').length;
126
+ res.json({ success: true, data: { hookId, filePath: hook.target, content, lineCount } });
127
+ }
128
+ catch (err) {
129
+ const msg = errMsg(err);
130
+ const status = msg.includes('ENOENT') ? 404 : 500;
131
+ res.status(status).json({ success: false, error: msg });
132
+ }
133
+ });
134
+ // GET /workflow/claude-hooks — returns all Claude Code hooks from settings.local.json
135
+ router.get('/workflow/claude-hooks', (_req, res) => {
136
+ try {
137
+ const settingsPath = path.resolve(projectRoot, '.claude/settings.local.json');
138
+ const data = parseCcHooks(settingsPath);
139
+ res.json({ success: true, data });
140
+ }
141
+ catch (err) {
142
+ res.status(500).json({ success: false, error: errMsg(err) });
143
+ }
144
+ });
145
+ // GET /workflow/hooks/source — read any hook source file by path
146
+ router.get('/workflow/hooks/source', async (req, res) => {
147
+ try {
148
+ const hookPath = req.query.path;
149
+ if (!hookPath) {
150
+ res.status(400).json({ success: false, error: 'path query parameter is required' });
151
+ return;
152
+ }
153
+ if (hookPath.includes('..')) {
154
+ res.status(400).json({ success: false, error: 'Invalid path: directory traversal not allowed' });
155
+ return;
156
+ }
157
+ const filePath = path.resolve(projectRoot, hookPath);
158
+ const content = await readFile(filePath, 'utf-8');
159
+ const lineCount = content.split('\n').length;
160
+ res.json({ success: true, data: { filePath: hookPath, content, lineCount } });
161
+ }
162
+ catch (err) {
163
+ const msg = errMsg(err);
164
+ const status = msg.includes('ENOENT') ? 404 : 500;
165
+ res.status(status).json({ success: false, error: msg });
166
+ }
167
+ });
168
+ // POST /workflow/apply — apply new config with orphan mappings
169
+ router.post('/workflow/apply', (req, res) => {
170
+ try {
171
+ const { config, orphanMappings } = req.body;
172
+ const plan = workflowService.applyMigration(config, orphanMappings ?? {});
173
+ res.json({ success: true, data: plan });
174
+ }
175
+ catch (err) {
176
+ const msg = errMsg(err);
177
+ const status = msg.includes('Missing orphan') || msg.includes('Validation failed') || msg.includes('not a valid') ? 400 : 500;
178
+ res.status(status).json({ success: false, error: msg });
179
+ }
180
+ });
181
+ return router;
182
+ }
183
+ function errMsg(err) {
184
+ return err instanceof Error ? err.message : String(err);
185
+ }
@@ -0,0 +1,90 @@
1
+ export const SCHEMA_DDL = `
2
+ -- Events from hooks/watchers
3
+ CREATE TABLE IF NOT EXISTS events (
4
+ id TEXT PRIMARY KEY,
5
+ type TEXT NOT NULL,
6
+ scope_id INTEGER,
7
+ session_id TEXT,
8
+ agent TEXT,
9
+ data TEXT DEFAULT '{}',
10
+ timestamp TEXT NOT NULL,
11
+ processed INTEGER DEFAULT 0
12
+ );
13
+
14
+ -- Quality gate results per scope
15
+ CREATE TABLE IF NOT EXISTS quality_gates (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ scope_id INTEGER,
18
+ gate_name TEXT NOT NULL,
19
+ status TEXT NOT NULL,
20
+ details TEXT,
21
+ duration_ms INTEGER,
22
+ run_at TEXT NOT NULL,
23
+ commit_sha TEXT
24
+ );
25
+
26
+ -- Deployment tracking
27
+ CREATE TABLE IF NOT EXISTS deployments (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ environment TEXT NOT NULL,
30
+ status TEXT NOT NULL,
31
+ commit_sha TEXT,
32
+ branch TEXT,
33
+ pr_number INTEGER,
34
+ health_check_url TEXT,
35
+ started_at TEXT,
36
+ completed_at TEXT,
37
+ details TEXT DEFAULT '{}'
38
+ );
39
+
40
+ -- Session history
41
+ CREATE TABLE IF NOT EXISTS sessions (
42
+ id TEXT PRIMARY KEY,
43
+ scope_id INTEGER,
44
+ claude_session_id TEXT,
45
+ action TEXT,
46
+ started_at TEXT,
47
+ ended_at TEXT,
48
+ handoff_file TEXT,
49
+ summary TEXT,
50
+ discoveries TEXT DEFAULT '[]',
51
+ next_steps TEXT DEFAULT '[]',
52
+ progress_pct INTEGER
53
+ );
54
+
55
+ -- Sprint containers for batching scope dispatch
56
+ CREATE TABLE IF NOT EXISTS sprints (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ name TEXT NOT NULL,
59
+ status TEXT NOT NULL DEFAULT 'assembling',
60
+ concurrency_cap INTEGER NOT NULL DEFAULT 5,
61
+ created_at TEXT NOT NULL,
62
+ updated_at TEXT NOT NULL,
63
+ dispatched_at TEXT,
64
+ completed_at TEXT,
65
+ dispatch_meta TEXT DEFAULT '{}'
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS sprint_scopes (
69
+ sprint_id INTEGER NOT NULL,
70
+ scope_id INTEGER NOT NULL,
71
+ layer INTEGER,
72
+ dispatch_status TEXT NOT NULL DEFAULT 'pending',
73
+ dispatched_at TEXT,
74
+ completed_at TEXT,
75
+ error TEXT,
76
+ PRIMARY KEY (sprint_id, scope_id),
77
+ FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE CASCADE
78
+ );
79
+
80
+ -- Indexes for common queries
81
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
82
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
83
+ CREATE INDEX IF NOT EXISTS idx_events_scope_id ON events(scope_id);
84
+ CREATE INDEX IF NOT EXISTS idx_gates_scope_id ON quality_gates(scope_id);
85
+ CREATE INDEX IF NOT EXISTS idx_gates_run_at ON quality_gates(run_at);
86
+ CREATE INDEX IF NOT EXISTS idx_deployments_env ON deployments(environment);
87
+ CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope_id);
88
+ CREATE INDEX IF NOT EXISTS idx_sprints_status ON sprints(status);
89
+ CREATE INDEX IF NOT EXISTS idx_sprint_scopes_sprint ON sprint_scopes(sprint_id);
90
+ `;
@@ -0,0 +1,253 @@
1
+ import { launchInCategorizedTerminal, escapeForAnsiC, snapshotSessionPids, discoverNewSession, isSessionPidAlive } from '../utils/terminal-launcher.js';
2
+ import { linkPidToDispatch, resolveDispatchEvent } from '../utils/dispatch-utils.js';
3
+ import { getConfig } from '../config.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+ const log = createLogger('batch');
6
+ const VALID_MERGE_MODES = ['push', 'pr'];
7
+ // ─── Orchestrator ───────────────────────────────────────────
8
+ export class BatchOrchestrator {
9
+ db;
10
+ io;
11
+ sprintService;
12
+ scopeService;
13
+ engine;
14
+ constructor(db, io, sprintService, scopeService, engine) {
15
+ this.db = db;
16
+ this.io = io;
17
+ this.sprintService = sprintService;
18
+ this.scopeService = scopeService;
19
+ this.engine = engine;
20
+ }
21
+ /** Dispatch a batch — validates constraints and routes to column-specific handler */
22
+ async dispatch(batchId, mergeMode) {
23
+ const batch = this.sprintService.getById(batchId);
24
+ if (!batch)
25
+ return { ok: false, error: 'Batch not found' };
26
+ if (batch.group_type !== 'batch')
27
+ return { ok: false, error: 'Not a batch group' };
28
+ if (batch.status !== 'assembling')
29
+ return { ok: false, error: `Batch status is '${batch.status}', expected 'assembling'` };
30
+ if (batch.scope_ids.length === 0)
31
+ return { ok: false, error: 'Batch has no scopes' };
32
+ // W-4: One active batch per column
33
+ const existingActive = this.sprintService.findActiveBatchForColumn(batch.target_column);
34
+ if (existingActive && existingActive.id !== batchId) {
35
+ return { ok: false, error: `Column '${batch.target_column}' already has an active batch (ID: ${existingActive.id})` };
36
+ }
37
+ const command = this.engine.getBatchCommand(batch.target_column);
38
+ if (!command)
39
+ return { ok: false, error: `No dispatch command for column '${batch.target_column}'` };
40
+ // Mark batch as dispatched
41
+ this.sprintService.updateStatus(batchId, 'dispatched');
42
+ log.info('Batch dispatched', { id: batchId, target_column: batch.target_column, scope_ids: batch.scope_ids });
43
+ // Build scope IDs env var prefix (W-1: prepend to command, not process.env)
44
+ const scopeIdsStr = batch.scope_ids.join(',');
45
+ const mergeModeStr = VALID_MERGE_MODES.includes(mergeMode ?? '') ? mergeMode : 'push';
46
+ // Record DISPATCH event
47
+ const eventId = crypto.randomUUID();
48
+ const eventData = {
49
+ command,
50
+ batch_id: batchId,
51
+ scope_ids: batch.scope_ids,
52
+ target_column: batch.target_column,
53
+ batch: true,
54
+ resolved: null,
55
+ };
56
+ this.db.prepare(`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
57
+ VALUES (?, 'DISPATCH', NULL, NULL, 'batch-orchestrator', ?, ?)`).run(eventId, JSON.stringify(eventData), new Date().toISOString());
58
+ this.io.emit('event:new', {
59
+ id: eventId, type: 'DISPATCH', scope_id: null,
60
+ session_id: null, agent: 'batch-orchestrator',
61
+ data: eventData, timestamp: new Date().toISOString(),
62
+ });
63
+ // Launch single CLI session with BATCH_SCOPE_IDS prepended to command
64
+ const escaped = escapeForAnsiC(command);
65
+ const fullCmd = `cd '${getConfig().projectRoot}' && BATCH_SCOPE_IDS='${scopeIdsStr}' MERGE_MODE='${mergeModeStr}' claude --dangerously-skip-permissions $'${escaped}'`;
66
+ const beforePids = snapshotSessionPids(getConfig().projectRoot);
67
+ try {
68
+ await launchInCategorizedTerminal(command, fullCmd);
69
+ // Store dispatch result timestamp
70
+ this.sprintService.updateDispatchResult(batchId, {
71
+ dispatched_at: new Date().toISOString(),
72
+ });
73
+ // Fire-and-forget: discover session PID and link to dispatch
74
+ discoverNewSession(getConfig().projectRoot, beforePids)
75
+ .then((session) => {
76
+ if (!session)
77
+ return;
78
+ linkPidToDispatch(this.db, eventId, session.pid);
79
+ // Store PID on the batch for two-phase completion
80
+ const currentResult = this.sprintService.getById(batchId)?.dispatch_result ?? {};
81
+ this.sprintService.updateDispatchResult(batchId, {
82
+ ...currentResult,
83
+ dispatched_at: currentResult.dispatched_at ?? new Date().toISOString(),
84
+ });
85
+ // Store PID in event data for later liveness checking
86
+ const row = this.db.prepare('SELECT data FROM events WHERE id = ?').get(eventId);
87
+ if (row) {
88
+ const data = JSON.parse(row.data);
89
+ data.pid = session.pid;
90
+ this.db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
91
+ }
92
+ })
93
+ .catch(err => log.error('PID discovery failed', { error: err.message }));
94
+ return { ok: true };
95
+ }
96
+ catch (err) {
97
+ this.sprintService.updateStatus(batchId, 'failed');
98
+ resolveDispatchEvent(this.db, this.io, eventId, 'failed', String(err));
99
+ return { ok: false, error: `Failed to launch terminal: ${err}` };
100
+ }
101
+ }
102
+ /** Called when a scope reaches a new status — check if it satisfies a batch,
103
+ * or remove the scope from the batch if its status diverged from the target. */
104
+ onScopeStatusChanged(scopeId, newStatus) {
105
+ // Find any active batch containing this scope
106
+ const match = this.sprintService.findActiveSprintForScope(scopeId);
107
+ if (!match)
108
+ return;
109
+ const batch = this.sprintService.getById(match.sprint_id);
110
+ if (!batch || batch.group_type !== 'batch')
111
+ return;
112
+ const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
113
+ if (newStatus === expectedStatus || this.engine.isTerminalStatus(newStatus)) {
114
+ this.sprintService.updateScopeStatus(batch.id, scopeId, 'completed');
115
+ // Check if all scopes have transitioned
116
+ if (batch.status === 'dispatched') {
117
+ this.sprintService.updateStatus(batch.id, 'in_progress');
118
+ }
119
+ }
120
+ else if (newStatus !== batch.target_column) {
121
+ // Scope diverged from batch target — remove it from the batch
122
+ this.sprintService.forceRemoveScope(batch.id, scopeId);
123
+ // If batch is now empty, mark it as failed
124
+ const remaining = this.sprintService.getSprintScopes(batch.id);
125
+ if (remaining.length === 0 && batch.status !== 'assembling') {
126
+ this.sprintService.updateStatus(batch.id, 'failed');
127
+ }
128
+ }
129
+ }
130
+ /** Called when a dispatched session PID dies — second phase of two-phase completion.
131
+ * Reverts un-transitioned scopes to their pre-dispatch status. */
132
+ onSessionPidDied(batchId) {
133
+ const batch = this.sprintService.getById(batchId);
134
+ if (!batch || batch.group_type !== 'batch')
135
+ return;
136
+ if (batch.status !== 'dispatched' && batch.status !== 'in_progress')
137
+ return;
138
+ const scopes = this.sprintService.getSprintScopes(batchId);
139
+ const allTransitioned = scopes.every((ss) => ss.dispatch_status === 'completed');
140
+ if (allTransitioned) {
141
+ this.sprintService.updateStatus(batchId, 'completed');
142
+ }
143
+ else {
144
+ const pending = scopes.filter((ss) => ss.dispatch_status !== 'completed').map((ss) => ss.scope_id);
145
+ this.sprintService.updateStatus(batchId, 'failed');
146
+ // Mark un-transitioned scopes as failed and revert their status
147
+ for (const scopeId of pending) {
148
+ this.sprintService.updateScopeStatus(batchId, scopeId, 'failed', 'Session exited before scope transitioned');
149
+ // Revert scope to pre-dispatch status (the batch's source column)
150
+ this.scopeService.updateStatus(scopeId, batch.target_column, 'rollback');
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Resolve stale batches — catches batches stuck due to lost PIDs, Orbital downtime, or
156
+ * missing PID records. Unlike recoverActiveBatches (which focuses on PID polling),
157
+ * this also resolves batches where no PID was ever recorded.
158
+ */
159
+ resolveStaleBatches() {
160
+ const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
161
+ const active = this.db.prepare(`SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`).all();
162
+ if (active.length > 0) {
163
+ log.debug('Checking stale batches', { activeCount: active.length });
164
+ }
165
+ let resolved = 0;
166
+ for (const { id } of active) {
167
+ const batch = this.sprintService.getById(id);
168
+ if (!batch)
169
+ continue;
170
+ const scopes = this.sprintService.getSprintScopes(id);
171
+ const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
172
+ // Phase 1: auto-complete scopes that reached or passed target status
173
+ for (const ss of scopes) {
174
+ if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
175
+ const scope = this.scopeService.getById(ss.scope_id);
176
+ if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
177
+ this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
178
+ }
179
+ }
180
+ }
181
+ // Phase 2: check PID liveness (check both unresolved and resolved events —
182
+ // SESSION_END may have resolved the dispatch event before we get here)
183
+ const dispatchEvent = this.db.prepare(`SELECT data FROM events
184
+ WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
185
+ ORDER BY timestamp DESC LIMIT 1`).get(id);
186
+ let pidDead = false;
187
+ if (dispatchEvent) {
188
+ const data = JSON.parse(dispatchEvent.data);
189
+ // If the dispatch event is already resolved, the session is definitely done
190
+ if (data.resolved != null) {
191
+ pidDead = true;
192
+ }
193
+ else if (typeof data.pid === 'number') {
194
+ pidDead = !isSessionPidAlive(data.pid);
195
+ }
196
+ else {
197
+ // No PID recorded — check if batch is old enough to consider stale
198
+ const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
199
+ pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
200
+ }
201
+ }
202
+ else {
203
+ // No dispatch event at all — check age
204
+ const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
205
+ pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
206
+ }
207
+ if (pidDead) {
208
+ this.onSessionPidDied(id);
209
+ resolved++;
210
+ }
211
+ }
212
+ return resolved;
213
+ }
214
+ /** Recover active batches after server restart (W-3) */
215
+ async recoverActiveBatches() {
216
+ const active = this.db.prepare(`SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`).all();
217
+ if (active.length > 0) {
218
+ log.debug('Recovering active batches', { count: active.length });
219
+ }
220
+ for (const { id } of active) {
221
+ const batch = this.sprintService.getById(id);
222
+ if (!batch)
223
+ continue;
224
+ const scopes = this.sprintService.getSprintScopes(id);
225
+ const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
226
+ // Check if scopes reached or passed target status while server was down
227
+ for (const ss of scopes) {
228
+ if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
229
+ const scope = this.scopeService.getById(ss.scope_id);
230
+ if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
231
+ this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
232
+ }
233
+ }
234
+ }
235
+ // Check if dispatch PID is still alive (include resolved events —
236
+ // SESSION_END may have resolved the dispatch before server restart)
237
+ const dispatchEvent = this.db.prepare(`SELECT data FROM events
238
+ WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
239
+ ORDER BY timestamp DESC LIMIT 1`).get(id);
240
+ if (dispatchEvent) {
241
+ const data = JSON.parse(dispatchEvent.data);
242
+ if (data.resolved != null) {
243
+ // Dispatch already resolved — session is done
244
+ this.onSessionPidDied(id);
245
+ }
246
+ else if (typeof data.pid === 'number' && !isSessionPidAlive(data.pid)) {
247
+ // PID is dead — trigger two-phase completion check
248
+ this.onSessionPidDied(id);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }