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,275 @@
1
+ import { Router } from 'express';
2
+ import type Database from 'better-sqlite3';
3
+ import type { Server } from 'socket.io';
4
+ import type { ScopeService } from '../services/scope-service.js';
5
+ import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
6
+ import { resolveDispatchEvent, resolveAbandonedDispatchesForScope, getActiveScopeIds, getAbandonedScopeIds, linkPidToDispatch } from '../utils/dispatch-utils.js';
7
+ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
8
+ import { createLogger } from '../utils/logger.js';
9
+
10
+ const log = createLogger('dispatch');
11
+
12
+ const MAX_BATCH_SIZE = 20;
13
+
14
+ interface DispatchBody {
15
+ scope_id?: number;
16
+ command: string;
17
+ prompt?: string;
18
+ transition?: { from: string; to: string };
19
+ }
20
+
21
+ interface DispatchRouteDeps {
22
+ db: Database.Database;
23
+ io: Server;
24
+ scopeService: ScopeService;
25
+ projectRoot: string;
26
+ engine: WorkflowEngine;
27
+ }
28
+
29
+ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine }: DispatchRouteDeps): Router {
30
+ const router = Router();
31
+
32
+ router.get('/dispatch/active-scopes', (_req, res) => {
33
+ const scope_ids = getActiveScopeIds(db, scopeService, engine);
34
+ const abandoned_scopes = getAbandonedScopeIds(db, scopeService, engine, scope_ids);
35
+ res.json({ scope_ids, abandoned_scopes });
36
+ });
37
+
38
+ router.get('/dispatch/active', (req, res) => {
39
+ const scopeId = Number(req.query.scope_id);
40
+ if (isNaN(scopeId) || scopeId <= 0) {
41
+ res.status(400).json({ error: 'Valid scope_id query param required' });
42
+ return;
43
+ }
44
+ const active = db.prepare(
45
+ `SELECT id, timestamp, JSON_EXTRACT(data, '$.command') as command
46
+ FROM events
47
+ WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
48
+ ORDER BY timestamp DESC LIMIT 1`
49
+ ).get(scopeId) as { id: string; timestamp: string; command: string } | undefined;
50
+
51
+ res.json({ active: active ?? null });
52
+ });
53
+
54
+ router.post('/dispatch', async (req, res) => {
55
+ const { scope_id, command, prompt, transition } = req.body as DispatchBody;
56
+
57
+ if (!command || !engine.isAllowedCommand(command)) {
58
+ res.status(400).json({ error: 'Command must start with /scope-, /git-, /test-, or /session-' });
59
+ return;
60
+ }
61
+
62
+ // W-11: Validate prompt field against allowed command prefixes
63
+ if (prompt && !engine.isAllowedCommand(prompt)) {
64
+ res.status(400).json({ error: 'Prompt must start with /scope-, /git-, /test-, or /session-' });
65
+ return;
66
+ }
67
+
68
+ // Active session guard
69
+ if (scope_id != null) {
70
+ const active = db.prepare(
71
+ `SELECT id FROM events
72
+ WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
73
+ ORDER BY timestamp DESC LIMIT 1`
74
+ ).get(scope_id) as { id: string } | undefined;
75
+
76
+ if (active) {
77
+ res.status(409).json({ error: 'Active dispatch exists', dispatch_id: active.id });
78
+ return;
79
+ }
80
+ }
81
+
82
+ // Update scope status if transition provided
83
+ if (scope_id != null && transition?.to) {
84
+ const result = scopeService.updateStatus(scope_id, transition.to, 'dispatch');
85
+ if (!result.ok) {
86
+ res.status(400).json({ error: result.error });
87
+ return;
88
+ }
89
+ }
90
+
91
+ // Record DISPATCH event
92
+ const eventId = crypto.randomUUID();
93
+ const eventData = { command, transition: transition ?? null, resolved: null };
94
+ db.prepare(
95
+ `INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
96
+ VALUES (?, 'DISPATCH', ?, NULL, 'dashboard', ?, ?)`
97
+ ).run(eventId, scope_id ?? null, JSON.stringify(eventData), new Date().toISOString());
98
+
99
+ io.emit('event:new', {
100
+ id: eventId, type: 'DISPATCH', scope_id: scope_id ?? null,
101
+ session_id: null, agent: 'dashboard', data: eventData,
102
+ timestamp: new Date().toISOString(),
103
+ });
104
+
105
+ // Build scope-aware session name before launch
106
+ const scope = scope_id != null ? scopeService.getById(scope_id) : undefined;
107
+ const sessionName = buildSessionName({ scopeId: scope_id ?? undefined, title: scope?.title, command });
108
+ const beforePids = snapshotSessionPids(projectRoot);
109
+
110
+ // Launch in iTerm — interactive TUI mode (no -p) for full visibility
111
+ const promptText = prompt ?? command;
112
+ const escaped = escapeForAnsiC(promptText);
113
+ const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions $'${escaped}'`;
114
+ try {
115
+ await launchInCategorizedTerminal(command, fullCmd, sessionName);
116
+ res.json({ ok: true, dispatch_id: eventId, scope_id: scope_id ?? null });
117
+
118
+ // Fire-and-forget: discover session PID, link to dispatch, and rename
119
+ discoverNewSession(projectRoot, beforePids)
120
+ .then((session) => {
121
+ if (!session) return;
122
+ linkPidToDispatch(db, eventId, session.pid);
123
+ if (sessionName) renameSession(projectRoot, session.sessionId, sessionName);
124
+ })
125
+ .catch(err => log.error('PID discovery failed', { error: err.message }));
126
+ } catch (err) {
127
+ if (scope_id != null && transition?.from) {
128
+ scopeService.updateStatus(scope_id, transition.from, 'rollback');
129
+ }
130
+ resolveDispatchEvent(db, io, eventId, 'failed', String(err));
131
+ res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
132
+ }
133
+ });
134
+
135
+ router.post('/dispatch/:id/resolve', (req, res) => {
136
+ const eventId = req.params.id;
137
+ const row = db.prepare('SELECT id FROM events WHERE id = ? AND type = ?')
138
+ .get(eventId, 'DISPATCH') as { id: string } | undefined;
139
+
140
+ if (!row) {
141
+ res.status(404).json({ error: 'Dispatch event not found' });
142
+ return;
143
+ }
144
+
145
+ resolveDispatchEvent(db, io, eventId, 'completed');
146
+ res.json({ ok: true, dispatch_id: eventId });
147
+ });
148
+
149
+ /** Recover an abandoned scope by reverting it to its pre-dispatch status. */
150
+ router.post('/dispatch/recover/:scopeId', (req, res) => {
151
+ try {
152
+ const scopeId = Number(req.params.scopeId);
153
+ if (isNaN(scopeId) || scopeId <= 0) {
154
+ res.status(400).json({ error: 'Valid scopeId required' });
155
+ return;
156
+ }
157
+
158
+ const { from_status } = req.body as { from_status?: string };
159
+ if (!from_status) {
160
+ res.status(400).json({ error: 'from_status is required' });
161
+ return;
162
+ }
163
+
164
+ // Revert scope to its pre-dispatch status
165
+ const result = scopeService.updateStatus(scopeId, from_status, 'rollback');
166
+ if (!result.ok) {
167
+ res.status(400).json({ error: result.error });
168
+ return;
169
+ }
170
+
171
+ resolveAbandonedDispatchesForScope(db, io, scopeId);
172
+ res.json({ ok: true, scope_id: scopeId, reverted_to: from_status });
173
+ } catch (err) {
174
+ log.error('Error recovering scope', { error: String(err) });
175
+ res.status(500).json({ error: 'Internal server error', details: String(err) });
176
+ }
177
+ });
178
+
179
+ /** Dismiss abandoned state without reverting scope status. */
180
+ router.post('/dispatch/dismiss-abandoned/:scopeId', (req, res) => {
181
+ try {
182
+ const scopeId = Number(req.params.scopeId);
183
+ if (isNaN(scopeId) || scopeId <= 0) {
184
+ res.status(400).json({ error: 'Valid scopeId required' });
185
+ return;
186
+ }
187
+
188
+ const dismissed = resolveAbandonedDispatchesForScope(db, io, scopeId);
189
+ res.json({ ok: true, scope_id: scopeId, dismissed });
190
+ } catch (err) {
191
+ log.error('Error dismissing abandoned dispatches', { error: String(err) });
192
+ res.status(500).json({ error: 'Internal server error', details: String(err) });
193
+ }
194
+ });
195
+
196
+ router.post('/dispatch/batch', async (req, res) => {
197
+ const { scope_ids, command, transition } = req.body as {
198
+ scope_ids: number[];
199
+ command: string;
200
+ transition?: { from: string; to: string };
201
+ };
202
+
203
+ if (!command || !engine.isAllowedCommand(command)) {
204
+ res.status(400).json({ error: 'Command must start with /scope-, /git-, /test-, or /session-' });
205
+ return;
206
+ }
207
+
208
+ if (!Array.isArray(scope_ids) || scope_ids.length === 0) {
209
+ res.status(400).json({ error: 'scope_ids must be a non-empty array' });
210
+ return;
211
+ }
212
+
213
+ // W-12: Validate batch size and scope ID types
214
+ if (scope_ids.length > MAX_BATCH_SIZE) {
215
+ res.status(400).json({ error: `Maximum batch size is ${MAX_BATCH_SIZE}` });
216
+ return;
217
+ }
218
+ if (!scope_ids.every(id => Number.isInteger(id) && id > 0)) {
219
+ res.status(400).json({ error: 'scope_ids must contain positive integers' });
220
+ return;
221
+ }
222
+
223
+ // Update all scope statuses
224
+ if (transition?.to) {
225
+ for (const id of scope_ids) {
226
+ const result = scopeService.updateStatus(id, transition.to, 'dispatch');
227
+ if (!result.ok) {
228
+ res.status(400).json({ error: `Scope ${id}: ${result.error}` });
229
+ return;
230
+ }
231
+ }
232
+ }
233
+
234
+ // Record single DISPATCH event for the batch
235
+ const eventId = crypto.randomUUID();
236
+ const eventData = { command, transition: transition ?? null, scope_ids, batch: true, resolved: null };
237
+ db.prepare(
238
+ `INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
239
+ VALUES (?, 'DISPATCH', NULL, NULL, 'dashboard', ?, ?)`
240
+ ).run(eventId, JSON.stringify(eventData), new Date().toISOString());
241
+
242
+ io.emit('event:new', {
243
+ id: eventId, type: 'DISPATCH', scope_id: null,
244
+ session_id: null, agent: 'dashboard', data: eventData,
245
+ timestamp: new Date().toISOString(),
246
+ });
247
+
248
+ // Launch single CLI session
249
+ const batchEscaped = escapeForAnsiC(command);
250
+ const beforePids = snapshotSessionPids(projectRoot);
251
+ const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions -p $'${batchEscaped}'`;
252
+ try {
253
+ await launchInCategorizedTerminal(command, fullCmd);
254
+ res.json({ ok: true, dispatch_id: eventId, scope_ids });
255
+
256
+ // Fire-and-forget: discover session PID and link to dispatch
257
+ discoverNewSession(projectRoot, beforePids)
258
+ .then((session) => {
259
+ if (!session) return;
260
+ linkPidToDispatch(db, eventId, session.pid);
261
+ })
262
+ .catch(err => log.error('Batch PID discovery failed', { error: err.message }));
263
+ } catch (err) {
264
+ if (transition?.from) {
265
+ for (const id of scope_ids) {
266
+ scopeService.updateStatus(id, transition.from, 'rollback');
267
+ }
268
+ }
269
+ resolveDispatchEvent(db, io, eventId, 'failed', String(err));
270
+ res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
271
+ }
272
+ });
273
+
274
+ return router;
275
+ }
@@ -0,0 +1,112 @@
1
+ import { Router } from 'express';
2
+ import type { GitService } from '../services/git-service.js';
3
+ import type { GitHubService } from '../services/github-service.js';
4
+ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
5
+
6
+ interface GitRoutesDeps {
7
+ gitService: GitService;
8
+ githubService: GitHubService;
9
+ engine: WorkflowEngine;
10
+ }
11
+
12
+ export function createGitRoutes({ gitService, githubService, engine }: GitRoutesDeps): Router {
13
+ const router = Router();
14
+
15
+ // ─── Git Overview ──────────────────────────────────────────
16
+
17
+ router.get('/git/overview', async (_req, res) => {
18
+ try {
19
+ const config = engine.getConfig();
20
+ const branchingMode = config.branchingMode ?? 'trunk';
21
+ const overview = await gitService.getOverview(branchingMode);
22
+ res.json(overview);
23
+ } catch (err) {
24
+ res.status(500).json({ error: 'Failed to get git overview', details: String(err) });
25
+ }
26
+ });
27
+
28
+ // ─── Commits ──────────────────────────────────────────────
29
+
30
+ router.get('/git/commits', async (req, res) => {
31
+ try {
32
+ const branch = (req.query.branch as string) || undefined;
33
+ const limit = Number(req.query.limit) || 50;
34
+ const offset = Number(req.query.offset) || 0;
35
+ const commits = await gitService.getCommits({ branch, limit, offset });
36
+ res.json(commits);
37
+ } catch (err) {
38
+ res.status(500).json({ error: 'Failed to get commits', details: String(err) });
39
+ }
40
+ });
41
+
42
+ // ─── Branches ──────────────────────────────────────────────
43
+
44
+ router.get('/git/branches', async (_req, res) => {
45
+ try {
46
+ const branches = await gitService.getBranches();
47
+ res.json(branches);
48
+ } catch (err) {
49
+ res.status(500).json({ error: 'Failed to get branches', details: String(err) });
50
+ }
51
+ });
52
+
53
+ // ─── Enhanced Worktrees ────────────────────────────────────
54
+
55
+ router.get('/git/worktrees', async (_req, res) => {
56
+ try {
57
+ const worktrees = await gitService.getEnhancedWorktrees();
58
+ res.json(worktrees);
59
+ } catch (err) {
60
+ res.status(500).json({ error: 'Failed to get worktrees', details: String(err) });
61
+ }
62
+ });
63
+
64
+ // ─── Dynamic Drift ─────────────────────────────────────────
65
+
66
+ router.get('/git/drift', async (_req, res) => {
67
+ try {
68
+ // Build drift pairs from workflow lists that have gitBranch set
69
+ const config = engine.getConfig();
70
+ const listsWithBranch = config.lists
71
+ .filter(l => l.gitBranch)
72
+ .sort((a, b) => a.order - b.order);
73
+
74
+ const pairs: Array<{ from: string; to: string }> = [];
75
+ for (let i = 0; i < listsWithBranch.length - 1; i++) {
76
+ pairs.push({
77
+ from: listsWithBranch[i].gitBranch!,
78
+ to: listsWithBranch[i + 1].gitBranch!,
79
+ });
80
+ }
81
+
82
+ const drift = await gitService.getDrift(pairs);
83
+ res.json(drift);
84
+ } catch (err) {
85
+ res.status(500).json({ error: 'Failed to compute drift', details: String(err) });
86
+ }
87
+ });
88
+
89
+ // ─── GitHub Status ─────────────────────────────────────────
90
+
91
+ router.get('/github/status', async (_req, res) => {
92
+ try {
93
+ const status = await githubService.getStatus();
94
+ res.json(status);
95
+ } catch (err) {
96
+ res.status(500).json({ error: 'Failed to get GitHub status', details: String(err) });
97
+ }
98
+ });
99
+
100
+ // ─── GitHub PRs ────────────────────────────────────────────
101
+
102
+ router.get('/github/prs', async (_req, res) => {
103
+ try {
104
+ const prs = await githubService.getOpenPRs();
105
+ res.json(prs);
106
+ } catch (err) {
107
+ res.status(500).json({ error: 'Failed to get PRs', details: String(err) });
108
+ }
109
+ });
110
+
111
+ return router;
112
+ }
@@ -0,0 +1,262 @@
1
+ import { Router } from 'express';
2
+ import { spawn } from 'child_process';
3
+ import type Database from 'better-sqlite3';
4
+ import type { Server } from 'socket.io';
5
+ import type { ScopeService } from '../services/scope-service.js';
6
+ import type { ReadinessService } from '../services/readiness-service.js';
7
+ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
8
+ import { launchInTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
9
+ import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
10
+ import { getConfig } from '../config.js';
11
+ import { createLogger } from '../utils/logger.js';
12
+
13
+ const log = createLogger('dispatch');
14
+
15
+ interface ScopeRouteDeps {
16
+ db: Database.Database;
17
+ io: Server;
18
+ scopeService: ScopeService;
19
+ readinessService: ReadinessService;
20
+ projectRoot: string;
21
+ engine: WorkflowEngine;
22
+ }
23
+
24
+ export function createScopeRoutes({ db, io, scopeService, readinessService, projectRoot, engine }: ScopeRouteDeps): Router {
25
+ const router = Router();
26
+
27
+ // ─── Scope CRUD ──────────────────────────────────────────
28
+
29
+ router.get('/scopes', (_req, res) => {
30
+ res.json(scopeService.getAll());
31
+ });
32
+
33
+ // ─── Transition Readiness ──────────────────────────────────
34
+
35
+ router.get('/scopes/:id/readiness', (req, res) => {
36
+ const readiness = readinessService.getReadiness(Number(req.params.id));
37
+ if (!readiness) {
38
+ res.status(404).json({ error: 'Scope not found' });
39
+ return;
40
+ }
41
+ res.json(readiness);
42
+ });
43
+
44
+ /** Bulk update — must come before :id route to avoid matching "bulk" as an id */
45
+ router.patch('/scopes/bulk/status', (req, res) => {
46
+ const { scopes } = req.body as { scopes: Array<{ id: number; status: string }> };
47
+ if (!Array.isArray(scopes)) {
48
+ res.status(400).json({ error: 'Expected { scopes: [{id, status}] }' });
49
+ return;
50
+ }
51
+ let updated = 0;
52
+ for (const { id, status } of scopes) {
53
+ const result = scopeService.updateStatus(id, status, 'bulk-sync');
54
+ if (result.ok) updated++;
55
+ }
56
+ res.json({ updated, total: scopes.length });
57
+ });
58
+
59
+ router.get('/scopes/:id', (req, res) => {
60
+ const scope = scopeService.getById(Number(req.params.id));
61
+ if (!scope) {
62
+ res.status(404).json({ error: 'Scope not found' });
63
+ return;
64
+ }
65
+ res.json(scope);
66
+ });
67
+
68
+ router.patch('/scopes/:id', (req, res) => {
69
+ const id = Number(req.params.id);
70
+ const result = scopeService.updateScopeFrontmatter(id, req.body);
71
+ if (!result.ok) {
72
+ const code = result.code === 'NOT_FOUND' ? 404 : 400;
73
+ res.status(code).json({ error: result.error, code: result.code });
74
+ return;
75
+ }
76
+ const scope = scopeService.getById(id);
77
+ res.json(scope ?? { ok: true });
78
+ });
79
+
80
+ // ─── Idea Routes ─────────────────────────────────────────
81
+
82
+ router.post('/ideas', (req, res) => {
83
+ const { title, description } = req.body as { title?: string; description?: string };
84
+ if (!title?.trim()) {
85
+ res.status(400).json({ error: 'title is required' });
86
+ return;
87
+ }
88
+ const idea = scopeService.createIdeaFile(title.trim(), (description ?? '').trim());
89
+ res.status(201).json(idea);
90
+ });
91
+
92
+ router.patch('/ideas/:id', (req, res) => {
93
+ const id = Number(req.params.id);
94
+ const { title, description } = req.body as { title?: string; description?: string };
95
+ if (!title?.trim()) {
96
+ res.status(400).json({ error: 'title is required' });
97
+ return;
98
+ }
99
+ const updated = scopeService.updateIdeaFile(id, title.trim(), (description ?? '').trim());
100
+ if (!updated) {
101
+ res.status(404).json({ error: 'Idea not found' });
102
+ return;
103
+ }
104
+ res.json({ ok: true });
105
+ });
106
+
107
+ router.delete('/ideas/:id', (req, res) => {
108
+ const id = Number(req.params.id);
109
+ const deleted = scopeService.deleteIdeaFile(id);
110
+ if (!deleted) {
111
+ res.status(404).json({ error: 'Idea not found' });
112
+ return;
113
+ }
114
+ res.json({ ok: true });
115
+ });
116
+
117
+ router.post('/ideas/:id/promote', async (req, res) => {
118
+ const ideaId = Number(req.params.id);
119
+ const result = scopeService.promoteIdea(ideaId);
120
+ if (!result) {
121
+ res.status(404).json({ error: 'Idea not found' });
122
+ return;
123
+ }
124
+
125
+ const scopeId = result.id;
126
+
127
+ // Read command from workflow edge config (user-overridable)
128
+ const entryPoint = engine.getEntryPoint();
129
+ const targets = engine.getValidTargets(entryPoint.id);
130
+ const promoteTarget = targets[0] ?? 'planning';
131
+ const edge = engine.findEdge(entryPoint.id, promoteTarget);
132
+ const edgeCommand = edge ? engine.buildCommand(edge, scopeId) : null;
133
+ const command = edgeCommand ?? `/scope-create ${String(scopeId).padStart(3, '0')}`;
134
+
135
+ // Record DISPATCH event for audit trail
136
+ const eventId = crypto.randomUUID();
137
+ const eventData = {
138
+ command,
139
+ transition: { from: entryPoint.id, to: promoteTarget },
140
+ resolved: null,
141
+ };
142
+ db.prepare(
143
+ `INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
144
+ VALUES (?, 'DISPATCH', ?, NULL, 'dashboard', ?, ?)`
145
+ ).run(eventId, scopeId, JSON.stringify(eventData), new Date().toISOString());
146
+
147
+ io.emit('event:new', {
148
+ id: eventId, type: 'DISPATCH', scope_id: scopeId,
149
+ session_id: null, agent: 'dashboard', data: eventData,
150
+ timestamp: new Date().toISOString(),
151
+ });
152
+
153
+ const escaped = escapeForAnsiC(command);
154
+ const fullCmd = `cd '${projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
155
+
156
+ const promoteSessionName = buildSessionName({ scopeId, title: result.title, command });
157
+ const promoteBeforePids = snapshotSessionPids(projectRoot);
158
+
159
+ try {
160
+ await launchInTerminal(fullCmd);
161
+ res.json({ ok: true, id: scopeId, filePath: result.filePath });
162
+
163
+ discoverNewSession(projectRoot, promoteBeforePids)
164
+ .then((session) => {
165
+ if (!session) return;
166
+ linkPidToDispatch(db, eventId, session.pid);
167
+ if (promoteSessionName) renameSession(projectRoot, session.sessionId, promoteSessionName);
168
+ })
169
+ .catch(err => log.error('PID discovery failed', { error: err.message }));
170
+ } catch (err) {
171
+ resolveDispatchEvent(db, io, eventId, 'failed', String(err));
172
+ res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
173
+ }
174
+ });
175
+
176
+ // ─── Surprise Me (AI idea generation) ────────────────────
177
+
178
+ let surpriseInProgress = false;
179
+
180
+ router.post('/ideas/surprise', (_req, res) => {
181
+ if (surpriseInProgress) {
182
+ res.status(409).json({ error: 'Surprise generation already in progress' });
183
+ return;
184
+ }
185
+ surpriseInProgress = true;
186
+
187
+ const nextIdStart = scopeService.getNextIceboxId();
188
+ const today = new Date().toISOString().split('T')[0];
189
+ const idRange = Array.from({ length: 5 }, (_, i) => nextIdStart + i);
190
+
191
+ const prompt = `You are analyzing the ${getConfig().projectName} codebase to suggest feature ideas. Your ONLY job is to create markdown files.
192
+
193
+ Create exactly 3 idea files in the scopes/icebox/ directory. Each file must use this EXACT format:
194
+
195
+ File: scopes/icebox/{ID}-{kebab-slug}.md
196
+
197
+ ---
198
+ id: {ID}
199
+ title: "{title}"
200
+ status: icebox
201
+ ghost: true
202
+ created: ${today}
203
+ updated: ${today}
204
+ blocked_by: []
205
+ blocks: []
206
+ tags: []
207
+ ---
208
+
209
+ {2-3 sentence description of the feature, what problem it solves, and a rough approach.}
210
+
211
+ Use these IDs: ${idRange[0]}, ${idRange[1]}, ${idRange[2]}
212
+
213
+ Rules:
214
+ - Focus on practical improvements: performance, UX, security, developer experience, monitoring, or reliability
215
+ - Be specific and actionable — not vague architectural rewrites
216
+ - Keep descriptions concise (2-3 sentences max)
217
+ - Filenames must be {ID}-{kebab-case-slug}.md
218
+ - The ghost: true field is required in frontmatter
219
+ - Do NOT create any other files or make any other changes`;
220
+
221
+ const child = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
222
+ cwd: projectRoot,
223
+ stdio: ['ignore', 'pipe', 'pipe'],
224
+ env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'orbital-surprise' },
225
+ });
226
+
227
+ child.unref();
228
+
229
+ child.on('close', () => {
230
+ surpriseInProgress = false;
231
+ const eventId = crypto.randomUUID();
232
+ io.emit('event:new', {
233
+ id: eventId, type: 'AGENT_COMPLETED', scope_id: null,
234
+ session_id: null, agent: 'surprise-me',
235
+ data: { action: 'surprise-ideas-generated' },
236
+ timestamp: new Date().toISOString(),
237
+ });
238
+ });
239
+
240
+ child.on('error', () => {
241
+ surpriseInProgress = false;
242
+ });
243
+
244
+ res.json({ ok: true, status: 'generating' });
245
+ });
246
+
247
+ router.post('/ideas/:id/approve', (req, res) => {
248
+ const id = Number(req.params.id);
249
+ const approved = scopeService.approveGhostIdea(id);
250
+ if (!approved) {
251
+ res.status(404).json({ error: 'Ghost idea not found' });
252
+ return;
253
+ }
254
+ res.json({ ok: true });
255
+ });
256
+
257
+ router.get('/ideas/surprise/status', (_req, res) => {
258
+ res.json({ generating: surpriseInProgress });
259
+ });
260
+
261
+ return router;
262
+ }