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,156 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { execFileSync } from 'child_process';
4
+ import os from 'os';
5
+ import { createLogger } from './utils/logger.js';
6
+ // ─── Defaults ───────────────────────────────────────────────
7
+ const DEFAULT_CONFIG = {
8
+ projectName: 'Project',
9
+ scopesDir: 'scopes',
10
+ eventsDir: '.claude/orbital-events',
11
+ dbDir: '.claude/orbital',
12
+ configDir: '.claude/config',
13
+ serverPort: 4444,
14
+ clientPort: 4445,
15
+ terminal: {
16
+ adapter: 'auto',
17
+ profilePrefix: 'Orbital',
18
+ },
19
+ claude: {
20
+ executable: 'claude',
21
+ flags: ['--dangerously-skip-permissions'],
22
+ },
23
+ commands: {
24
+ typeCheck: null,
25
+ lint: null,
26
+ build: null,
27
+ test: null,
28
+ validateTemplates: null,
29
+ validateDocs: null,
30
+ checkRules: null,
31
+ },
32
+ logLevel: 'info',
33
+ categories: ['feature', 'bugfix', 'refactor', 'infrastructure', 'docs'],
34
+ agents: [
35
+ { id: 'attacker', label: 'Attacker', emoji: '\u{1F5E1}\u{FE0F}', color: '#ff1744' },
36
+ { id: 'chaos', label: 'Chaos', emoji: '\u{1F4A5}', color: '#F97316' },
37
+ { id: 'solana-expert', label: 'Solana Expert', emoji: '\u{26D3}\u{FE0F}', color: '#8B5CF6' },
38
+ { id: 'frontend-designer', label: 'Frontend Designer', emoji: '\u{1F3A8}', color: '#EC4899' },
39
+ { id: 'architect', label: 'Architect', emoji: '\u{1F3D7}\u{FE0F}', color: '#536dfe' },
40
+ { id: 'rules-enforcer', label: 'Rules Enforcer', emoji: '\u{1F4CB}', color: '#6B7280' },
41
+ ],
42
+ };
43
+ // ─── Project Root Resolution ────────────────────────────────
44
+ /**
45
+ * Resolve the project root directory.
46
+ * Priority: ORBITAL_PROJECT_ROOT env > git rev-parse > cwd walk > cwd
47
+ */
48
+ export function resolveProjectRoot() {
49
+ // 1. Explicit env var
50
+ if (process.env.ORBITAL_PROJECT_ROOT) {
51
+ return path.resolve(process.env.ORBITAL_PROJECT_ROOT);
52
+ }
53
+ // 2. git rev-parse --show-toplevel
54
+ try {
55
+ const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
56
+ encoding: 'utf-8',
57
+ stdio: ['pipe', 'pipe', 'pipe'],
58
+ }).trim();
59
+ if (gitRoot)
60
+ return gitRoot;
61
+ }
62
+ catch {
63
+ // Not in a git repo — continue
64
+ }
65
+ // 3. Walk up from cwd looking for .git directory
66
+ let dir = process.cwd();
67
+ while (dir !== path.dirname(dir)) {
68
+ if (fs.existsSync(path.join(dir, '.git')))
69
+ return dir;
70
+ dir = path.dirname(dir);
71
+ }
72
+ // 4. Fall back to cwd
73
+ return process.cwd();
74
+ }
75
+ // ─── Claude Sessions Path ───────────────────────────────────
76
+ /**
77
+ * Derive the Claude Code sessions directory for a project.
78
+ * Claude Code encodes the project path by replacing `/` with `-`.
79
+ * The leading `/` becomes the leading `-` in the directory name.
80
+ */
81
+ export function getClaudeSessionsDir(projectRoot) {
82
+ const encoded = projectRoot.replace(/\//g, '-');
83
+ return path.join(os.homedir(), '.claude', 'projects', encoded);
84
+ }
85
+ // ─── Config Loading ─────────────────────────────────────────
86
+ /**
87
+ * Load and merge orbital.config.json with defaults.
88
+ * Resolves all relative paths to absolute using projectRoot.
89
+ */
90
+ export function loadConfig(projectRoot) {
91
+ const root = projectRoot ?? resolveProjectRoot();
92
+ // Try loading user config
93
+ const configPath = path.join(root, '.claude', 'orbital.config.json');
94
+ let userConfig = {};
95
+ const log = createLogger('config');
96
+ if (fs.existsSync(configPath)) {
97
+ try {
98
+ userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
99
+ }
100
+ catch (err) {
101
+ log.warn('Failed to parse orbital.config.json — using defaults', { error: err.message });
102
+ }
103
+ }
104
+ // Merge with defaults
105
+ const projectName = userConfig.projectName ?? DEFAULT_CONFIG.projectName;
106
+ const scopesDir = path.resolve(root, userConfig.scopesDir ?? DEFAULT_CONFIG.scopesDir);
107
+ const eventsDir = path.resolve(root, userConfig.eventsDir ?? DEFAULT_CONFIG.eventsDir);
108
+ const dbDir = path.resolve(root, userConfig.dbDir ?? DEFAULT_CONFIG.dbDir);
109
+ const configDir = path.resolve(root, userConfig.configDir ?? DEFAULT_CONFIG.configDir);
110
+ const serverPort = userConfig.serverPort ?? DEFAULT_CONFIG.serverPort;
111
+ const clientPort = userConfig.clientPort ?? DEFAULT_CONFIG.clientPort;
112
+ const terminal = {
113
+ ...DEFAULT_CONFIG.terminal,
114
+ ...(userConfig.terminal ?? {}),
115
+ };
116
+ const claude = {
117
+ ...DEFAULT_CONFIG.claude,
118
+ ...(userConfig.claude ?? {}),
119
+ };
120
+ const commands = {
121
+ ...DEFAULT_CONFIG.commands,
122
+ ...(userConfig.commands ?? {}),
123
+ };
124
+ const logLevel = userConfig.logLevel ?? DEFAULT_CONFIG.logLevel;
125
+ const categories = userConfig.categories ?? DEFAULT_CONFIG.categories;
126
+ const agents = userConfig.agents ?? DEFAULT_CONFIG.agents;
127
+ return {
128
+ projectName,
129
+ projectRoot: root,
130
+ scopesDir,
131
+ eventsDir,
132
+ dbDir,
133
+ configDir,
134
+ serverPort,
135
+ clientPort,
136
+ terminal,
137
+ claude,
138
+ commands,
139
+ logLevel,
140
+ categories,
141
+ agents,
142
+ };
143
+ }
144
+ // ─── Singleton ──────────────────────────────────────────────
145
+ let _config = null;
146
+ /** Get the global config singleton. Lazy-loaded on first access. */
147
+ export function getConfig() {
148
+ if (!_config) {
149
+ _config = loadConfig();
150
+ }
151
+ return _config;
152
+ }
153
+ /** Reset the config singleton (for testing or hot-reload). */
154
+ export function resetConfig() {
155
+ _config = null;
156
+ }
@@ -0,0 +1,90 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { SCHEMA_DDL } from './schema.js';
5
+ import { getConfig } from './config.js';
6
+ import { createLogger } from './utils/logger.js';
7
+ const log = createLogger('database');
8
+ function getDbPaths() {
9
+ const config = getConfig();
10
+ return { dir: config.dbDir, file: path.join(config.dbDir, 'orbital.db') };
11
+ }
12
+ let db = null;
13
+ export function getDatabase() {
14
+ if (db)
15
+ return db;
16
+ const { dir, file } = getDbPaths();
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ db = new Database(file);
19
+ log.info('Database initialized', { path: file });
20
+ // Performance pragmas for a local dev tool
21
+ db.pragma('journal_mode = WAL');
22
+ db.pragma('synchronous = NORMAL');
23
+ db.pragma('foreign_keys = ON');
24
+ // Run schema migrations (SQLite db.exec, not child_process)
25
+ db.exec(SCHEMA_DDL);
26
+ // Incremental migrations for existing databases
27
+ runMigrations(db);
28
+ return db;
29
+ }
30
+ /** Check if a table exists in the database */
31
+ function tableExists(database, tableName) {
32
+ const row = database.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(tableName);
33
+ return row !== undefined;
34
+ }
35
+ /** Run incremental migrations for schema changes on existing databases */
36
+ function runMigrations(database) {
37
+ log.debug('Running database migrations');
38
+ // Migration 2: Add claude_session_id column to sessions
39
+ const sessionCols = database.pragma('table_info(sessions)');
40
+ if (!sessionCols.some((c) => c.name === 'claude_session_id')) {
41
+ database.exec('ALTER TABLE sessions ADD COLUMN claude_session_id TEXT');
42
+ }
43
+ // Migration 6: Add action column to sessions for frontmatter lifecycle phase
44
+ if (!sessionCols.some((c) => c.name === 'action')) {
45
+ database.exec('ALTER TABLE sessions ADD COLUMN action TEXT');
46
+ }
47
+ // Migration 8: Add batch group columns to sprints
48
+ const sprintCols = database.pragma('table_info(sprints)');
49
+ if (!sprintCols.some((c) => c.name === 'target_column')) {
50
+ database.exec("ALTER TABLE sprints ADD COLUMN target_column TEXT DEFAULT 'backlog'");
51
+ database.exec("ALTER TABLE sprints ADD COLUMN group_type TEXT DEFAULT 'sprint'");
52
+ database.exec("ALTER TABLE sprints ADD COLUMN dispatch_result TEXT DEFAULT '{}'");
53
+ database.exec('CREATE INDEX IF NOT EXISTS idx_sprints_target_column ON sprints(target_column)');
54
+ }
55
+ // Migration 7: Drop scopes table — scopes are now served from in-memory cache.
56
+ // Recreate sprint_scopes without the FK to scopes.
57
+ if (tableExists(database, 'scopes')) {
58
+ database.exec(`
59
+ -- Backup sprint_scopes data
60
+ CREATE TABLE IF NOT EXISTS sprint_scopes_backup AS SELECT * FROM sprint_scopes;
61
+ -- Drop tables with FK dependencies first
62
+ DROP TABLE IF EXISTS sprint_scopes;
63
+ -- Drop the scopes table
64
+ DROP TABLE IF EXISTS scopes;
65
+ -- Recreate sprint_scopes without FK to scopes
66
+ CREATE TABLE IF NOT EXISTS sprint_scopes (
67
+ sprint_id INTEGER NOT NULL,
68
+ scope_id INTEGER NOT NULL,
69
+ layer INTEGER,
70
+ dispatch_status TEXT NOT NULL DEFAULT 'pending',
71
+ dispatched_at TEXT,
72
+ completed_at TEXT,
73
+ error TEXT,
74
+ PRIMARY KEY (sprint_id, scope_id),
75
+ FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE CASCADE
76
+ );
77
+ -- Restore data
78
+ INSERT OR IGNORE INTO sprint_scopes SELECT * FROM sprint_scopes_backup;
79
+ -- Cleanup
80
+ DROP TABLE IF EXISTS sprint_scopes_backup;
81
+ `);
82
+ }
83
+ }
84
+ export function closeDatabase() {
85
+ if (db) {
86
+ db.close();
87
+ db = null;
88
+ log.debug('Database closed');
89
+ }
90
+ }
@@ -0,0 +1,372 @@
1
+ import express from 'express';
2
+ import { createServer } from 'http';
3
+ import { Server } from 'socket.io';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+ import { getDatabase, closeDatabase } from './database.js';
8
+ import { getConfig, resetConfig } from './config.js';
9
+ import { ScopeCache } from './services/scope-cache.js';
10
+ import { ScopeService } from './services/scope-service.js';
11
+ import { EventService } from './services/event-service.js';
12
+ import { GateService } from './services/gate-service.js';
13
+ import { DeployService } from './services/deploy-service.js';
14
+ import { SprintService } from './services/sprint-service.js';
15
+ import { SprintOrchestrator } from './services/sprint-orchestrator.js';
16
+ import { BatchOrchestrator } from './services/batch-orchestrator.js';
17
+ import { ReadinessService } from './services/readiness-service.js';
18
+ import { startScopeWatcher } from './watchers/scope-watcher.js';
19
+ import { startEventWatcher } from './watchers/event-watcher.js';
20
+ import { ensureDynamicProfiles } from './utils/terminal-launcher.js';
21
+ import { syncClaudeSessionsToDB } from './services/claude-session-service.js';
22
+ import { resolveStaleDispatches, resolveActiveDispatchesForScope, resolveDispatchesByPid, resolveDispatchesByDispatchId, linkPidToDispatch } from './utils/dispatch-utils.js';
23
+ import { createScopeRoutes } from './routes/scope-routes.js';
24
+ import { createDataRoutes } from './routes/data-routes.js';
25
+ import { createDispatchRoutes } from './routes/dispatch-routes.js';
26
+ import { createSprintRoutes } from './routes/sprint-routes.js';
27
+ import { createWorkflowRoutes } from './routes/workflow-routes.js';
28
+ import { createConfigRoutes } from './routes/config-routes.js';
29
+ import { createGitRoutes } from './routes/git-routes.js';
30
+ import { createVersionRoutes } from './routes/version-routes.js';
31
+ import { WorkflowService } from './services/workflow-service.js';
32
+ import { GitService } from './services/git-service.js';
33
+ import { GitHubService } from './services/github-service.js';
34
+ import { WorkflowEngine } from '../shared/workflow-engine.js';
35
+ import defaultWorkflow from '../shared/default-workflow.json' with { type: 'json' };
36
+ import { createLogger, setLogLevel } from './utils/logger.js';
37
+ // ─── Server Factory ─────────────────────────────────────────
38
+ export async function startServer(overrides) {
39
+ // Apply project root override before config loads
40
+ if (overrides?.projectRoot) {
41
+ process.env.ORBITAL_PROJECT_ROOT = overrides.projectRoot;
42
+ resetConfig();
43
+ }
44
+ const config = getConfig();
45
+ const envLevel = process.env.ORBITAL_LOG_LEVEL;
46
+ if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {
47
+ setLogLevel(envLevel);
48
+ }
49
+ else {
50
+ setLogLevel(config.logLevel);
51
+ }
52
+ const log = createLogger('server');
53
+ const port = overrides?.port ?? config.serverPort;
54
+ const workflowEngine = new WorkflowEngine(defaultWorkflow);
55
+ // Generate shell manifest for bash hooks (config-driven lifecycle)
56
+ const MANIFEST_PATH = path.join(config.configDir, 'workflow-manifest.sh');
57
+ if (!fs.existsSync(config.configDir))
58
+ fs.mkdirSync(config.configDir, { recursive: true });
59
+ fs.writeFileSync(MANIFEST_PATH, workflowEngine.generateShellManifest(), 'utf-8');
60
+ const ICEBOX_DIR = path.join(config.scopesDir, 'icebox');
61
+ // Resolve path to the bundled default workflow config.
62
+ const __selfDir2 = path.dirname(fileURLToPath(import.meta.url));
63
+ const DEFAULT_CONFIG_PATH = path.resolve(__selfDir2, '../shared/default-workflow.json');
64
+ // Ensure icebox directory exists for idea files
65
+ if (!fs.existsSync(ICEBOX_DIR))
66
+ fs.mkdirSync(ICEBOX_DIR, { recursive: true });
67
+ const app = express();
68
+ const httpServer = createServer(app);
69
+ const io = new Server(httpServer, {
70
+ cors: {
71
+ origin: (origin, callback) => {
72
+ // Allow all localhost origins (dev tool, not production)
73
+ if (!origin || origin.startsWith('http://localhost:')) {
74
+ callback(null, true);
75
+ }
76
+ else {
77
+ callback(new Error('CORS not allowed'));
78
+ }
79
+ },
80
+ methods: ['GET', 'POST'],
81
+ },
82
+ });
83
+ // Middleware
84
+ app.use(express.json());
85
+ // Initialize database
86
+ const db = getDatabase();
87
+ // Initialize services
88
+ const scopeCache = new ScopeCache();
89
+ const scopeService = new ScopeService(scopeCache, io, config.scopesDir, workflowEngine);
90
+ const eventService = new EventService(db, io);
91
+ const gateService = new GateService(db, io);
92
+ const deployService = new DeployService(db, io);
93
+ const sprintService = new SprintService(db, io, scopeService);
94
+ const sprintOrchestrator = new SprintOrchestrator(db, io, sprintService, scopeService, workflowEngine);
95
+ const batchOrchestrator = new BatchOrchestrator(db, io, sprintService, scopeService, workflowEngine);
96
+ const readinessService = new ReadinessService(scopeService, gateService, workflowEngine, config.projectRoot);
97
+ const workflowService = new WorkflowService(config.configDir, workflowEngine, config.scopesDir, DEFAULT_CONFIG_PATH);
98
+ workflowService.setSocketServer(io);
99
+ // Ensure in-memory engine reflects the actual active config (may differ from bundled default
100
+ // if the user applied a custom preset)
101
+ workflowEngine.reload(workflowService.getActive());
102
+ const gitService = new GitService(config.projectRoot, scopeCache);
103
+ const githubService = new GitHubService(config.projectRoot);
104
+ // Wire active-group guard into scope service (blocks manual moves for scopes in active batches/sprints)
105
+ scopeService.setActiveGroupCheck((scopeId) => sprintService.getActiveGroupForScope(scopeId));
106
+ // ─── Event Wiring ──────────────────────────────────────────
107
+ function inferScopeStatus(eventType, scopeId, data) {
108
+ if (scopeId == null)
109
+ return;
110
+ const id = Number(scopeId);
111
+ if (isNaN(id) || id <= 0)
112
+ return;
113
+ // Don't infer status for icebox idea cards
114
+ const current = scopeService.getById(id);
115
+ if (current?.status === 'icebox')
116
+ return;
117
+ const currentStatus = current?.status ?? '';
118
+ const result = workflowEngine.inferStatus(eventType, currentStatus, data);
119
+ if (result === null)
120
+ return;
121
+ // Handle dispatch resolution (AGENT_COMPLETED with outcome)
122
+ if (typeof result === 'object' && 'dispatchResolution' in result) {
123
+ resolveActiveDispatchesForScope(db, io, id, result.resolution);
124
+ return;
125
+ }
126
+ scopeService.updateStatus(id, result, 'event');
127
+ }
128
+ eventService.onIngest((eventType, scopeId, data) => {
129
+ // Handle SESSION_START: link PID to dispatch via dispatch_id env var
130
+ if (eventType === 'SESSION_START' && typeof data.dispatch_id === 'string' && typeof data.pid === 'number') {
131
+ linkPidToDispatch(db, data.dispatch_id, data.pid);
132
+ log.info('SESSION_START: linked PID to dispatch', { pid: data.pid, dispatch_id: data.dispatch_id });
133
+ return;
134
+ }
135
+ // Handle SESSION_END: resolve dispatches by dispatch_id (preferred) or PID (fallback)
136
+ if (eventType === 'SESSION_END') {
137
+ let count = 0;
138
+ if (typeof data.dispatch_id === 'string') {
139
+ count = resolveDispatchesByDispatchId(db, io, data.dispatch_id);
140
+ if (count > 0) {
141
+ log.info('SESSION_END: resolved dispatches', { count, dispatch_id: data.dispatch_id });
142
+ }
143
+ }
144
+ // PID fallback for old hooks without dispatch_id
145
+ if (count === 0 && typeof data.pid === 'number') {
146
+ count = resolveDispatchesByPid(db, io, data.pid);
147
+ if (count > 0) {
148
+ log.info('SESSION_END: resolved dispatches by PID fallback', { count, pid: data.pid });
149
+ }
150
+ }
151
+ // Immediately resolve any batches/sprints whose session just ended,
152
+ // rather than waiting for the next stale-check interval
153
+ if (count > 0) {
154
+ batchOrchestrator.resolveStaleBatches();
155
+ }
156
+ return;
157
+ }
158
+ inferScopeStatus(eventType, scopeId, data);
159
+ });
160
+ scopeService.onStatusChange((scopeId, newStatus) => {
161
+ if (newStatus === 'dev') {
162
+ sprintOrchestrator.onScopeReachedDev(scopeId);
163
+ }
164
+ // Batch orchestrator tracks all status transitions (dev, staging, production)
165
+ batchOrchestrator.onScopeStatusChanged(scopeId, newStatus);
166
+ });
167
+ scopeService.onStatusChange((scopeId, newStatus) => {
168
+ if (workflowEngine.isTerminalStatus(newStatus)) {
169
+ resolveActiveDispatchesForScope(db, io, scopeId, 'completed');
170
+ }
171
+ });
172
+ // ─── Routes ────────────────────────────────────────────────
173
+ app.get('/api/orbital/health', (_req, res) => {
174
+ res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
175
+ });
176
+ // Serve dynamic config to the frontend
177
+ app.get('/api/orbital/config', (_req, res) => {
178
+ res.json({
179
+ projectName: config.projectName,
180
+ categories: config.categories,
181
+ agents: config.agents,
182
+ serverPort: config.serverPort,
183
+ clientPort: config.clientPort,
184
+ });
185
+ });
186
+ app.use('/api/orbital', createScopeRoutes({ db, io, scopeService, readinessService, projectRoot: config.projectRoot, engine: workflowEngine }));
187
+ app.use('/api/orbital', createDataRoutes({ db, io, gateService, deployService, engine: workflowEngine, projectRoot: config.projectRoot, inferScopeStatus }));
188
+ app.use('/api/orbital', createDispatchRoutes({ db, io, scopeService, projectRoot: config.projectRoot, engine: workflowEngine }));
189
+ app.use('/api/orbital', createSprintRoutes({ sprintService, sprintOrchestrator, batchOrchestrator }));
190
+ app.use('/api/orbital', createWorkflowRoutes({ workflowService, projectRoot: config.projectRoot }));
191
+ app.use('/api/orbital', createConfigRoutes({ projectRoot: config.projectRoot, workflowService, io }));
192
+ app.use('/api/orbital', createGitRoutes({ gitService, githubService, engine: workflowEngine }));
193
+ app.use('/api/orbital', createVersionRoutes({ io }));
194
+ // ─── Static File Serving (production) ───────────────────────
195
+ // Resolve the Vite-built frontend dist directory (server/ → ../dist).
196
+ const __selfDir = path.dirname(fileURLToPath(import.meta.url));
197
+ const distDir = path.resolve(__selfDir, '../dist');
198
+ if (fs.existsSync(path.join(distDir, 'index.html'))) {
199
+ app.use(express.static(distDir));
200
+ app.get('*', (req, res, next) => {
201
+ if (req.path.startsWith('/api/') || req.path.startsWith('/socket.io'))
202
+ return next();
203
+ res.sendFile(path.join(distDir, 'index.html'));
204
+ });
205
+ }
206
+ else {
207
+ // Dev mode: redirect root to Vite dev server
208
+ app.get('/', (_req, res) => res.redirect(`http://localhost:${config.clientPort}`));
209
+ }
210
+ // ─── Socket.io ──────────────────────────────────────────────
211
+ io.on('connection', (socket) => {
212
+ log.debug('Client connected', { socketId: socket.id });
213
+ socket.on('disconnect', () => {
214
+ log.debug('Client disconnected', { socketId: socket.id });
215
+ });
216
+ });
217
+ // ─── Startup ───────────────────────────────────────────────
218
+ // References for graceful shutdown
219
+ let scopeWatcher;
220
+ let eventWatcher;
221
+ let batchRecoveryInterval;
222
+ let staleCleanupInterval;
223
+ let sessionSyncInterval;
224
+ let gitPollInterval;
225
+ const actualPort = await new Promise((resolve, reject) => {
226
+ let attempt = 0;
227
+ const maxAttempts = 10;
228
+ httpServer.on('error', (err) => {
229
+ if (err.code === 'EADDRINUSE' && attempt < maxAttempts) {
230
+ attempt++;
231
+ const nextPort = port + attempt;
232
+ log.warn('Port in use, trying next', { tried: port + attempt - 1, next: nextPort });
233
+ httpServer.listen(nextPort);
234
+ }
235
+ else {
236
+ reject(new Error(`Failed to start server: ${err.message}`));
237
+ }
238
+ });
239
+ httpServer.on('listening', () => {
240
+ const addr = httpServer.address();
241
+ const listenPort = typeof addr === 'object' && addr ? addr.port : port;
242
+ resolve(listenPort);
243
+ });
244
+ httpServer.listen(port);
245
+ });
246
+ // ─── Post-listen initialization ────────────────────────────
247
+ // Sync scopes from filesystem on startup (populates in-memory cache)
248
+ const scopeCount = scopeService.syncFromFilesystem();
249
+ // Resolve stale dispatch events (terminal scopes + age-based)
250
+ const staleResolved = resolveStaleDispatches(db, io, scopeService, workflowEngine);
251
+ if (staleResolved > 0) {
252
+ log.info('Resolved stale dispatch events', { count: staleResolved });
253
+ }
254
+ // Write iTerm2 dispatch profiles (idempotent, fire-and-forget)
255
+ ensureDynamicProfiles(workflowEngine);
256
+ // Start file watchers
257
+ scopeWatcher = startScopeWatcher(config.scopesDir, scopeService);
258
+ eventWatcher = startEventWatcher(config.eventsDir, eventService);
259
+ // Recover any active sprints/batches from before server restart
260
+ sprintOrchestrator.recoverActiveSprints().catch(err => log.error('Sprint recovery failed', { error: err.message }));
261
+ batchOrchestrator.recoverActiveBatches().catch(err => log.error('Batch recovery failed', { error: err.message }));
262
+ // Resolve stale batches on startup (catches stuck dispatches from previous runs)
263
+ const staleBatchesResolved = batchOrchestrator.resolveStaleBatches();
264
+ if (staleBatchesResolved > 0) {
265
+ log.info('Resolved stale batches', { count: staleBatchesResolved });
266
+ }
267
+ // Poll active batch PIDs every 30s for two-phase completion (B-1)
268
+ batchRecoveryInterval = setInterval(() => {
269
+ batchOrchestrator.recoverActiveBatches().catch(err => log.error('Batch recovery failed', { error: err.message }));
270
+ }, 30_000);
271
+ // Periodic stale dispatch + batch cleanup (crash recovery — catches SIGKILL'd sessions)
272
+ staleCleanupInterval = setInterval(() => {
273
+ const count = resolveStaleDispatches(db, io, scopeService, workflowEngine);
274
+ if (count > 0) {
275
+ log.info('Periodic cleanup: resolved stale dispatches', { count });
276
+ }
277
+ const batchCount = batchOrchestrator.resolveStaleBatches();
278
+ if (batchCount > 0) {
279
+ log.info('Periodic cleanup: resolved stale batches', { count: batchCount });
280
+ }
281
+ }, 30_000);
282
+ // Sync frontmatter-derived sessions into DB (non-blocking)
283
+ syncClaudeSessionsToDB(db, scopeService).then((count) => {
284
+ log.info('Synced frontmatter sessions', { count });
285
+ // Purge legacy pattern-matched rows (no action = old regex system)
286
+ const purged = db.prepare("DELETE FROM sessions WHERE action IS NULL AND id LIKE 'claude-%'").run();
287
+ if (purged.changes > 0) {
288
+ log.info('Purged legacy pattern-matched session rows', { count: purged.changes });
289
+ }
290
+ }).catch(err => log.error('Session sync failed', { error: err.message }));
291
+ // Re-sync every 5 minutes so new sessions appear without restart
292
+ sessionSyncInterval = setInterval(() => {
293
+ syncClaudeSessionsToDB(db, scopeService)
294
+ .then((count) => {
295
+ if (count > 0)
296
+ io.emit('session:updated', { type: 'resync', count });
297
+ })
298
+ .catch(err => log.error('Session resync failed', { error: err.message }));
299
+ }, 5 * 60 * 1000);
300
+ // Poll git status every 10s — emit socket event on change
301
+ let lastGitHash = '';
302
+ gitPollInterval = setInterval(async () => {
303
+ try {
304
+ const hash = await gitService.getStatusHash();
305
+ if (lastGitHash && hash !== lastGitHash) {
306
+ gitService.clearCache();
307
+ io.emit('git:status:changed');
308
+ }
309
+ lastGitHash = hash;
310
+ }
311
+ catch { /* ok */ }
312
+ }, 10_000);
313
+ // eslint-disable-next-line no-console
314
+ console.log(`
315
+ ╔══════════════════════════════════════════════════════╗
316
+ ║ Orbital Command ║
317
+ ║ ${config.projectName.padEnd(42)} ║
318
+ ║ ║
319
+ ║ >>> Open: http://localhost:${actualPort} <<< ║
320
+ ║ ║
321
+ ╠══════════════════════════════════════════════════════╣
322
+ ║ Scopes: ${String(scopeCount).padEnd(3)} loaded from filesystem ║
323
+ ║ API: http://localhost:${actualPort}/api/orbital/* ║
324
+ ║ Socket.io: ws://localhost:${actualPort} ║
325
+ ╚══════════════════════════════════════════════════════╝
326
+ `);
327
+ // ─── Graceful Shutdown ─────────────────────────────────────
328
+ let shuttingDown = false;
329
+ function shutdown() {
330
+ if (shuttingDown)
331
+ return Promise.resolve();
332
+ shuttingDown = true;
333
+ log.info('Shutting down');
334
+ scopeWatcher.close();
335
+ eventWatcher.close();
336
+ clearInterval(batchRecoveryInterval);
337
+ clearInterval(staleCleanupInterval);
338
+ clearInterval(sessionSyncInterval);
339
+ clearInterval(gitPollInterval);
340
+ return new Promise((resolve) => {
341
+ const forceTimeout = setTimeout(() => {
342
+ closeDatabase();
343
+ resolve();
344
+ }, 2000);
345
+ io.close(() => {
346
+ clearTimeout(forceTimeout);
347
+ closeDatabase();
348
+ resolve();
349
+ });
350
+ });
351
+ }
352
+ return { app, io, db, workflowEngine, httpServer, shutdown };
353
+ }
354
+ // ─── Direct Execution (backward compat: tsx watch server/index.ts) ───
355
+ const isDirectRun = process.argv[1] && (process.argv[1].endsWith('server/index.ts') ||
356
+ process.argv[1].endsWith('server/index.js') ||
357
+ process.argv[1].endsWith('server'));
358
+ if (isDirectRun) {
359
+ startServer().then(({ shutdown }) => {
360
+ process.on('SIGINT', async () => {
361
+ await shutdown();
362
+ process.exit(0);
363
+ });
364
+ process.on('SIGTERM', async () => {
365
+ await shutdown();
366
+ process.exit(0);
367
+ });
368
+ }).catch((err) => {
369
+ createLogger('server').error('Failed to start server', { error: err.message });
370
+ process.exit(1);
371
+ });
372
+ }