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,395 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { Server } from 'socket.io';
3
+ import type { ScopeService } from '../services/scope-service.js';
4
+ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
5
+ import { isSessionPidAlive } from './terminal-launcher.js';
6
+ import { createLogger } from './logger.js';
7
+
8
+ const log = createLogger('dispatch');
9
+
10
+ interface DispatchRow {
11
+ data: string;
12
+ scope_id: number | null;
13
+ }
14
+
15
+ /** Mark a DISPATCH event as resolved and emit socket notification. */
16
+ export function resolveDispatchEvent(
17
+ db: Database.Database,
18
+ io: Server,
19
+ eventId: string,
20
+ outcome: 'completed' | 'failed' | 'abandoned',
21
+ error?: string,
22
+ ): void {
23
+ const row = db.prepare('SELECT data, scope_id FROM events WHERE id = ?')
24
+ .get(eventId) as DispatchRow | undefined;
25
+ if (!row) return;
26
+
27
+ let data: Record<string, unknown>;
28
+ try {
29
+ data = JSON.parse(row.data);
30
+ } catch (e) {
31
+ log.error('Failed to parse DISPATCH event data', { eventId, error: String(e) });
32
+ return;
33
+ }
34
+ data.resolved = { outcome, at: new Date().toISOString(), ...(error ? { error } : {}) };
35
+ db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
36
+
37
+ io.emit('dispatch:resolved', {
38
+ event_id: eventId,
39
+ scope_id: row.scope_id,
40
+ scope_ids: data.scope_ids ?? null,
41
+ outcome,
42
+ });
43
+ }
44
+
45
+ /** Resolve all unresolved DISPATCH events for a given scope */
46
+ export function resolveActiveDispatchesForScope(
47
+ db: Database.Database,
48
+ io: Server,
49
+ scopeId: number,
50
+ outcome: 'completed' | 'failed' | 'abandoned',
51
+ ): void {
52
+ const rows = db.prepare(
53
+ `SELECT id FROM events
54
+ WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
55
+ ).all(scopeId) as Array<{ id: string }>;
56
+
57
+ for (const row of rows) {
58
+ resolveDispatchEvent(db, io, row.id, outcome);
59
+ }
60
+ }
61
+
62
+ /** Re-resolve abandoned DISPATCH events for a scope as completed.
63
+ * Used by both recover and dismiss-abandoned routes to clear abandoned state. */
64
+ export function resolveAbandonedDispatchesForScope(
65
+ db: Database.Database,
66
+ io: Server,
67
+ scopeId: number,
68
+ ): number {
69
+ const rows = db.prepare(
70
+ `SELECT id FROM events
71
+ WHERE type = 'DISPATCH' AND scope_id = ?
72
+ AND JSON_EXTRACT(data, '$.resolved.outcome') = 'abandoned'`,
73
+ ).all(scopeId) as Array<{ id: string }>;
74
+
75
+ for (const row of rows) {
76
+ resolveDispatchEvent(db, io, row.id, 'completed');
77
+ }
78
+
79
+ return rows.length;
80
+ }
81
+
82
+ /** Store the PID of the Claude session working on a dispatch.
83
+ * Called after discoverNewSession finds the launched session, or when
84
+ * a SESSION_START event includes ORBITAL_DISPATCH_ID from the env var. */
85
+ export function linkPidToDispatch(
86
+ db: Database.Database,
87
+ eventId: string,
88
+ pid: number,
89
+ ): void {
90
+ const row = db.prepare('SELECT data FROM events WHERE id = ?')
91
+ .get(eventId) as { data: string } | undefined;
92
+ if (!row) return;
93
+ let data: Record<string, unknown>;
94
+ try {
95
+ data = JSON.parse(row.data);
96
+ } catch (e) {
97
+ log.error('Failed to parse DISPATCH event data', { eventId, error: String(e) });
98
+ return;
99
+ }
100
+ data.pid = pid;
101
+ db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
102
+ }
103
+
104
+ /** Resolve all unresolved DISPATCH events linked to a specific PID.
105
+ * Called when a SESSION_END event is received, indicating the Claude session
106
+ * process has exited and its dispatches should be cleared.
107
+ *
108
+ * NOTE: Does NOT revert scope status. Skills like /scope-implement intentionally
109
+ * keep scopes at the transition target (e.g. "implementing") after completion.
110
+ * Reverting on session end was destroying completed work and deleting scope files. */
111
+ export function resolveDispatchesByPid(
112
+ db: Database.Database,
113
+ io: Server,
114
+ pid: number,
115
+ ): number {
116
+ const rows = db.prepare(
117
+ `SELECT id FROM events
118
+ WHERE type = 'DISPATCH'
119
+ AND JSON_EXTRACT(data, '$.resolved') IS NULL
120
+ AND JSON_EXTRACT(data, '$.pid') = ?`,
121
+ ).all(pid) as Array<{ id: string }>;
122
+
123
+ for (const row of rows) {
124
+ resolveDispatchEvent(db, io, row.id, 'abandoned');
125
+ }
126
+
127
+ return rows.length;
128
+ }
129
+
130
+ /** Resolve all unresolved DISPATCH events linked to a specific dispatch ID.
131
+ * Called when a SESSION_END event includes dispatch_id from ORBITAL_DISPATCH_ID env var.
132
+ * Defaults to 'abandoned' — successful completions emit AGENT_COMPLETED first
133
+ * which resolves via inferScopeStatus as 'completed'. */
134
+ export function resolveDispatchesByDispatchId(
135
+ db: Database.Database,
136
+ io: Server,
137
+ dispatchId: string,
138
+ ): number {
139
+ const row = db.prepare(
140
+ `SELECT id FROM events
141
+ WHERE id = ? AND type = 'DISPATCH' AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
142
+ ).get(dispatchId) as { id: string } | undefined;
143
+
144
+ if (!row) return 0;
145
+ resolveDispatchEvent(db, io, row.id, 'abandoned');
146
+ return 1;
147
+ }
148
+
149
+ /** Fallback age threshold for dispatches without a linked PID (30 minutes). */
150
+ const STALE_AGE_MS = 30 * 60 * 1000;
151
+
152
+ /** Get all scope IDs that have actively running DISPATCH events.
153
+ * Uses PID liveness (process.kill(pid, 0)) when available, falls back to
154
+ * age-based heuristic for legacy dispatches without a linked PID. */
155
+ export function getActiveScopeIds(db: Database.Database, scopeService: ScopeService, engine: WorkflowEngine): number[] {
156
+ const rows = db.prepare(
157
+ `SELECT scope_id, data FROM events
158
+ WHERE type = 'DISPATCH'
159
+ AND scope_id IS NOT NULL
160
+ AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
161
+ ).all() as Array<{ scope_id: number; data: string }>;
162
+
163
+ const cutoff = new Date(Date.now() - STALE_AGE_MS).toISOString();
164
+ const active = new Set<number>();
165
+
166
+ for (const row of rows) {
167
+ if (active.has(row.scope_id)) continue; // already confirmed active
168
+
169
+ // Skip scopes in terminal states
170
+ const scope = scopeService.getById(row.scope_id);
171
+ if (scope && engine.isTerminalStatus(scope.status)) continue;
172
+
173
+ let data: Record<string, unknown>;
174
+ try {
175
+ data = JSON.parse(row.data);
176
+ } catch (e) {
177
+ log.error('Failed to parse DISPATCH event data', { scope_id: row.scope_id, error: String(e) });
178
+ continue;
179
+ }
180
+ if (typeof data.pid === 'number') {
181
+ // Preferred: check if the Claude session process is still running
182
+ if (isSessionPidAlive(data.pid)) {
183
+ active.add(row.scope_id);
184
+ }
185
+ } else {
186
+ // Fallback for legacy dispatches without PID: use age-based check
187
+ const dispatch = db.prepare(
188
+ `SELECT timestamp FROM events
189
+ WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
190
+ ORDER BY timestamp DESC LIMIT 1`,
191
+ ).get(row.scope_id) as { timestamp: string } | undefined;
192
+ if (dispatch && dispatch.timestamp > cutoff) {
193
+ active.add(row.scope_id);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Also check batch dispatches (scope_id IS NULL, batch = true)
199
+ const batchRows = db.prepare(
200
+ `SELECT data FROM events
201
+ WHERE type = 'DISPATCH'
202
+ AND scope_id IS NULL
203
+ AND JSON_EXTRACT(data, '$.batch') = 1
204
+ AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
205
+ ).all() as Array<{ data: string }>;
206
+
207
+ for (const batchRow of batchRows) {
208
+ let batchData: Record<string, unknown>;
209
+ try {
210
+ batchData = JSON.parse(batchRow.data);
211
+ } catch {
212
+ continue;
213
+ }
214
+
215
+ const scopeIds = batchData.scope_ids as number[] | undefined;
216
+ if (!Array.isArray(scopeIds)) continue;
217
+
218
+ let batchAlive = false;
219
+ if (typeof batchData.pid === 'number') {
220
+ batchAlive = isSessionPidAlive(batchData.pid);
221
+ } else {
222
+ // No PID — consider active (stale cleanup will catch it)
223
+ batchAlive = true;
224
+ }
225
+
226
+ if (batchAlive) {
227
+ for (const id of scopeIds) {
228
+ const scope = scopeService.getById(id);
229
+ if (scope && !engine.isTerminalStatus(scope.status)) {
230
+ active.add(id);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ return [...active];
237
+ }
238
+
239
+ /** Resolve stale DISPATCH events. Three staleness criteria:
240
+ * 1. Scope already in a terminal state (as defined by workflow config)
241
+ * 2. Linked PID is no longer running (session ended/crashed)
242
+ * 3. No linked PID and dispatch older than STALE_AGE_MS (fallback)
243
+ * Called once at startup and periodically to clean up unresolved dispatches.
244
+ *
245
+ * NOTE: Does NOT revert scope status. Skills like /scope-implement intentionally
246
+ * keep scopes at the transition target after completion. Auto-reverting was
247
+ * destroying completed work and deleting scope files. Users can manually
248
+ * move scopes back from the dashboard if needed. */
249
+ export function resolveStaleDispatches(db: Database.Database, io: Server, scopeService: ScopeService, engine: WorkflowEngine): number {
250
+ const cutoff = new Date(Date.now() - STALE_AGE_MS).toISOString();
251
+
252
+ // Single query on events only — split by cache status
253
+ const rows = db.prepare(
254
+ `SELECT id, scope_id, data, timestamp FROM events
255
+ WHERE type = 'DISPATCH'
256
+ AND scope_id IS NOT NULL
257
+ AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
258
+ ).all() as Array<{ id: string; scope_id: number; data: string; timestamp: string }>;
259
+
260
+ let resolved = 0;
261
+
262
+ for (const row of rows) {
263
+ const scope = scopeService.getById(row.scope_id);
264
+ const scopeStatus = scope?.status;
265
+
266
+ // Criterion 1: scope in terminal state
267
+ if (scopeStatus && engine.isTerminalStatus(scopeStatus)) {
268
+ resolveDispatchEvent(db, io, row.id, 'completed');
269
+ resolved++;
270
+ continue;
271
+ }
272
+
273
+ // Criteria 2+3: dead PID or old age
274
+ let data: Record<string, unknown>;
275
+ try {
276
+ data = JSON.parse(row.data);
277
+ } catch (e) {
278
+ log.error('Failed to parse DISPATCH event data', { eventId: row.id, error: String(e) });
279
+ continue;
280
+ }
281
+ let isStale = false;
282
+
283
+ if (typeof data.pid === 'number') {
284
+ isStale = !isSessionPidAlive(data.pid);
285
+ } else {
286
+ isStale = row.timestamp <= cutoff;
287
+ }
288
+
289
+ if (isStale) {
290
+ resolveDispatchEvent(db, io, row.id, 'abandoned');
291
+ resolved++;
292
+ }
293
+ }
294
+
295
+ // Second pass: batch dispatches (scope_id IS NULL, batch = true)
296
+ const batchRows = db.prepare(
297
+ `SELECT id, data, timestamp FROM events
298
+ WHERE type = 'DISPATCH'
299
+ AND scope_id IS NULL
300
+ AND JSON_EXTRACT(data, '$.batch') = 1
301
+ AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
302
+ ).all() as Array<{ id: string; data: string; timestamp: string }>;
303
+
304
+ for (const batchRow of batchRows) {
305
+ let batchData: Record<string, unknown>;
306
+ try {
307
+ batchData = JSON.parse(batchRow.data);
308
+ } catch {
309
+ continue;
310
+ }
311
+
312
+ const scopeIds = batchData.scope_ids as number[] | undefined;
313
+
314
+ // Criterion 1: all batch scopes in terminal state
315
+ if (Array.isArray(scopeIds) && scopeIds.length > 0) {
316
+ const allTerminal = scopeIds.every(id => {
317
+ const scope = scopeService.getById(id);
318
+ return scope && engine.isTerminalStatus(scope.status);
319
+ });
320
+ if (allTerminal) {
321
+ resolveDispatchEvent(db, io, batchRow.id, 'completed');
322
+ resolved++;
323
+ continue;
324
+ }
325
+ }
326
+
327
+ // Criteria 2+3: dead PID or old age
328
+ if (typeof batchData.pid === 'number') {
329
+ if (!isSessionPidAlive(batchData.pid)) {
330
+ resolveDispatchEvent(db, io, batchRow.id, 'abandoned');
331
+ resolved++;
332
+ }
333
+ } else if (batchRow.timestamp <= cutoff) {
334
+ resolveDispatchEvent(db, io, batchRow.id, 'abandoned');
335
+ resolved++;
336
+ }
337
+ }
338
+
339
+ return resolved;
340
+ }
341
+
342
+ /** Get scope IDs with recent abandoned dispatch outcomes.
343
+ * Returns an array of abandoned scope entries with scope_id, from_status, and abandoned_at.
344
+ * Only includes scopes that are NOT currently in a terminal state and
345
+ * do NOT have a newer active (unresolved) dispatch. */
346
+ export function getAbandonedScopeIds(
347
+ db: Database.Database,
348
+ scopeService: ScopeService,
349
+ engine: WorkflowEngine,
350
+ activeScopeIds?: number[],
351
+ ): Array<{ scope_id: number; from_status: string | null; abandoned_at: string }> {
352
+ const rows = db.prepare(
353
+ `SELECT scope_id, data, timestamp FROM events
354
+ WHERE type = 'DISPATCH'
355
+ AND scope_id IS NOT NULL
356
+ AND JSON_EXTRACT(data, '$.resolved.outcome') = 'abandoned'
357
+ ORDER BY timestamp DESC`,
358
+ ).all() as Array<{ scope_id: number; data: string; timestamp: string }>;
359
+
360
+ // Get active scope IDs to exclude scopes with new dispatches
361
+ const activeScopes = activeScopeIds ?? getActiveScopeIds(db, scopeService, engine);
362
+ const activeSet = new Set(activeScopes);
363
+
364
+ const seen = new Set<number>();
365
+ const result: Array<{ scope_id: number; from_status: string | null; abandoned_at: string }> = [];
366
+
367
+ for (const row of rows) {
368
+ if (seen.has(row.scope_id)) continue;
369
+ seen.add(row.scope_id);
370
+
371
+ // Skip if scope has a new active dispatch
372
+ if (activeSet.has(row.scope_id)) continue;
373
+
374
+ // Skip if scope is in terminal state
375
+ const scope = scopeService.getById(row.scope_id);
376
+ if (!scope) continue;
377
+ if (engine.isTerminalStatus(scope.status)) continue;
378
+
379
+ let data: Record<string, unknown>;
380
+ try {
381
+ data = JSON.parse(row.data);
382
+ } catch (e) {
383
+ log.error('Failed to parse DISPATCH event data', { scope_id: row.scope_id, error: String(e) });
384
+ continue;
385
+ }
386
+ const transition = data.transition as Record<string, unknown> | null;
387
+ const resolved = data.resolved as Record<string, unknown> | null;
388
+ const fromStatus = transition?.from as string ?? null;
389
+ const abandonedAt = resolved?.at as string ?? row.timestamp;
390
+
391
+ result.push({ scope_id: row.scope_id, from_status: fromStatus, abandoned_at: abandonedAt });
392
+ }
393
+
394
+ return result;
395
+ }
@@ -0,0 +1,109 @@
1
+ // ─── Lightweight Structured Logger ──────────────────────────
2
+ //
3
+ // Zero dependencies. Colored, timestamped, component-namespaced output.
4
+ // Usage:
5
+ // import { createLogger } from './utils/logger.js';
6
+ // const log = createLogger('scope');
7
+ // log.info('Status updated', { id: 3, from: 'backlog', to: 'implementing' });
8
+ // // => 12:34:56.789 INFO [scope] Status updated id=3 from=backlog to=implementing
9
+
10
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
11
+
12
+ const LEVEL_VALUE: Record<LogLevel, number> = {
13
+ debug: 0,
14
+ info: 1,
15
+ warn: 2,
16
+ error: 3,
17
+ };
18
+
19
+ let currentLevel: LogLevel = 'info';
20
+
21
+ export function setLogLevel(level: LogLevel): void {
22
+ currentLevel = level;
23
+ }
24
+
25
+ export function getLogLevel(): LogLevel {
26
+ return currentLevel;
27
+ }
28
+
29
+ // ─── Colors (ANSI) ──────────────────────────────────────────
30
+
31
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
32
+
33
+ const c = {
34
+ reset: useColor ? '\x1b[0m' : '',
35
+ dim: useColor ? '\x1b[2m' : '',
36
+ gray: useColor ? '\x1b[90m' : '',
37
+ cyan: useColor ? '\x1b[36m' : '',
38
+ yellow: useColor ? '\x1b[33m' : '',
39
+ red: useColor ? '\x1b[31m' : '',
40
+ };
41
+
42
+ const LEVEL_COLOR: Record<LogLevel, string> = {
43
+ debug: c.gray,
44
+ info: c.cyan,
45
+ warn: c.yellow,
46
+ error: c.red,
47
+ };
48
+
49
+ const LEVEL_LABEL: Record<LogLevel, string> = {
50
+ debug: 'DEBUG',
51
+ info: 'INFO ',
52
+ warn: 'WARN ',
53
+ error: 'ERROR',
54
+ };
55
+
56
+ // ─── Formatting ─────────────────────────────────────────────
57
+
58
+ function timestamp(): string {
59
+ const d = new Date();
60
+ const h = String(d.getHours()).padStart(2, '0');
61
+ const m = String(d.getMinutes()).padStart(2, '0');
62
+ const s = String(d.getSeconds()).padStart(2, '0');
63
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
64
+ return `${h}:${m}:${s}.${ms}`;
65
+ }
66
+
67
+ function formatData(data?: Record<string, unknown>): string {
68
+ if (!data) return '';
69
+ const pairs: string[] = [];
70
+ for (const [k, v] of Object.entries(data)) {
71
+ if (v === undefined || v === null) continue;
72
+ const val = typeof v === 'object' ? JSON.stringify(v) : String(v);
73
+ pairs.push(`${k}=${val}`);
74
+ }
75
+ return pairs.length > 0 ? ' ' + pairs.join(' ') : '';
76
+ }
77
+
78
+ // ─── Logger Factory ─────────────────────────────────────────
79
+
80
+ export interface Logger {
81
+ debug(msg: string, data?: Record<string, unknown>): void;
82
+ info(msg: string, data?: Record<string, unknown>): void;
83
+ warn(msg: string, data?: Record<string, unknown>): void;
84
+ error(msg: string, data?: Record<string, unknown>): void;
85
+ }
86
+
87
+ function write(level: LogLevel, component: string, msg: string, data?: Record<string, unknown>): void {
88
+ if (LEVEL_VALUE[level] < LEVEL_VALUE[currentLevel]) return;
89
+
90
+ const color = LEVEL_COLOR[level];
91
+ const label = LEVEL_LABEL[level];
92
+ const kv = formatData(data);
93
+ const line = `${c.dim}${timestamp()}${c.reset} ${color}${label}${c.reset} ${c.dim}[${component}]${c.reset} ${msg}${kv}\n`;
94
+
95
+ if (level === 'warn' || level === 'error') {
96
+ process.stderr.write(line);
97
+ } else {
98
+ process.stdout.write(line);
99
+ }
100
+ }
101
+
102
+ export function createLogger(component: string): Logger {
103
+ return {
104
+ debug: (msg, data) => write('debug', component, msg, data),
105
+ info: (msg, data) => write('info', component, msg, data),
106
+ warn: (msg, data) => write('warn', component, msg, data),
107
+ error: (msg, data) => write('error', component, msg, data),
108
+ };
109
+ }