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,286 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { Server } from 'socket.io';
3
+ import type { SprintService } from './sprint-service.js';
4
+ import type { ScopeService } from './scope-service.js';
5
+ import { launchInCategorizedTerminal, escapeForAnsiC, snapshotSessionPids, discoverNewSession, isSessionPidAlive } from '../utils/terminal-launcher.js';
6
+ import { linkPidToDispatch, resolveDispatchEvent } 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('batch');
12
+ const VALID_MERGE_MODES = ['push', 'pr'] as const;
13
+
14
+ // ─── Orchestrator ───────────────────────────────────────────
15
+
16
+ export class BatchOrchestrator {
17
+ constructor(
18
+ private db: Database.Database,
19
+ private io: Server,
20
+ private sprintService: SprintService,
21
+ private scopeService: ScopeService,
22
+ private engine: WorkflowEngine,
23
+ ) {}
24
+
25
+ /** Dispatch a batch — validates constraints and routes to column-specific handler */
26
+ async dispatch(batchId: number, mergeMode?: string): Promise<{ ok: boolean; error?: string }> {
27
+ const batch = this.sprintService.getById(batchId);
28
+ if (!batch) return { ok: false, error: 'Batch not found' };
29
+ if (batch.group_type !== 'batch') return { ok: false, error: 'Not a batch group' };
30
+ if (batch.status !== 'assembling') return { ok: false, error: `Batch status is '${batch.status}', expected 'assembling'` };
31
+ if (batch.scope_ids.length === 0) return { ok: false, error: 'Batch has no scopes' };
32
+
33
+ // W-4: One active batch per column
34
+ const existingActive = this.sprintService.findActiveBatchForColumn(batch.target_column);
35
+ if (existingActive && existingActive.id !== batchId) {
36
+ return { ok: false, error: `Column '${batch.target_column}' already has an active batch (ID: ${existingActive.id})` };
37
+ }
38
+
39
+ const command = this.engine.getBatchCommand(batch.target_column);
40
+ if (!command) return { ok: false, error: `No dispatch command for column '${batch.target_column}'` };
41
+
42
+ // Mark batch as dispatched
43
+ this.sprintService.updateStatus(batchId, 'dispatched');
44
+ log.info('Batch dispatched', { id: batchId, target_column: batch.target_column, scope_ids: batch.scope_ids });
45
+
46
+ // Build scope IDs env var prefix (W-1: prepend to command, not process.env)
47
+ const scopeIdsStr = batch.scope_ids.join(',');
48
+ const mergeModeStr = (VALID_MERGE_MODES as readonly string[]).includes(mergeMode ?? '') ? mergeMode! : 'push';
49
+
50
+ // Record DISPATCH event
51
+ const eventId = crypto.randomUUID();
52
+ const eventData = {
53
+ command,
54
+ batch_id: batchId,
55
+ scope_ids: batch.scope_ids,
56
+ target_column: batch.target_column,
57
+ batch: true,
58
+ resolved: null,
59
+ };
60
+ this.db.prepare(
61
+ `INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
62
+ VALUES (?, 'DISPATCH', NULL, NULL, 'batch-orchestrator', ?, ?)`,
63
+ ).run(eventId, JSON.stringify(eventData), new Date().toISOString());
64
+
65
+ this.io.emit('event:new', {
66
+ id: eventId, type: 'DISPATCH', scope_id: null,
67
+ session_id: null, agent: 'batch-orchestrator',
68
+ data: eventData, timestamp: new Date().toISOString(),
69
+ });
70
+
71
+ // Launch single CLI session with BATCH_SCOPE_IDS prepended to command
72
+ const escaped = escapeForAnsiC(command);
73
+ const fullCmd = `cd '${getConfig().projectRoot}' && BATCH_SCOPE_IDS='${scopeIdsStr}' MERGE_MODE='${mergeModeStr}' claude --dangerously-skip-permissions $'${escaped}'`;
74
+ const beforePids = snapshotSessionPids(getConfig().projectRoot);
75
+
76
+ try {
77
+ await launchInCategorizedTerminal(command, fullCmd);
78
+
79
+ // Store dispatch result timestamp
80
+ this.sprintService.updateDispatchResult(batchId, {
81
+ dispatched_at: new Date().toISOString(),
82
+ });
83
+
84
+ // Fire-and-forget: discover session PID and link to dispatch
85
+ discoverNewSession(getConfig().projectRoot, beforePids)
86
+ .then((session) => {
87
+ if (!session) return;
88
+ linkPidToDispatch(this.db, eventId, session.pid);
89
+ // Store PID on the batch for two-phase completion
90
+ const currentResult = this.sprintService.getById(batchId)?.dispatch_result ?? {};
91
+ this.sprintService.updateDispatchResult(batchId, {
92
+ ...currentResult,
93
+ dispatched_at: currentResult.dispatched_at ?? new Date().toISOString(),
94
+ });
95
+ // Store PID in event data for later liveness checking
96
+ const row = this.db.prepare('SELECT data FROM events WHERE id = ?').get(eventId) as { data: string } | undefined;
97
+ if (row) {
98
+ const data = JSON.parse(row.data);
99
+ data.pid = session.pid;
100
+ this.db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
101
+ }
102
+ })
103
+ .catch(err => log.error('PID discovery failed', { error: err.message }));
104
+
105
+ return { ok: true };
106
+ } catch (err) {
107
+ this.sprintService.updateStatus(batchId, 'failed');
108
+ resolveDispatchEvent(this.db, this.io, eventId, 'failed', String(err));
109
+ return { ok: false, error: `Failed to launch terminal: ${err}` };
110
+ }
111
+ }
112
+
113
+ /** Called when a scope reaches a new status — check if it satisfies a batch,
114
+ * or remove the scope from the batch if its status diverged from the target. */
115
+ onScopeStatusChanged(scopeId: number, newStatus: string): void {
116
+ // Find any active batch containing this scope
117
+ const match = this.sprintService.findActiveSprintForScope(scopeId);
118
+ if (!match) return;
119
+
120
+ const batch = this.sprintService.getById(match.sprint_id);
121
+ if (!batch || batch.group_type !== 'batch') return;
122
+
123
+ const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
124
+ if (newStatus === expectedStatus || this.engine.isTerminalStatus(newStatus)) {
125
+ this.sprintService.updateScopeStatus(batch.id, scopeId, 'completed');
126
+
127
+ // Check if all scopes have transitioned
128
+ if (batch.status === 'dispatched') {
129
+ this.sprintService.updateStatus(batch.id, 'in_progress');
130
+ }
131
+ } else if (newStatus !== batch.target_column) {
132
+ // Scope diverged from batch target — remove it from the batch
133
+ this.sprintService.forceRemoveScope(batch.id, scopeId);
134
+
135
+ // If batch is now empty, mark it as failed
136
+ const remaining = this.sprintService.getSprintScopes(batch.id);
137
+ if (remaining.length === 0 && batch.status !== 'assembling') {
138
+ this.sprintService.updateStatus(batch.id, 'failed');
139
+ }
140
+ }
141
+ }
142
+
143
+ /** Called when a dispatched session PID dies — second phase of two-phase completion.
144
+ * Reverts un-transitioned scopes to their pre-dispatch status. */
145
+ onSessionPidDied(batchId: number): void {
146
+ const batch = this.sprintService.getById(batchId);
147
+ if (!batch || batch.group_type !== 'batch') return;
148
+ if (batch.status !== 'dispatched' && batch.status !== 'in_progress') return;
149
+
150
+ const scopes = this.sprintService.getSprintScopes(batchId);
151
+ const allTransitioned = scopes.every((ss) => ss.dispatch_status === 'completed');
152
+
153
+ if (allTransitioned) {
154
+ this.sprintService.updateStatus(batchId, 'completed');
155
+ } else {
156
+ const pending = scopes.filter((ss) => ss.dispatch_status !== 'completed').map((ss) => ss.scope_id);
157
+ this.sprintService.updateStatus(batchId, 'failed');
158
+ // Mark un-transitioned scopes as failed and revert their status
159
+ for (const scopeId of pending) {
160
+ this.sprintService.updateScopeStatus(batchId, scopeId, 'failed', 'Session exited before scope transitioned');
161
+ // Revert scope to pre-dispatch status (the batch's source column)
162
+ this.scopeService.updateStatus(scopeId, batch.target_column, 'rollback');
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Resolve stale batches — catches batches stuck due to lost PIDs, Orbital downtime, or
169
+ * missing PID records. Unlike recoverActiveBatches (which focuses on PID polling),
170
+ * this also resolves batches where no PID was ever recorded.
171
+ */
172
+ resolveStaleBatches(): number {
173
+ const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
174
+
175
+ const active = this.db.prepare(
176
+ `SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`,
177
+ ).all() as Array<{ id: number }>;
178
+
179
+ if (active.length > 0) {
180
+ log.debug('Checking stale batches', { activeCount: active.length });
181
+ }
182
+
183
+ let resolved = 0;
184
+
185
+ for (const { id } of active) {
186
+ const batch = this.sprintService.getById(id);
187
+ if (!batch) continue;
188
+
189
+ const scopes = this.sprintService.getSprintScopes(id);
190
+ const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
191
+
192
+ // Phase 1: auto-complete scopes that reached or passed target status
193
+ for (const ss of scopes) {
194
+ if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
195
+ const scope = this.scopeService.getById(ss.scope_id);
196
+ if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
197
+ this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
198
+ }
199
+ }
200
+ }
201
+
202
+ // Phase 2: check PID liveness (check both unresolved and resolved events —
203
+ // SESSION_END may have resolved the dispatch event before we get here)
204
+ const dispatchEvent = this.db.prepare(
205
+ `SELECT data FROM events
206
+ WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
207
+ ORDER BY timestamp DESC LIMIT 1`,
208
+ ).get(id) as { data: string } | undefined;
209
+
210
+ let pidDead = false;
211
+
212
+ if (dispatchEvent) {
213
+ const data = JSON.parse(dispatchEvent.data) as Record<string, unknown>;
214
+ // If the dispatch event is already resolved, the session is definitely done
215
+ if (data.resolved != null) {
216
+ pidDead = true;
217
+ } else if (typeof data.pid === 'number') {
218
+ pidDead = !isSessionPidAlive(data.pid);
219
+ } else {
220
+ // No PID recorded — check if batch is old enough to consider stale
221
+ const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
222
+ pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
223
+ }
224
+ } else {
225
+ // No dispatch event at all — check age
226
+ const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
227
+ pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
228
+ }
229
+
230
+ if (pidDead) {
231
+ this.onSessionPidDied(id);
232
+ resolved++;
233
+ }
234
+ }
235
+
236
+ return resolved;
237
+ }
238
+
239
+ /** Recover active batches after server restart (W-3) */
240
+ async recoverActiveBatches(): Promise<void> {
241
+ const active = this.db.prepare(
242
+ `SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`,
243
+ ).all() as Array<{ id: number }>;
244
+
245
+ if (active.length > 0) {
246
+ log.debug('Recovering active batches', { count: active.length });
247
+ }
248
+
249
+ for (const { id } of active) {
250
+ const batch = this.sprintService.getById(id);
251
+ if (!batch) continue;
252
+
253
+ const scopes = this.sprintService.getSprintScopes(id);
254
+ const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
255
+
256
+ // Check if scopes reached or passed target status while server was down
257
+ for (const ss of scopes) {
258
+ if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
259
+ const scope = this.scopeService.getById(ss.scope_id);
260
+ if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
261
+ this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
262
+ }
263
+ }
264
+ }
265
+
266
+ // Check if dispatch PID is still alive (include resolved events —
267
+ // SESSION_END may have resolved the dispatch before server restart)
268
+ const dispatchEvent = this.db.prepare(
269
+ `SELECT data FROM events
270
+ WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
271
+ ORDER BY timestamp DESC LIMIT 1`,
272
+ ).get(id) as { data: string } | undefined;
273
+
274
+ if (dispatchEvent) {
275
+ const data = JSON.parse(dispatchEvent.data);
276
+ if (data.resolved != null) {
277
+ // Dispatch already resolved — session is done
278
+ this.onSessionPidDied(id);
279
+ } else if (typeof data.pid === 'number' && !isSessionPidAlive(data.pid)) {
280
+ // PID is dead — trigger two-phase completion check
281
+ this.onSessionPidDied(id);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ }