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