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,548 @@
1
+ import { Router } from 'express';
2
+ import { execFile } from 'child_process';
3
+ import path from 'path';
4
+ import { promisify } from 'util';
5
+ import type Database from 'better-sqlite3';
6
+ import type { Server } from 'socket.io';
7
+ import type { GateService } from '../services/gate-service.js';
8
+ import type { DeployService } from '../services/deploy-service.js';
9
+ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
10
+ import { getHookEnforcement } from '../../shared/workflow-config.js';
11
+ import { getClaudeSessions, getSessionStats, type SessionStats } from '../services/claude-session-service.js';
12
+ import { launchInTerminal } from '../utils/terminal-launcher.js';
13
+
14
+ const execFileAsync = promisify(execFile);
15
+
16
+ // ─── Types & Helpers ────────────────────────────────────────
17
+
18
+ interface DriftCommit { sha: string; message: string; author: string; date: string }
19
+ interface BranchHead { sha: string; date: string; message: string }
20
+ interface PipelineDriftData {
21
+ devToStaging: { count: number; commits: DriftCommit[]; oldestDate: string | null };
22
+ stagingToMain: { count: number; commits: DriftCommit[]; oldestDate: string | null };
23
+ heads: { dev: BranchHead; staging: BranchHead; main: BranchHead };
24
+ }
25
+
26
+ const JSON_FIELDS = ['tags', 'blocked_by', 'blocks', 'data', 'discoveries', 'next_steps', 'details'];
27
+
28
+ type Row = Record<string, unknown>;
29
+
30
+ function parseJsonFields(row: Row): Row {
31
+ const parsed = { ...row };
32
+ for (const field of JSON_FIELDS) {
33
+ if (typeof parsed[field] === 'string') {
34
+ try { parsed[field] = JSON.parse(parsed[field] as string); } catch { /* keep string */ }
35
+ }
36
+ }
37
+ return parsed;
38
+ }
39
+
40
+ function parseDriftCommits(raw: string): DriftCommit[] {
41
+ if (!raw) return [];
42
+ return raw.split('\n').map((line) => {
43
+ const [sha, date, message, author] = line.split('|');
44
+ return { sha, date, message: message ?? '', author: author ?? '' };
45
+ });
46
+ }
47
+
48
+ function parseHead(raw: string): BranchHead {
49
+ const [sha, date, message] = raw.split('|');
50
+ return { sha: sha ?? '', date: date ?? '', message: message ?? '' };
51
+ }
52
+
53
+ // ─── Route Factory ──────────────────────────────────────────
54
+
55
+ interface DataRouteDeps {
56
+ db: Database.Database;
57
+ io: Server;
58
+ gateService: GateService;
59
+ deployService: DeployService;
60
+ engine: WorkflowEngine;
61
+ projectRoot: string;
62
+ inferScopeStatus: (type: string, scopeId: unknown, data: Record<string, unknown>) => void;
63
+ }
64
+
65
+ export function createDataRoutes({
66
+ db, io, gateService, deployService, engine, projectRoot, inferScopeStatus,
67
+ }: DataRouteDeps): Router {
68
+ const router = Router();
69
+
70
+ // ─── Pipeline Drift (cached) ─────────────────────────────
71
+
72
+ let driftCache: { data: PipelineDriftData; ts: number } | null = null;
73
+ const DRIFT_CACHE_MS = 60_000;
74
+
75
+ async function gitLog(args: string[]): Promise<string> {
76
+ const { stdout } = await execFileAsync('git', args, { cwd: projectRoot });
77
+ return stdout.trim();
78
+ }
79
+
80
+ async function computeDrift(): Promise<PipelineDriftData> {
81
+ if (driftCache && Date.now() - driftCache.ts < DRIFT_CACHE_MS) return driftCache.data;
82
+
83
+ const [devToStagingRaw, stagingToMainRaw, devHead, stagingHead, mainHead] =
84
+ await Promise.all([
85
+ gitLog(['log', 'origin/dev', '--not', 'origin/staging', '--reverse', '--format=%H|%aI|%s|%an']),
86
+ gitLog(['log', 'origin/staging', '--not', 'origin/main', '--reverse', '--format=%H|%aI|%s|%an']),
87
+ gitLog(['log', 'origin/dev', '-1', '--format=%H|%aI|%s']),
88
+ gitLog(['log', 'origin/staging', '-1', '--format=%H|%aI|%s']),
89
+ gitLog(['log', 'origin/main', '-1', '--format=%H|%aI|%s']),
90
+ ]);
91
+
92
+ const devToStaging = parseDriftCommits(devToStagingRaw);
93
+ const stagingToMain = parseDriftCommits(stagingToMainRaw);
94
+
95
+ const data: PipelineDriftData = {
96
+ devToStaging: {
97
+ count: devToStaging.length,
98
+ commits: devToStaging,
99
+ oldestDate: devToStaging[0]?.date ?? null,
100
+ },
101
+ stagingToMain: {
102
+ count: stagingToMain.length,
103
+ commits: stagingToMain,
104
+ oldestDate: stagingToMain[0]?.date ?? null,
105
+ },
106
+ heads: {
107
+ dev: parseHead(devHead),
108
+ staging: parseHead(stagingHead),
109
+ main: parseHead(mainHead),
110
+ },
111
+ };
112
+
113
+ driftCache = { data, ts: Date.now() };
114
+ return data;
115
+ }
116
+
117
+ // ─── Event Routes ──────────────────────────────────────────
118
+
119
+ router.get('/events', (req, res) => {
120
+ const limit = Number(req.query.limit) || 50;
121
+ const type = req.query.type as string | undefined;
122
+ const scopeId = req.query.scope_id as string | undefined;
123
+
124
+ let query = 'SELECT * FROM events WHERE 1=1';
125
+ const params: unknown[] = [];
126
+
127
+ if (type) { query += ' AND type = ?'; params.push(type); }
128
+ if (scopeId) { query += ' AND scope_id = ?'; params.push(Number(scopeId)); }
129
+
130
+ query += ' ORDER BY timestamp DESC LIMIT ?';
131
+ params.push(limit);
132
+
133
+ const events = db.prepare(query).all(...params) as Row[];
134
+ res.json(events.map(parseJsonFields));
135
+ });
136
+
137
+ router.post('/events', (req, res) => {
138
+ const { id, type, scope_id, session_id, agent, data, timestamp } = req.body;
139
+
140
+ if (!type || typeof type !== 'string') {
141
+ res.status(400).json({ error: 'type must be a non-empty string' });
142
+ return;
143
+ }
144
+ if (scope_id != null && (!Number.isInteger(scope_id) || scope_id <= 0)) {
145
+ res.status(400).json({ error: 'scope_id must be a positive integer or null' });
146
+ return;
147
+ }
148
+
149
+ const eventId = id || crypto.randomUUID();
150
+ const ts = timestamp || new Date().toISOString();
151
+ const eventData = data ?? {};
152
+
153
+ db.prepare(
154
+ `INSERT OR IGNORE INTO events (id, type, scope_id, session_id, agent, data, timestamp)
155
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
156
+ ).run(eventId, type, scope_id ?? null, session_id ?? null, agent ?? null, JSON.stringify(eventData), ts);
157
+
158
+ const event = { id: eventId, type, scope_id, session_id, agent, data: eventData, timestamp: ts };
159
+ io.emit('event:new', event);
160
+
161
+ inferScopeStatus(type, scope_id ?? eventData.scope_id, eventData);
162
+
163
+ res.status(201).json(event);
164
+ });
165
+
166
+ // ─── Violations Summary ──────────────────────────────────
167
+
168
+ router.get('/events/violations/summary', (_req, res) => {
169
+ try {
170
+ const byRule = db.prepare(
171
+ `SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
172
+ FROM events WHERE type = 'VIOLATION' GROUP BY rule ORDER BY count DESC`
173
+ ).all();
174
+ const byFile = db.prepare(
175
+ `SELECT JSON_EXTRACT(data, '$.file') as file, COUNT(*) as count FROM events
176
+ WHERE type = 'VIOLATION' AND JSON_EXTRACT(data, '$.file') IS NOT NULL AND JSON_EXTRACT(data, '$.file') != ''
177
+ GROUP BY file ORDER BY count DESC LIMIT 20`
178
+ ).all();
179
+ const overrides = db.prepare(
180
+ `SELECT JSON_EXTRACT(data, '$.rule') as rule, JSON_EXTRACT(data, '$.reason') as reason, timestamp as date
181
+ FROM events WHERE type = 'OVERRIDE' ORDER BY timestamp DESC LIMIT 50`
182
+ ).all();
183
+ const totalViolations = db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'VIOLATION'`).get() as { count: number };
184
+ const totalOverrides = db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'OVERRIDE'`).get() as { count: number };
185
+ res.json({ byRule, byFile, overrides, totalViolations: totalViolations.count, totalOverrides: totalOverrides.count });
186
+ } catch {
187
+ res.status(500).json({ error: 'Failed to query violations summary' });
188
+ }
189
+ });
190
+
191
+ // ─── Enforcement Rules ───────────────────────────────────────
192
+
193
+ router.get('/enforcement/rules', (_req, res) => {
194
+ try {
195
+ const allHooks = engine.getAllHooks();
196
+ const allEdges = engine.getAllEdges();
197
+
198
+ // Build edge map: hookId → edges it's attached to
199
+ const hookEdgeMap = new Map<string, Array<{ from: string; to: string; label: string }>>();
200
+ for (const edge of allEdges) {
201
+ for (const hookId of edge.hooks ?? []) {
202
+ if (!hookEdgeMap.has(hookId)) hookEdgeMap.set(hookId, []);
203
+ hookEdgeMap.get(hookId)!.push({ from: edge.from, to: edge.to, label: edge.label });
204
+ }
205
+ }
206
+
207
+ // Query violation and override stats per rule
208
+ const violationStats = db.prepare(
209
+ `SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
210
+ FROM events WHERE type = 'VIOLATION' GROUP BY rule`
211
+ ).all() as Array<{ rule: string; count: number; last_seen: string }>;
212
+
213
+ const overrideStats = db.prepare(
214
+ `SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
215
+ FROM events WHERE type = 'OVERRIDE' GROUP BY rule`
216
+ ).all() as Array<{ rule: string; count: number }>;
217
+
218
+ const violationMap = new Map(violationStats.map((v) => [v.rule, v]));
219
+ const overrideMap = new Map(overrideStats.map((o) => [o.rule, o]));
220
+
221
+ // Build summary counts
222
+ const summary = { guards: 0, gates: 0, lifecycle: 0, observers: 0 };
223
+ for (const hook of allHooks) {
224
+ if (hook.category === 'guard') summary.guards++;
225
+ else if (hook.category === 'gate') summary.gates++;
226
+ else if (hook.category === 'lifecycle') summary.lifecycle++;
227
+ else if (hook.category === 'observer') summary.observers++;
228
+ }
229
+
230
+ const rules = allHooks.map((hook) => ({
231
+ hook,
232
+ enforcement: getHookEnforcement(hook),
233
+ edges: hookEdgeMap.get(hook.id) ?? [],
234
+ stats: {
235
+ violations: violationMap.get(hook.id)?.count ?? 0,
236
+ overrides: overrideMap.get(hook.id)?.count ?? 0,
237
+ last_triggered: violationMap.get(hook.id)?.last_seen ?? null,
238
+ },
239
+ }));
240
+
241
+ res.json({ summary, rules, totalEdges: allEdges.length });
242
+ } catch {
243
+ res.status(500).json({ error: 'Failed to query enforcement rules' });
244
+ }
245
+ });
246
+
247
+ // ─── Violation Trends ──────────────────────────────────────
248
+
249
+ router.get('/events/violations/trend', (req, res) => {
250
+ try {
251
+ const days = Number(req.query.days) || 30;
252
+ const trend = db.prepare(
253
+ `SELECT date(timestamp) as day, JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
254
+ FROM events WHERE type = 'VIOLATION' AND timestamp >= datetime('now', ? || ' days')
255
+ GROUP BY day, rule ORDER BY day ASC`
256
+ ).all(`-${days}`) as Array<{ day: string; rule: string; count: number }>;
257
+ res.json(trend);
258
+ } catch {
259
+ res.status(500).json({ error: 'Failed to query violation trends' });
260
+ }
261
+ });
262
+
263
+ // ─── Gate Routes ───────────────────────────────────────────
264
+
265
+ router.get('/gates', (req, res) => {
266
+ const scopeId = req.query.scope_id;
267
+ if (scopeId) {
268
+ res.json(gateService.getLatestForScope(Number(scopeId)));
269
+ } else {
270
+ res.json(gateService.getLatestRun());
271
+ }
272
+ });
273
+
274
+ router.get('/gates/trend', (req, res) => {
275
+ const limit = Number(req.query.limit) || 30;
276
+ res.json(gateService.getTrend(limit));
277
+ });
278
+
279
+ router.get('/gates/stats', (_req, res) => {
280
+ res.json(gateService.getStats());
281
+ });
282
+
283
+ router.post('/gates', (req, res) => {
284
+ const { scope_id, gate_name, status, details, duration_ms, commit_sha } = req.body;
285
+
286
+ const VALID_GATE_STATUSES = ['pass', 'fail', 'running', 'skipped'];
287
+ if (!gate_name || typeof gate_name !== 'string') {
288
+ res.status(400).json({ error: 'gate_name must be a non-empty string' });
289
+ return;
290
+ }
291
+ if (status && !VALID_GATE_STATUSES.includes(status)) {
292
+ res.status(400).json({ error: `status must be one of: ${VALID_GATE_STATUSES.join(', ')}` });
293
+ return;
294
+ }
295
+
296
+ gateService.record({ scope_id, gate_name, status, details, duration_ms, commit_sha });
297
+ res.status(201).json({ ok: true });
298
+ });
299
+
300
+ // ─── Deployment Routes ─────────────────────────────────────
301
+
302
+ router.get('/deployments', (_req, res) => {
303
+ res.json((deployService.getRecent() as unknown as Row[]).map(parseJsonFields));
304
+ });
305
+
306
+ router.get('/deployments/latest', (_req, res) => {
307
+ res.json((deployService.getLatestPerEnv() as unknown as Row[]).map(parseJsonFields));
308
+ });
309
+
310
+ router.post('/deployments', (req, res) => {
311
+ const VALID_ENVIRONMENTS = ['staging', 'production'];
312
+ if (req.body.environment && !VALID_ENVIRONMENTS.includes(req.body.environment)) {
313
+ res.status(400).json({ error: `environment must be one of: ${VALID_ENVIRONMENTS.join(', ')}` });
314
+ return;
315
+ }
316
+ const id = deployService.record(req.body);
317
+ res.status(201).json({ id });
318
+ });
319
+
320
+ router.patch('/deployments/:id', (req, res) => {
321
+ const id = Number(req.params.id);
322
+ const { status, details } = req.body;
323
+ if (!status) {
324
+ res.status(400).json({ error: 'status is required' });
325
+ return;
326
+ }
327
+ deployService.updateStatus(id, status, details);
328
+ res.json({ ok: true });
329
+ });
330
+
331
+ router.get('/pipeline/drift', async (_req, res) => {
332
+ try {
333
+ const drift = await computeDrift();
334
+ res.json(drift);
335
+ } catch (err) {
336
+ res.status(500).json({ error: 'Failed to compute drift', details: String(err) });
337
+ }
338
+ });
339
+
340
+ router.get('/deployments/frequency', (_req, res) => {
341
+ try {
342
+ const rows = db.prepare(
343
+ `SELECT environment, strftime('%Y-W%W', started_at) as week, COUNT(*) as count
344
+ FROM deployments WHERE started_at > datetime('now', '-56 days') GROUP BY environment, week ORDER BY week ASC`
345
+ ).all() as Array<{ environment: string; week: string; count: number }>;
346
+ const weekMap = new Map<string, { week: string; staging: number; production: number }>();
347
+ for (const row of rows) {
348
+ if (!weekMap.has(row.week)) weekMap.set(row.week, { week: row.week, staging: 0, production: 0 });
349
+ const entry = weekMap.get(row.week)!;
350
+ if (row.environment === 'staging') entry.staging = row.count;
351
+ if (row.environment === 'production') entry.production = row.count;
352
+ }
353
+ res.json([...weekMap.values()]);
354
+ } catch {
355
+ res.status(500).json({ error: 'Failed to query deployment frequency' });
356
+ }
357
+ });
358
+
359
+ // ─── Session Routes ────────────────────────────────────────
360
+
361
+ router.get('/sessions', (_req, res) => {
362
+ const rows = (db.prepare('SELECT * FROM sessions ORDER BY started_at DESC').all() as Row[])
363
+ .map(parseJsonFields);
364
+
365
+ const seen = new Map<string, Record<string, unknown>>();
366
+ const scopeMap = new Map<string, number[]>();
367
+ const actionMap = new Map<string, string[]>();
368
+
369
+ for (const row of rows) {
370
+ const key = (row.claude_session_id as string | null) ?? (row.id as string);
371
+ if (!seen.has(key)) {
372
+ seen.set(key, row);
373
+ scopeMap.set(key, []);
374
+ actionMap.set(key, []);
375
+ }
376
+ const sid = row.scope_id as number | null;
377
+ if (sid != null) {
378
+ const arr = scopeMap.get(key)!;
379
+ if (!arr.includes(sid)) arr.push(sid);
380
+ }
381
+ const action = row.action as string | null;
382
+ if (action) {
383
+ const actions = actionMap.get(key)!;
384
+ if (!actions.includes(action)) actions.push(action);
385
+ }
386
+ }
387
+
388
+ const results = [...seen.values()].map((row) => {
389
+ const key = (row.claude_session_id as string | null) ?? (row.id as string);
390
+ return { ...row, scope_ids: scopeMap.get(key) ?? [], actions: actionMap.get(key) ?? [] };
391
+ });
392
+
393
+ res.json(results.slice(0, 50));
394
+ });
395
+
396
+ // ─── Scope Sessions ───────────────────────────────────────
397
+
398
+ router.get('/scopes/:id/sessions', (req, res) => {
399
+ const scopeId = Number(req.params.id);
400
+ const sessions = db.prepare('SELECT * FROM sessions WHERE scope_id = ? ORDER BY started_at DESC')
401
+ .all(scopeId) as Row[];
402
+ res.json(sessions.map(parseJsonFields));
403
+ });
404
+
405
+ router.get('/sessions/:id/content', async (req, res) => {
406
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?')
407
+ .get(req.params.id) as Row | undefined;
408
+
409
+ if (!session) {
410
+ res.status(404).json({ error: 'Session not found' });
411
+ return;
412
+ }
413
+
414
+ const parsed = parseJsonFields(session);
415
+ let content = '';
416
+ let meta: Record<string, unknown> | null = null;
417
+ let stats: SessionStats | null = null;
418
+
419
+ if (parsed.claude_session_id && typeof parsed.claude_session_id === 'string') {
420
+ const claudeSessions = await getClaudeSessions();
421
+ const match = claudeSessions.find(s => s.id === parsed.claude_session_id);
422
+ if (match) {
423
+ meta = {
424
+ slug: match.slug,
425
+ branch: match.branch,
426
+ fileSize: match.fileSize,
427
+ summary: match.summary,
428
+ startedAt: match.startedAt,
429
+ lastActiveAt: match.lastActiveAt,
430
+ };
431
+ }
432
+ stats = getSessionStats(parsed.claude_session_id as string);
433
+ }
434
+
435
+ if (!content) {
436
+ const parts: string[] = [];
437
+ if (parsed.summary) parts.push(`# ${parsed.summary}\n`);
438
+ const discoveries = Array.isArray(parsed.discoveries) ? parsed.discoveries : [];
439
+ if (discoveries.length > 0) {
440
+ parts.push('## Completed\n');
441
+ for (const d of discoveries) parts.push(`- ${d}`);
442
+ parts.push('');
443
+ }
444
+ const nextSteps = Array.isArray(parsed.next_steps) ? parsed.next_steps : [];
445
+ if (nextSteps.length > 0) {
446
+ parts.push('## Next Steps\n');
447
+ for (const n of nextSteps) parts.push(`- ${n}`);
448
+ }
449
+ content = parts.join('\n');
450
+ }
451
+
452
+ res.json({
453
+ id: parsed.id,
454
+ content,
455
+ claude_session_id: parsed.claude_session_id ?? null,
456
+ meta,
457
+ stats,
458
+ });
459
+ });
460
+
461
+ router.post('/sessions/:id/resume', async (req, res) => {
462
+ const { claude_session_id } = req.body as { claude_session_id?: string };
463
+
464
+ if (!claude_session_id || !/^[0-9a-f-]{36}$/i.test(claude_session_id)) {
465
+ res.status(400).json({ error: 'Valid claude_session_id (UUID) required' });
466
+ return;
467
+ }
468
+
469
+ const resumeCmd = `cd '${projectRoot}' && claude --dangerously-skip-permissions --resume '${claude_session_id}'`;
470
+
471
+ try {
472
+ await launchInTerminal(resumeCmd);
473
+ res.json({ ok: true, session_id: claude_session_id });
474
+ } catch (err) {
475
+ res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
476
+ }
477
+ });
478
+
479
+
480
+ // ─── Git Status ────────────────────────────────────────────
481
+ router.get('/git/status', async (_req, res) => {
482
+ try {
483
+ const [branchResult, statusResult] = await Promise.all([
484
+ execFileAsync('git', ['branch', '--show-current'], { cwd: projectRoot }),
485
+ execFileAsync('git', ['status', '--porcelain'], { cwd: projectRoot }),
486
+ ]);
487
+ const branch = branchResult.stdout.trim();
488
+ const dirty = statusResult.stdout.trim().length > 0;
489
+ let detached = false;
490
+ if (!branch) {
491
+ detached = true;
492
+ }
493
+ res.json({ branch: branch || '(detached)', dirty, detached });
494
+ } catch (err) {
495
+ res.status(500).json({ error: 'Failed to get git status', details: String(err) });
496
+ }
497
+ });
498
+
499
+ router.get('/worktrees', async (_req, res) => {
500
+ try {
501
+ const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: projectRoot });
502
+ const worktrees: Array<{ path: string; branch: string; head: string }> = [];
503
+ let current: { path: string; branch: string; head: string } = { path: '', branch: '', head: '' };
504
+ for (const line of stdout.split('\n')) {
505
+ if (line.startsWith('worktree ')) {
506
+ if (current.path) worktrees.push(current);
507
+ current = { path: line.slice(9), branch: '', head: '' };
508
+ } else if (line.startsWith('HEAD ')) {
509
+ current.head = line.slice(5);
510
+ } else if (line.startsWith('branch ')) {
511
+ current.branch = line.slice(7);
512
+ } else if (line === '') {
513
+ if (current.path) worktrees.push(current);
514
+ current = { path: '', branch: '', head: '' };
515
+ }
516
+ }
517
+ if (current.path) worktrees.push(current);
518
+ res.json(worktrees);
519
+ } catch (err) {
520
+ res.status(500).json({ error: 'Failed to list worktrees', details: String(err) });
521
+ }
522
+ });
523
+
524
+ // ─── Open File ──────────────────────────────────────────────
525
+
526
+ router.post('/open-file', (req, res) => {
527
+ const filePath = (req.query.path as string) || '';
528
+ if (!filePath || filePath.includes('..')) {
529
+ res.status(400).json({ error: 'Invalid path' });
530
+ return;
531
+ }
532
+ const absolute = path.resolve(projectRoot, filePath);
533
+ const resolvedRoot = path.resolve(projectRoot) + path.sep;
534
+ if (!absolute.startsWith(resolvedRoot)) {
535
+ res.status(400).json({ error: 'Path escapes project root' });
536
+ return;
537
+ }
538
+ execFile('open', [absolute], (err) => {
539
+ if (err) {
540
+ res.status(500).json({ error: 'Failed to open file' });
541
+ return;
542
+ }
543
+ res.json({ ok: true });
544
+ });
545
+ });
546
+
547
+ return router;
548
+ }