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,415 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { Server } from 'socket.io';
3
+ import type { ScopeService } from './scope-service.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+
6
+ const log = createLogger('sprint');
7
+
8
+ // ─── Types ──────────────────────────────────────────────────
9
+
10
+ export type { SprintStatus, SprintScopeStatus, GroupType } from '../../shared/api-types.js';
11
+ import type { SprintStatus, SprintScopeStatus, GroupType } from '../../shared/api-types.js';
12
+ export type GroupTargetColumn = 'backlog' | 'implementing' | 'review' | 'completed' | 'dev' | 'staging';
13
+
14
+ export interface BatchDispatchResult {
15
+ commit_sha?: string;
16
+ pr_url?: string;
17
+ pr_number?: number;
18
+ dispatched_at?: string;
19
+ }
20
+
21
+ interface SprintRow {
22
+ id: number;
23
+ name: string;
24
+ status: SprintStatus;
25
+ concurrency_cap: number;
26
+ created_at: string;
27
+ updated_at: string;
28
+ dispatched_at: string | null;
29
+ completed_at: string | null;
30
+ dispatch_meta: string;
31
+ target_column: GroupTargetColumn;
32
+ group_type: GroupType;
33
+ dispatch_result: string;
34
+ }
35
+
36
+ interface SprintScopeRow {
37
+ sprint_id: number;
38
+ scope_id: number;
39
+ layer: number | null;
40
+ dispatch_status: SprintScopeStatus;
41
+ dispatched_at: string | null;
42
+ completed_at: string | null;
43
+ error: string | null;
44
+ }
45
+
46
+ interface UnmetDep {
47
+ scope_id: number;
48
+ title: string;
49
+ status: string;
50
+ }
51
+
52
+ export interface AddScopesResult {
53
+ added: number[];
54
+ unmet_dependencies: Array<{ scope_id: number; missing: UnmetDep[] }>;
55
+ }
56
+
57
+ export interface SprintDetail {
58
+ id: number;
59
+ name: string;
60
+ status: SprintStatus;
61
+ concurrency_cap: number;
62
+ group_type: GroupType;
63
+ target_column: GroupTargetColumn;
64
+ dispatch_result: BatchDispatchResult | null;
65
+ scope_ids: number[];
66
+ scopes: Array<{
67
+ scope_id: number;
68
+ title: string;
69
+ scope_status: string;
70
+ effort_estimate: string | null;
71
+ layer: number | null;
72
+ dispatch_status: SprintScopeStatus;
73
+ }>;
74
+ layers: number[][] | null;
75
+ progress: { pending: number; in_progress: number; completed: number; failed: number; skipped: number };
76
+ created_at: string;
77
+ updated_at: string;
78
+ dispatched_at: string | null;
79
+ completed_at: string | null;
80
+ }
81
+
82
+ // ─── Service ────────────────────────────────────────────────
83
+
84
+ export class SprintService {
85
+ constructor(
86
+ private db: Database.Database,
87
+ private io: Server,
88
+ private scopeService: ScopeService,
89
+ ) {}
90
+
91
+ /** Create a new sprint or batch in assembling state */
92
+ create(name: string, options?: { target_column?: GroupTargetColumn; group_type?: GroupType }): SprintDetail {
93
+ const now = new Date().toISOString();
94
+ const targetColumn = options?.target_column ?? 'backlog';
95
+ const groupType = options?.group_type ?? 'sprint';
96
+ const result = this.db.prepare(
97
+ `INSERT INTO sprints (name, status, concurrency_cap, created_at, updated_at, target_column, group_type)
98
+ VALUES (?, 'assembling', 5, ?, ?, ?, ?)`,
99
+ ).run(name, now, now, targetColumn, groupType);
100
+
101
+ const sprint = this.getById(Number(result.lastInsertRowid))!;
102
+ log.info('Sprint created', { id: sprint.id, name, group_type: groupType, target_column: targetColumn });
103
+ this.io.emit('sprint:created', sprint);
104
+ return sprint;
105
+ }
106
+
107
+ /** Rename a sprint/batch (only while assembling) */
108
+ rename(id: number, name: string): boolean {
109
+ const result = this.db.prepare(
110
+ `UPDATE sprints SET name = ?, updated_at = ? WHERE id = ? AND status = 'assembling'`,
111
+ ).run(name, new Date().toISOString(), id);
112
+ if (result.changes > 0) {
113
+ this.emitUpdate(id);
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+
119
+ /** List sprints, optionally filtered by status and/or target column */
120
+ getAll(status?: SprintStatus, targetColumn?: GroupTargetColumn): SprintDetail[] {
121
+ let rows: SprintRow[];
122
+ if (status && targetColumn) {
123
+ rows = this.db.prepare('SELECT * FROM sprints WHERE status = ? AND target_column = ? ORDER BY created_at DESC')
124
+ .all(status, targetColumn) as SprintRow[];
125
+ } else if (status) {
126
+ rows = this.db.prepare('SELECT * FROM sprints WHERE status = ? ORDER BY created_at DESC').all(status) as SprintRow[];
127
+ } else if (targetColumn) {
128
+ rows = this.db.prepare('SELECT * FROM sprints WHERE target_column = ? ORDER BY created_at DESC').all(targetColumn) as SprintRow[];
129
+ } else {
130
+ rows = this.db.prepare('SELECT * FROM sprints ORDER BY created_at DESC').all() as SprintRow[];
131
+ }
132
+ return rows.map((row) => this.buildDetail(row));
133
+ }
134
+
135
+ /** Get full sprint detail by ID */
136
+ getById(id: number): SprintDetail | null {
137
+ const row = this.db.prepare('SELECT * FROM sprints WHERE id = ?').get(id) as SprintRow | undefined;
138
+ if (!row) return null;
139
+ return this.buildDetail(row);
140
+ }
141
+
142
+ /** Delete a sprint (only if assembling) */
143
+ delete(id: number): boolean {
144
+ const row = this.db.prepare('SELECT status FROM sprints WHERE id = ?').get(id) as { status: string } | undefined;
145
+ if (!row || row.status !== 'assembling') return false;
146
+
147
+ this.db.prepare('DELETE FROM sprint_scopes WHERE sprint_id = ?').run(id);
148
+ this.db.prepare('DELETE FROM sprints WHERE id = ?').run(id);
149
+ this.io.emit('sprint:deleted', { id });
150
+ return true;
151
+ }
152
+
153
+ /** Add scopes to a sprint; returns which were added and any unmet dependencies */
154
+ addScopes(sprintId: number, scopeIds: number[]): AddScopesResult | null {
155
+ const sprint = this.db.prepare('SELECT * FROM sprints WHERE id = ?').get(sprintId) as SprintRow | undefined;
156
+ if (!sprint || sprint.status !== 'assembling') return null;
157
+
158
+ // Existing scope IDs already in this sprint
159
+ const existingIds = new Set(
160
+ (this.db.prepare('SELECT scope_id FROM sprint_scopes WHERE sprint_id = ?').all(sprintId) as Array<{ scope_id: number }>)
161
+ .map((r) => r.scope_id),
162
+ );
163
+
164
+ const added: number[] = [];
165
+ const unmet: Array<{ scope_id: number; missing: UnmetDep[] }> = [];
166
+
167
+ const insert = this.db.prepare(
168
+ `INSERT OR IGNORE INTO sprint_scopes (sprint_id, scope_id, dispatch_status)
169
+ VALUES (?, ?, 'pending')`,
170
+ );
171
+
172
+ for (const scopeId of scopeIds) {
173
+ if (existingIds.has(scopeId)) continue;
174
+
175
+ // Check dependencies via cache
176
+ const scope = this.scopeService.getById(scopeId);
177
+ if (!scope) continue;
178
+
179
+ // W-8: For batch groups, validate scope status matches target column
180
+ if (sprint.group_type === 'batch' && scope.status !== sprint.target_column) {
181
+ continue; // silently skip — frontend shows toast for rejected drops
182
+ }
183
+
184
+ const missing: UnmetDep[] = [];
185
+
186
+ for (const depId of scope.blocked_by) {
187
+ if (existingIds.has(depId) || scopeIds.includes(depId)) continue;
188
+ // Check if dependency is already complete (dev or beyond)
189
+ const dep = this.scopeService.getById(depId);
190
+ if (!dep) continue;
191
+ const completedStatuses = ['dev', 'staging', 'production'];
192
+ if (!completedStatuses.includes(dep.status)) {
193
+ missing.push({ scope_id: dep.id, title: dep.title, status: dep.status });
194
+ }
195
+ }
196
+
197
+ if (missing.length > 0) {
198
+ unmet.push({ scope_id: scopeId, missing });
199
+ }
200
+
201
+ insert.run(sprintId, scopeId);
202
+ existingIds.add(scopeId);
203
+ added.push(scopeId);
204
+ }
205
+
206
+ this.touchUpdatedAt(sprintId);
207
+ this.emitUpdate(sprintId);
208
+ return { added, unmet_dependencies: unmet };
209
+ }
210
+
211
+ /** Remove scopes from a sprint (assembling only) */
212
+ removeScopes(sprintId: number, scopeIds: number[]): boolean {
213
+ const sprint = this.db.prepare('SELECT status FROM sprints WHERE id = ?').get(sprintId) as { status: string } | undefined;
214
+ if (!sprint || sprint.status !== 'assembling') return false;
215
+
216
+ const remove = this.db.prepare('DELETE FROM sprint_scopes WHERE sprint_id = ? AND scope_id = ?');
217
+ for (const scopeId of scopeIds) {
218
+ remove.run(sprintId, scopeId);
219
+ }
220
+
221
+ this.touchUpdatedAt(sprintId);
222
+ this.emitUpdate(sprintId);
223
+ return true;
224
+ }
225
+
226
+ /** Update sprint status */
227
+ updateStatus(id: number, status: SprintStatus): boolean {
228
+ const now = new Date().toISOString();
229
+ const extras: Record<string, string> = {};
230
+ if (status === 'dispatched') extras.dispatched_at = now;
231
+ if (status === 'completed' || status === 'failed' || status === 'cancelled') extras.completed_at = now;
232
+
233
+ const setClauses = ['status = ?', 'updated_at = ?'];
234
+ const params: unknown[] = [status, now];
235
+
236
+ for (const [col, val] of Object.entries(extras)) {
237
+ setClauses.push(`${col} = ?`);
238
+ params.push(val);
239
+ }
240
+ params.push(id);
241
+
242
+ const result = this.db.prepare(`UPDATE sprints SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
243
+ if (result.changes > 0) {
244
+ log.info('Sprint status updated', { id, status });
245
+ this.emitUpdate(id);
246
+ if (status === 'completed') {
247
+ const detail = this.getById(id);
248
+ if (detail) this.io.emit('sprint:completed', detail);
249
+ }
250
+ }
251
+ return result.changes > 0;
252
+ }
253
+
254
+ /** Update a sprint scope's dispatch status */
255
+ updateScopeStatus(sprintId: number, scopeId: number, status: SprintScopeStatus, error?: string): void {
256
+ const now = new Date().toISOString();
257
+ const extras: string[] = [];
258
+ const params: unknown[] = [status];
259
+
260
+ if (status === 'dispatched') {
261
+ extras.push('dispatched_at = ?');
262
+ params.push(now);
263
+ }
264
+ if (status === 'completed' || status === 'failed' || status === 'skipped') {
265
+ extras.push('completed_at = ?');
266
+ params.push(now);
267
+ }
268
+ if (error != null) {
269
+ extras.push('error = ?');
270
+ params.push(error);
271
+ }
272
+
273
+ const setClauses = ['dispatch_status = ?', ...extras];
274
+ params.push(sprintId, scopeId);
275
+
276
+ this.db.prepare(
277
+ `UPDATE sprint_scopes SET ${setClauses.join(', ')} WHERE sprint_id = ? AND scope_id = ?`,
278
+ ).run(...params);
279
+
280
+ this.emitUpdate(sprintId);
281
+ }
282
+
283
+ /** Persist layer assignments for all scopes in a sprint */
284
+ setLayers(sprintId: number, layers: number[][]): void {
285
+ const update = this.db.prepare('UPDATE sprint_scopes SET layer = ? WHERE sprint_id = ? AND scope_id = ?');
286
+ for (let i = 0; i < layers.length; i++) {
287
+ for (const scopeId of layers[i]) {
288
+ update.run(i, sprintId, scopeId);
289
+ }
290
+ }
291
+
292
+ this.db.prepare('UPDATE sprints SET dispatch_meta = ?, updated_at = ? WHERE id = ?')
293
+ .run(JSON.stringify({ layers }), new Date().toISOString(), sprintId);
294
+ }
295
+
296
+ /** Find the active sprint containing a given scope (for orchestrator callbacks) */
297
+ findActiveSprintForScope(scopeId: number): { sprint_id: number } | null {
298
+ return this.db.prepare(
299
+ `SELECT ss.sprint_id FROM sprint_scopes ss
300
+ JOIN sprints s ON s.id = ss.sprint_id
301
+ WHERE ss.scope_id = ? AND s.status IN ('dispatched', 'in_progress')
302
+ LIMIT 1`,
303
+ ).get(scopeId) as { sprint_id: number } | null;
304
+ }
305
+
306
+ /** Find any active group (assembling/dispatched/in_progress) containing a scope.
307
+ * Used to guard against moving scopes that are part of an active batch/sprint. */
308
+ getActiveGroupForScope(scopeId: number): { sprint_id: number; group_type: GroupType } | null {
309
+ return this.db.prepare(
310
+ `SELECT ss.sprint_id, s.group_type FROM sprint_scopes ss
311
+ JOIN sprints s ON s.id = ss.sprint_id
312
+ WHERE ss.scope_id = ? AND s.status IN ('assembling', 'dispatched', 'in_progress')
313
+ LIMIT 1`,
314
+ ).get(scopeId) as { sprint_id: number; group_type: GroupType } | null;
315
+ }
316
+
317
+ /** Force-remove a scope from a sprint regardless of sprint status.
318
+ * Used for cleanup when a scope's status diverges from the batch target. */
319
+ forceRemoveScope(sprintId: number, scopeId: number): void {
320
+ this.db.prepare('DELETE FROM sprint_scopes WHERE sprint_id = ? AND scope_id = ?')
321
+ .run(sprintId, scopeId);
322
+ this.touchUpdatedAt(sprintId);
323
+ this.emitUpdate(sprintId);
324
+ }
325
+
326
+ /** Get all sprint scopes for a sprint */
327
+ getSprintScopes(sprintId: number): SprintScopeRow[] {
328
+ return this.db.prepare('SELECT * FROM sprint_scopes WHERE sprint_id = ?').all(sprintId) as SprintScopeRow[];
329
+ }
330
+
331
+ /** Store typed dispatch result (commit SHA, PR URL, etc.) for a batch */
332
+ updateDispatchResult(id: number, result: BatchDispatchResult): void {
333
+ this.db.prepare('UPDATE sprints SET dispatch_result = ?, updated_at = ? WHERE id = ?')
334
+ .run(JSON.stringify(result), new Date().toISOString(), id);
335
+ this.emitUpdate(id);
336
+ }
337
+
338
+ /** Check if there's an active (assembling/dispatched/in_progress) batch in the given column */
339
+ findActiveBatchForColumn(targetColumn: GroupTargetColumn): SprintDetail | null {
340
+ const row = this.db.prepare(
341
+ `SELECT * FROM sprints WHERE group_type = 'batch' AND target_column = ? AND status IN ('assembling', 'dispatched', 'in_progress')
342
+ ORDER BY created_at DESC LIMIT 1`,
343
+ ).get(targetColumn) as SprintRow | undefined;
344
+ if (!row) return null;
345
+ return this.buildDetail(row);
346
+ }
347
+
348
+ // ─── Private Helpers ────────────────────────────────────────
349
+
350
+ private buildDetail(row: SprintRow): SprintDetail {
351
+ const ssRows = this.db.prepare(
352
+ `SELECT scope_id, layer, dispatch_status FROM sprint_scopes
353
+ WHERE sprint_id = ? ORDER BY layer ASC, scope_id ASC`,
354
+ ).all(row.id) as Array<{ scope_id: number; layer: number | null; dispatch_status: SprintScopeStatus }>;
355
+
356
+ const progress = { pending: 0, in_progress: 0, completed: 0, failed: 0, skipped: 0 };
357
+ const scopes: SprintDetail['scopes'] = [];
358
+
359
+ for (const ss of ssRows) {
360
+ const scope = this.scopeService.getById(ss.scope_id);
361
+ scopes.push({
362
+ scope_id: ss.scope_id,
363
+ title: scope?.title ?? `Scope ${ss.scope_id}`,
364
+ scope_status: scope?.status ?? 'unknown',
365
+ effort_estimate: scope?.effort_estimate ?? null,
366
+ layer: ss.layer,
367
+ dispatch_status: ss.dispatch_status,
368
+ });
369
+
370
+ const key = ss.dispatch_status === 'dispatched' || ss.dispatch_status === 'queued'
371
+ ? 'in_progress' : ss.dispatch_status;
372
+ if (key in progress) progress[key as keyof typeof progress]++;
373
+ else progress.pending++;
374
+ }
375
+
376
+ let layers: number[][] | null = null;
377
+ try {
378
+ const meta = JSON.parse(row.dispatch_meta || '{}');
379
+ if (meta.layers) layers = meta.layers;
380
+ } catch { /* ignore */ }
381
+
382
+ let dispatchResult: BatchDispatchResult | null = null;
383
+ try {
384
+ const parsed = JSON.parse(row.dispatch_result || '{}');
385
+ if (Object.keys(parsed).length > 0) dispatchResult = parsed;
386
+ } catch { /* ignore */ }
387
+
388
+ return {
389
+ id: row.id,
390
+ name: row.name,
391
+ status: row.status,
392
+ concurrency_cap: row.concurrency_cap,
393
+ group_type: row.group_type ?? 'sprint',
394
+ target_column: row.target_column ?? 'backlog',
395
+ dispatch_result: dispatchResult,
396
+ scope_ids: ssRows.map((ss) => ss.scope_id),
397
+ scopes,
398
+ layers,
399
+ progress,
400
+ created_at: row.created_at,
401
+ updated_at: row.updated_at,
402
+ dispatched_at: row.dispatched_at,
403
+ completed_at: row.completed_at,
404
+ };
405
+ }
406
+
407
+ private touchUpdatedAt(id: number): void {
408
+ this.db.prepare('UPDATE sprints SET updated_at = ? WHERE id = ?').run(new Date().toISOString(), id);
409
+ }
410
+
411
+ private emitUpdate(id: number): void {
412
+ const detail = this.getById(id);
413
+ if (detail) this.io.emit('sprint:updated', detail);
414
+ }
415
+ }