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,361 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { Server } from 'socket.io';
3
+ import { SprintService } from './sprint-service.js';
4
+ import { ScopeService } from './scope-service.js';
5
+ import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
6
+ import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
7
+ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
8
+ import { getConfig } from '../config.js';
9
+ import { createLogger } from '../utils/logger.js';
10
+
11
+ const log = createLogger('sprint');
12
+ const LAUNCH_STAGGER_MS = 2000;
13
+
14
+ function sleep(ms: number): Promise<void> {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+
18
+ // ─── Orchestrator ───────────────────────────────────────────
19
+
20
+ export class SprintOrchestrator {
21
+ constructor(
22
+ private db: Database.Database,
23
+ private io: Server,
24
+ private sprintService: SprintService,
25
+ private scopeService: ScopeService,
26
+ private engine: WorkflowEngine,
27
+ ) {}
28
+
29
+ /** Build execution layers using Kahn's topological sort */
30
+ buildExecutionLayers(sprintScopeIds: number[]): { layers: number[][]; cycle: number[] } {
31
+ const sprintSet = new Set(sprintScopeIds);
32
+
33
+ // Load dependency info for each scope in the sprint
34
+ const scopeDeps = new Map<number, number[]>();
35
+ for (const id of sprintScopeIds) {
36
+ const scope = this.scopeService.getById(id);
37
+ if (!scope) continue;
38
+ // Only keep deps that are WITHIN the sprint
39
+ scopeDeps.set(id, scope.blocked_by.filter((d) => sprintSet.has(d)));
40
+ }
41
+
42
+ // Build in-degree map — in-degree = count of internal deps for each scope
43
+ const inDegree = new Map<number, number>();
44
+ for (const [id, deps] of scopeDeps) {
45
+ inDegree.set(id, deps.length);
46
+ }
47
+
48
+ const layers: number[][] = [];
49
+ const remaining = new Set(sprintScopeIds);
50
+
51
+ while (remaining.size > 0) {
52
+ // Find all nodes with in-degree 0
53
+ const layer: number[] = [];
54
+ for (const id of remaining) {
55
+ if ((inDegree.get(id) ?? 0) === 0) {
56
+ layer.push(id);
57
+ }
58
+ }
59
+
60
+ if (layer.length === 0) {
61
+ // Cycle detected — return remaining as cycle
62
+ return { layers, cycle: [...remaining] };
63
+ }
64
+
65
+ // Remove this layer and decrement dependents
66
+ for (const id of layer) {
67
+ remaining.delete(id);
68
+ }
69
+
70
+ // Decrement in-degree for scopes that depended on this layer's scopes
71
+ for (const id of remaining) {
72
+ const deps = scopeDeps.get(id) ?? [];
73
+ let newDeg = 0;
74
+ for (const dep of deps) {
75
+ if (remaining.has(dep)) newDeg++;
76
+ }
77
+ inDegree.set(id, newDeg);
78
+ }
79
+
80
+ layers.push(layer.sort((a, b) => a - b));
81
+ }
82
+
83
+ return { layers, cycle: [] };
84
+ }
85
+
86
+ /** Start sprint dispatch: build layers, persist, launch Layer 0 */
87
+ async startSprint(sprintId: number): Promise<{ ok: boolean; error?: string; layers?: number[][] }> {
88
+ const sprint = this.sprintService.getById(sprintId);
89
+ if (!sprint) return { ok: false, error: 'Sprint not found' };
90
+ if (sprint.status !== 'assembling') return { ok: false, error: `Sprint status is '${sprint.status}', expected 'assembling'` };
91
+ if (sprint.scope_ids.length === 0) return { ok: false, error: 'Sprint has no scopes' };
92
+
93
+ // Build dependency graph
94
+ const { layers, cycle } = this.buildExecutionLayers(sprint.scope_ids);
95
+ if (cycle.length > 0) {
96
+ return { ok: false, error: `Dependency cycle detected among scopes: ${cycle.join(', ')}` };
97
+ }
98
+
99
+ // Persist layer assignments
100
+ this.sprintService.setLayers(sprintId, layers);
101
+ this.sprintService.updateStatus(sprintId, 'dispatched');
102
+
103
+ log.info('Sprint started', { id: sprintId, layers: layers.length, scopes: sprint.scope_ids.length });
104
+
105
+ // Dispatch Layer 0
106
+ await this.dispatchLayer(sprintId, layers[0], sprint.concurrency_cap);
107
+
108
+ return { ok: true, layers };
109
+ }
110
+
111
+ /** Called when a scope reaches 'dev' status — advance the sprint */
112
+ async onScopeReachedDev(scopeId: number): Promise<void> {
113
+ const match = this.sprintService.findActiveSprintForScope(scopeId);
114
+ if (!match) return;
115
+ log.debug('Scope reached dev', { scopeId, sprintId: match.sprint_id });
116
+
117
+ const sprintId = match.sprint_id;
118
+ this.sprintService.updateScopeStatus(sprintId, scopeId, 'completed');
119
+
120
+ // Ensure sprint is in 'in_progress' state
121
+ const sprint = this.sprintService.getById(sprintId);
122
+ if (!sprint) return;
123
+ if (sprint.status === 'dispatched') {
124
+ this.sprintService.updateStatus(sprintId, 'in_progress');
125
+ }
126
+
127
+ // Check for newly unblocked scopes and dispatch them
128
+ await this.dispatchUnblockedScopes(sprintId);
129
+ this.checkSprintCompletion(sprintId);
130
+ }
131
+
132
+ /** Called when a scope fails during sprint execution */
133
+ async onScopeFailed(scopeId: number, error?: string): Promise<void> {
134
+ const match = this.sprintService.findActiveSprintForScope(scopeId);
135
+ if (!match) return;
136
+
137
+ const sprintId = match.sprint_id;
138
+ this.sprintService.updateScopeStatus(sprintId, scopeId, 'failed', error);
139
+
140
+ // Skip downstream dependents transitively
141
+ this.skipDownstream(sprintId, scopeId);
142
+
143
+ // Try dispatching other unblocked parallel paths
144
+ await this.dispatchUnblockedScopes(sprintId);
145
+ this.checkSprintCompletion(sprintId);
146
+ }
147
+
148
+ /** Cancel an active sprint */
149
+ cancelSprint(sprintId: number): boolean {
150
+ const sprint = this.sprintService.getById(sprintId);
151
+ if (!sprint) return false;
152
+ if (!['assembling', 'dispatched', 'in_progress'].includes(sprint.status)) return false;
153
+
154
+ // Mark pending/queued scopes as skipped
155
+ const scopes = this.sprintService.getSprintScopes(sprintId);
156
+ for (const ss of scopes) {
157
+ if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'queued') {
158
+ this.sprintService.updateScopeStatus(sprintId, ss.scope_id, 'skipped');
159
+ }
160
+ }
161
+
162
+ this.sprintService.updateStatus(sprintId, 'cancelled');
163
+ return true;
164
+ }
165
+
166
+ /** Recover active sprints after server restart */
167
+ async recoverActiveSprints(): Promise<void> {
168
+ const active = this.db.prepare(
169
+ `SELECT id FROM sprints WHERE group_type = 'sprint' AND status IN ('dispatched', 'in_progress')`,
170
+ ).all() as Array<{ id: number }>;
171
+
172
+ if (active.length > 0) {
173
+ log.info('Recovering active sprints', { count: active.length });
174
+ }
175
+
176
+ for (const { id } of active) {
177
+ // Check if any scopes completed while server was down
178
+ const scopes = this.sprintService.getSprintScopes(id);
179
+ for (const ss of scopes) {
180
+ if (ss.dispatch_status === 'dispatched' || ss.dispatch_status === 'in_progress') {
181
+ // Check actual scope status
182
+ const scope = this.scopeService.getById(ss.scope_id);
183
+ if (scope && this.engine.getStatusOrder(scope.status) >= this.engine.getStatusOrder('dev')) {
184
+ this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
185
+ }
186
+ }
187
+ }
188
+
189
+ await this.dispatchUnblockedScopes(id);
190
+ this.checkSprintCompletion(id);
191
+ }
192
+ }
193
+
194
+ /** Get execution graph data for visualization */
195
+ getExecutionGraph(sprintId: number): { layers: number[][]; edges: Array<{ from: number; to: number }> } | null {
196
+ const sprint = this.sprintService.getById(sprintId);
197
+ if (!sprint) return null;
198
+
199
+ const layers = sprint.layers ?? [];
200
+ const sprintSet = new Set(sprint.scope_ids);
201
+ const edges: Array<{ from: number; to: number }> = [];
202
+
203
+ for (const scopeId of sprint.scope_ids) {
204
+ const scope = this.scopeService.getById(scopeId);
205
+ if (!scope) continue;
206
+ for (const dep of scope.blocked_by) {
207
+ if (sprintSet.has(dep)) {
208
+ edges.push({ from: dep, to: scopeId });
209
+ }
210
+ }
211
+ }
212
+
213
+ return { layers, edges };
214
+ }
215
+
216
+ // ─── Private Helpers ────────────────────────────────────────
217
+
218
+ private async dispatchLayer(sprintId: number, scopeIds: number[], concurrencyCap: number): Promise<void> {
219
+ const toDispatch = scopeIds.slice(0, concurrencyCap);
220
+
221
+ for (let i = 0; i < toDispatch.length; i++) {
222
+ const scopeId = toDispatch[i];
223
+
224
+ // Capture current status before optimistic update (for rollback)
225
+ const currentScope = this.scopeService.getById(scopeId);
226
+ const previousStatus = currentScope?.status ?? 'implementing';
227
+
228
+ // Record DISPATCH event
229
+ const eventId = crypto.randomUUID();
230
+ const command = `/scope implement ${scopeId}`;
231
+ this.db.prepare(
232
+ `INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
233
+ VALUES (?, 'DISPATCH', ?, NULL, 'sprint-orchestrator', ?, ?)`,
234
+ ).run(eventId, scopeId, JSON.stringify({ command, sprint_id: sprintId, resolved: null }), new Date().toISOString());
235
+
236
+ this.io.emit('event:new', {
237
+ id: eventId, type: 'DISPATCH', scope_id: scopeId,
238
+ session_id: null, agent: 'sprint-orchestrator',
239
+ data: { command, sprint_id: sprintId, resolved: null },
240
+ timestamp: new Date().toISOString(),
241
+ });
242
+
243
+ // Update scope + sprint_scope status
244
+ this.scopeService.updateStatus(scopeId, 'implementing', 'dispatch');
245
+ this.sprintService.updateScopeStatus(sprintId, scopeId, 'dispatched');
246
+
247
+ // Build scope-aware session name and snapshot PIDs
248
+ const scopeRow = this.scopeService.getById(scopeId);
249
+ const sessionName = buildSessionName({ scopeId, title: scopeRow?.title, command });
250
+ const beforePids = snapshotSessionPids(getConfig().projectRoot);
251
+
252
+ // Launch in iTerm — interactive TUI mode (no -p) for full visibility
253
+ const escaped = escapeForAnsiC(command);
254
+ const fullCmd = `cd '${getConfig().projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
255
+ try {
256
+ await launchInCategorizedTerminal(command, fullCmd, sessionName);
257
+
258
+ // Fire-and-forget: discover session PID, link to dispatch, and rename
259
+ discoverNewSession(getConfig().projectRoot, beforePids)
260
+ .then((session) => {
261
+ if (!session) return;
262
+ linkPidToDispatch(this.db, eventId, session.pid);
263
+ if (sessionName) renameSession(getConfig().projectRoot, session.sessionId, sessionName);
264
+ })
265
+ .catch(err => log.error('PID discovery failed', { error: err.message }));
266
+ } catch (err) {
267
+ // Rollback scope status to previous value
268
+ this.scopeService.updateStatus(scopeId, previousStatus, 'rollback');
269
+ this.sprintService.updateScopeStatus(sprintId, scopeId, 'failed', `Launch failed: ${err}`);
270
+ resolveDispatchEvent(this.db, this.io, eventId, 'failed', `Launch failed: ${err}`);
271
+ }
272
+
273
+ // Stagger launches to prevent AppleScript race conditions
274
+ if (i < toDispatch.length - 1) {
275
+ await sleep(LAUNCH_STAGGER_MS);
276
+ }
277
+ }
278
+ }
279
+
280
+ private async dispatchUnblockedScopes(sprintId: number): Promise<void> {
281
+ const sprint = this.sprintService.getById(sprintId);
282
+ if (!sprint) return;
283
+
284
+ const scopes = this.sprintService.getSprintScopes(sprintId);
285
+ const completedSet = new Set(
286
+ scopes.filter((ss) => ss.dispatch_status === 'completed').map((ss) => ss.scope_id),
287
+ );
288
+ const activeCount = scopes.filter(
289
+ (ss) => ss.dispatch_status === 'dispatched' || ss.dispatch_status === 'in_progress',
290
+ ).length;
291
+ const available = sprint.concurrency_cap - activeCount;
292
+ if (available <= 0) return;
293
+
294
+ // Find pending scopes whose internal deps are all completed
295
+ const ready: number[] = [];
296
+ for (const ss of scopes) {
297
+ if (ss.dispatch_status !== 'pending') continue;
298
+
299
+ const scope = this.scopeService.getById(ss.scope_id);
300
+ if (!scope) continue;
301
+ const internalDeps = scope.blocked_by.filter((d) => sprint.scope_ids.includes(d));
302
+ const allMet = internalDeps.every((d) => completedSet.has(d));
303
+
304
+ if (allMet) ready.push(ss.scope_id);
305
+ if (ready.length >= available) break;
306
+ }
307
+
308
+ if (ready.length > 0) {
309
+ await this.dispatchLayer(sprintId, ready, available);
310
+ }
311
+ }
312
+
313
+ private skipDownstream(sprintId: number, failedScopeId: number): void {
314
+ const scopes = this.sprintService.getSprintScopes(sprintId);
315
+ const sprintScopeIds = scopes.map((ss) => ss.scope_id);
316
+
317
+ // Build reverse dependency map: scope → scopes that depend on it
318
+ const dependents = new Map<number, number[]>();
319
+ for (const scopeId of sprintScopeIds) {
320
+ const scope = this.scopeService.getById(scopeId);
321
+ if (!scope) continue;
322
+ for (const dep of scope.blocked_by) {
323
+ if (!dependents.has(dep)) dependents.set(dep, []);
324
+ dependents.get(dep)!.push(scopeId);
325
+ }
326
+ }
327
+
328
+ // BFS to find all transitive dependents
329
+ const toSkip = new Set<number>();
330
+ const queue = [failedScopeId];
331
+ while (queue.length > 0) {
332
+ const current = queue.shift()!;
333
+ const downstream = dependents.get(current) ?? [];
334
+ for (const id of downstream) {
335
+ if (!toSkip.has(id)) {
336
+ toSkip.add(id);
337
+ queue.push(id);
338
+ }
339
+ }
340
+ }
341
+
342
+ for (const scopeId of toSkip) {
343
+ const ss = scopes.find((s) => s.scope_id === scopeId);
344
+ if (ss && ss.dispatch_status === 'pending') {
345
+ this.sprintService.updateScopeStatus(sprintId, scopeId, 'skipped', `Skipped: dependency ${failedScopeId} failed`);
346
+ }
347
+ }
348
+ }
349
+
350
+ private checkSprintCompletion(sprintId: number): void {
351
+ const scopes = this.sprintService.getSprintScopes(sprintId);
352
+ const allDone = scopes.every(
353
+ (ss) => ss.dispatch_status === 'completed' || ss.dispatch_status === 'failed' || ss.dispatch_status === 'skipped',
354
+ );
355
+
356
+ if (!allDone) return;
357
+
358
+ const anyFailed = scopes.some((ss) => ss.dispatch_status === 'failed');
359
+ this.sprintService.updateStatus(sprintId, anyFailed ? 'failed' : 'completed');
360
+ }
361
+ }