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,98 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { Server } from 'socket.io';
3
+ import type { RawEvent } from '../parsers/event-parser.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+
6
+ const log = createLogger('event');
7
+
8
+ export type EventIngestCallback = (type: string, scopeId: unknown, data: Record<string, unknown>) => void;
9
+
10
+ export interface EventRow {
11
+ id: string;
12
+ type: string;
13
+ scope_id: number | null;
14
+ session_id: string | null;
15
+ agent: string | null;
16
+ data: string;
17
+ timestamp: string;
18
+ processed: number;
19
+ }
20
+
21
+ export class EventService {
22
+ private onIngestCallbacks: EventIngestCallback[] = [];
23
+
24
+ constructor(
25
+ private db: Database.Database,
26
+ private io: Server
27
+ ) {}
28
+
29
+ /** Register a callback to be called after each successful event ingest */
30
+ onIngest(callback: EventIngestCallback): void {
31
+ this.onIngestCallbacks.push(callback);
32
+ }
33
+
34
+ /** Ingest a parsed event into the database and broadcast it */
35
+ ingest(event: RawEvent): void {
36
+ const result = this.db.prepare(
37
+ `INSERT OR IGNORE INTO events (id, type, scope_id, session_id, agent, data, timestamp, processed)
38
+ VALUES (?, ?, ?, ?, ?, ?, ?, 1)`
39
+ ).run(
40
+ event.id,
41
+ event.type,
42
+ event.scope_id ?? null,
43
+ event.session_id ?? null,
44
+ event.agent ?? null,
45
+ JSON.stringify(event.data ?? {}),
46
+ event.timestamp
47
+ );
48
+
49
+ // Only broadcast if this was a new insert (not a duplicate)
50
+ if (result.changes === 0) {
51
+ log.debug('Event duplicate skipped', { id: event.id });
52
+ return;
53
+ }
54
+
55
+ log.info('Event ingested', { type: event.type, id: event.id, scope_id: event.scope_id, agent: event.agent });
56
+ const data = event.data ?? {};
57
+ this.io.emit('event:new', {
58
+ id: event.id,
59
+ type: event.type,
60
+ scope_id: event.scope_id ?? null,
61
+ session_id: event.session_id ?? null,
62
+ agent: event.agent ?? null,
63
+ data,
64
+ timestamp: event.timestamp,
65
+ });
66
+
67
+ // Trigger event-driven inference
68
+ for (const cb of this.onIngestCallbacks) {
69
+ cb(event.type, event.scope_id ?? data.scope_id, data);
70
+ }
71
+ }
72
+
73
+ /** Get recent events, optionally filtered by type */
74
+ getRecent(limit: number = 50, type?: string): EventRow[] {
75
+ if (type) {
76
+ return this.db
77
+ .prepare('SELECT * FROM events WHERE type = ? ORDER BY timestamp DESC LIMIT ?')
78
+ .all(type, limit) as EventRow[];
79
+ }
80
+ return this.db
81
+ .prepare('SELECT * FROM events ORDER BY timestamp DESC LIMIT ?')
82
+ .all(limit) as EventRow[];
83
+ }
84
+
85
+ /** Get events for a specific agent */
86
+ getByAgent(agent: string, limit: number = 50): EventRow[] {
87
+ return this.db
88
+ .prepare('SELECT * FROM events WHERE agent = ? ORDER BY timestamp DESC LIMIT ?')
89
+ .all(agent, limit) as EventRow[];
90
+ }
91
+
92
+ /** Get events for a specific scope */
93
+ getByScope(scopeId: number, limit: number = 50): EventRow[] {
94
+ return this.db
95
+ .prepare('SELECT * FROM events WHERE scope_id = ? ORDER BY timestamp DESC LIMIT ?')
96
+ .all(scopeId, limit) as EventRow[];
97
+ }
98
+ }
@@ -0,0 +1,126 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { Server } from 'socket.io';
3
+ import type { GateStatus } from '../../shared/api-types.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+
6
+ const log = createLogger('gate');
7
+
8
+ export interface GateResult {
9
+ scope_id: number | null;
10
+ gate_name: string;
11
+ status: GateStatus;
12
+ details: string | null;
13
+ duration_ms: number | null;
14
+ commit_sha: string | null;
15
+ }
16
+
17
+ export interface GateRow {
18
+ id: number;
19
+ scope_id: number | null;
20
+ gate_name: string;
21
+ status: GateStatus;
22
+ details: string | null;
23
+ duration_ms: number | null;
24
+ run_at: string;
25
+ commit_sha: string | null;
26
+ }
27
+
28
+ // The 13 quality gates from /test-checks
29
+ export const GATE_NAMES = [
30
+ 'type-check',
31
+ 'lint',
32
+ 'build',
33
+ 'template-validation',
34
+ 'doc-links',
35
+ 'doc-freshness',
36
+ 'rule-enforcement',
37
+ 'no-placeholders',
38
+ 'no-mock-data',
39
+ 'no-shortcuts',
40
+ 'no-default-secrets',
41
+ 'no-stale-scopes',
42
+ 'tests',
43
+ ] as const;
44
+
45
+ export class GateService {
46
+ constructor(
47
+ private db: Database.Database,
48
+ private io: Server
49
+ ) {}
50
+
51
+ /** Record a gate result */
52
+ record(gate: GateResult): void {
53
+ const result = this.db.prepare(
54
+ `INSERT INTO quality_gates (scope_id, gate_name, status, details, duration_ms, run_at, commit_sha)
55
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
56
+ ).run(
57
+ gate.scope_id,
58
+ gate.gate_name,
59
+ gate.status,
60
+ gate.details,
61
+ gate.duration_ms,
62
+ new Date().toISOString(),
63
+ gate.commit_sha
64
+ );
65
+
66
+ log.info('Gate recorded', { scope_id: gate.scope_id, gate: gate.gate_name, status: gate.status, duration_ms: gate.duration_ms });
67
+ const inserted = this.db.prepare('SELECT * FROM quality_gates WHERE id = ?').get(result.lastInsertRowid);
68
+ if (inserted) {
69
+ this.io.emit('gate:updated', inserted);
70
+ }
71
+ }
72
+
73
+ /** Get latest gate results for a scope */
74
+ getLatestForScope(scopeId: number): GateRow[] {
75
+ return this.db.prepare(`
76
+ SELECT * FROM quality_gates
77
+ WHERE scope_id = ? AND id IN (
78
+ SELECT MAX(id) FROM quality_gates
79
+ WHERE scope_id = ?
80
+ GROUP BY gate_name
81
+ )
82
+ ORDER BY gate_name
83
+ `).all(scopeId, scopeId) as GateRow[];
84
+ }
85
+
86
+ /** Get latest gate run (all gates from most recent execution) */
87
+ getLatestRun(): GateRow[] {
88
+ // Get the most recent run_at timestamp
89
+ const latest = this.db.prepare(
90
+ 'SELECT run_at FROM quality_gates ORDER BY run_at DESC LIMIT 1'
91
+ ).get() as { run_at: string } | undefined;
92
+
93
+ if (!latest) return [];
94
+
95
+ // Get all gates from that run (within 60 seconds of each other)
96
+ return this.db.prepare(`
97
+ SELECT * FROM quality_gates
98
+ WHERE run_at >= datetime(?, '-60 seconds')
99
+ ORDER BY gate_name
100
+ `).all(latest.run_at) as GateRow[];
101
+ }
102
+
103
+ /** Get gate history for trend chart */
104
+ getTrend(limit: number = 30): GateRow[] {
105
+ return this.db.prepare(`
106
+ SELECT gate_name, status, run_at, duration_ms
107
+ FROM quality_gates
108
+ ORDER BY run_at DESC
109
+ LIMIT ?
110
+ `).all(limit * GATE_NAMES.length) as GateRow[]; // Get enough to cover N runs
111
+ }
112
+
113
+ /** Get aggregate pass/fail stats */
114
+ getStats(): { gate_name: string; total: number; passed: number; failed: number }[] {
115
+ return this.db.prepare(`
116
+ SELECT
117
+ gate_name,
118
+ COUNT(*) as total,
119
+ SUM(CASE WHEN status = 'pass' THEN 1 ELSE 0 END) as passed,
120
+ SUM(CASE WHEN status = 'fail' THEN 1 ELSE 0 END) as failed
121
+ FROM quality_gates
122
+ GROUP BY gate_name
123
+ ORDER BY gate_name
124
+ `).all() as { gate_name: string; total: number; passed: number; failed: number }[];
125
+ }
126
+ }
@@ -0,0 +1,391 @@
1
+ import { execFile as execFileCb } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { listWorktrees } from '../utils/worktree-manager.js';
4
+ import type { ScopeCache } from './scope-cache.js';
5
+
6
+ const execFile = promisify(execFileCb);
7
+
8
+ // ─── Types ──────────────────────────────────────────────────
9
+
10
+ export interface GitOverview {
11
+ branchingMode: 'trunk' | 'worktree';
12
+ currentBranch: string;
13
+ dirty: boolean;
14
+ detached: boolean;
15
+ mainHead: { sha: string; message: string; date: string } | null;
16
+ aheadBehind: { ahead: number; behind: number } | null;
17
+ worktreeCount: number;
18
+ featureBranchCount: number;
19
+ }
20
+
21
+ export interface CommitEntry {
22
+ sha: string;
23
+ shortSha: string;
24
+ message: string;
25
+ author: string;
26
+ date: string;
27
+ branch: string;
28
+ scopeId: number | null;
29
+ refs: string[];
30
+ }
31
+
32
+ export interface BranchInfo {
33
+ name: string;
34
+ isRemote: boolean;
35
+ isCurrent: boolean;
36
+ headSha: string;
37
+ headMessage: string;
38
+ headDate: string;
39
+ aheadBehind: { ahead: number; behind: number } | null;
40
+ scopeId: number | null;
41
+ isStale: boolean;
42
+ }
43
+
44
+ export interface WorktreeDetail {
45
+ path: string;
46
+ branch: string;
47
+ head: string;
48
+ scopeId: number | null;
49
+ scopeTitle: string | null;
50
+ scopeStatus: string | null;
51
+ dirty: boolean;
52
+ aheadBehind: { ahead: number; behind: number } | null;
53
+ }
54
+
55
+ export interface DriftPair {
56
+ from: string;
57
+ to: string;
58
+ count: number;
59
+ commits: Array<{ sha: string; message: string; author: string; date: string }>;
60
+ }
61
+
62
+ // ─── Cache Utility ──────────────────────────────────────────
63
+
64
+ interface CacheEntry<T> { data: T; ts: number }
65
+
66
+ const CACHE_TTL = 60_000; // 60 seconds
67
+
68
+ function cached<T>(cache: Map<string, CacheEntry<T>>, key: string): T | null {
69
+ const entry = cache.get(key);
70
+ if (entry && Date.now() - entry.ts < CACHE_TTL) return entry.data;
71
+ return null;
72
+ }
73
+
74
+ function setCache<T>(cache: Map<string, CacheEntry<T>>, key: string, data: T): void {
75
+ cache.set(key, { data, ts: Date.now() });
76
+ }
77
+
78
+ // ─── Service ────────────────────────────────────────────────
79
+
80
+ const SCOPE_BRANCH_RE = /(?:feat|fix|scope)[/-](?:scope-)?(\d+)/;
81
+
82
+ export class GitService {
83
+ private cache = new Map<string, CacheEntry<unknown>>();
84
+
85
+ constructor(
86
+ private projectRoot: string,
87
+ private scopeCache: ScopeCache,
88
+ ) {}
89
+
90
+ private async git(args: string[], cwd?: string): Promise<string> {
91
+ // Uses execFile (not exec) — safe against shell injection
92
+ const { stdout } = await execFile('git', args, {
93
+ cwd: cwd ?? this.projectRoot,
94
+ maxBuffer: 10 * 1024 * 1024,
95
+ });
96
+ return stdout;
97
+ }
98
+
99
+ // ─── Overview ──────────────────────────────────────────────
100
+
101
+ async getOverview(branchingMode: 'trunk' | 'worktree'): Promise<GitOverview> {
102
+ const cacheKey = `overview:${branchingMode}`;
103
+ const hit = cached<GitOverview>(this.cache as Map<string, CacheEntry<GitOverview>>, cacheKey);
104
+ if (hit) return hit;
105
+
106
+ const [branchRaw, statusRaw] = await Promise.all([
107
+ this.git(['branch', '--show-current']).catch(() => ''),
108
+ this.git(['status', '--porcelain']).catch(() => ''),
109
+ ]);
110
+
111
+ const currentBranch = branchRaw.trim() || '(detached)';
112
+ const dirty = statusRaw.trim().length > 0;
113
+ const detached = !branchRaw.trim();
114
+
115
+ // Main HEAD
116
+ let mainHead: GitOverview['mainHead'] = null;
117
+ try {
118
+ const raw = await this.git(['log', 'HEAD', '-1', '--format=%H|%aI|%s']);
119
+ const [sha, date, ...msgParts] = raw.trim().split('|');
120
+ if (sha) mainHead = { sha, message: msgParts.join('|'), date };
121
+ } catch { /* no commits yet */ }
122
+
123
+ // Ahead/behind relative to origin/main (or origin/master)
124
+ let aheadBehind: GitOverview['aheadBehind'] = null;
125
+ if (!detached) {
126
+ try {
127
+ const raw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${currentBranch}`]);
128
+ const [behind, ahead] = raw.trim().split('\t').map(Number);
129
+ aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
130
+ } catch {
131
+ try {
132
+ const raw = await this.git(['rev-list', '--left-right', '--count', `origin/master...${currentBranch}`]);
133
+ const [behind, ahead] = raw.trim().split('\t').map(Number);
134
+ aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
135
+ } catch { /* no remote tracking */ }
136
+ }
137
+ }
138
+
139
+ // Worktree and feature branch counts
140
+ let worktreeCount = 0;
141
+ let featureBranchCount = 0;
142
+ try {
143
+ const wts = await listWorktrees(this.projectRoot);
144
+ worktreeCount = wts.length;
145
+ } catch { /* ok */ }
146
+ try {
147
+ const raw = await this.git(['branch', '--format=%(refname:short)']);
148
+ const branches = raw.trim().split('\n').filter(Boolean);
149
+ featureBranchCount = branches.filter(b => SCOPE_BRANCH_RE.test(b) || b.startsWith('feat/')).length;
150
+ } catch { /* ok */ }
151
+
152
+ const result: GitOverview = {
153
+ branchingMode,
154
+ currentBranch,
155
+ dirty,
156
+ detached,
157
+ mainHead,
158
+ aheadBehind,
159
+ worktreeCount,
160
+ featureBranchCount,
161
+ };
162
+ setCache(this.cache as Map<string, CacheEntry<GitOverview>>, cacheKey, result);
163
+ return result;
164
+ }
165
+
166
+ // ─── Commits ──────────────────────────────────────────────
167
+
168
+ async getCommits(opts: { branch?: string; limit?: number; offset?: number } = {}): Promise<CommitEntry[]> {
169
+ const { branch, limit = 50, offset = 0 } = opts;
170
+ const cacheKey = `commits:${branch ?? 'all'}:${limit}:${offset}`;
171
+ const hit = cached<CommitEntry[]>(this.cache as Map<string, CacheEntry<CommitEntry[]>>, cacheKey);
172
+ if (hit) return hit;
173
+
174
+ const args = ['log', '--format=%H|%h|%aI|%an|%s|%D'];
175
+ if (branch && branch !== 'all') {
176
+ args.push(branch);
177
+ } else {
178
+ args.push('--all');
179
+ }
180
+ args.push(`--skip=${offset}`, `-${limit}`);
181
+
182
+ let raw: string;
183
+ try {
184
+ raw = await this.git(args);
185
+ } catch {
186
+ return [];
187
+ }
188
+
189
+ const commits: CommitEntry[] = [];
190
+ for (const line of raw.trim().split('\n')) {
191
+ if (!line) continue;
192
+ const parts = line.split('|');
193
+ const sha = parts[0];
194
+ const shortSha = parts[1];
195
+ const date = parts[2];
196
+ const author = parts[3];
197
+ const message = parts[4];
198
+ const refStr = parts.slice(5).join('|');
199
+
200
+ const refs = refStr
201
+ ? refStr.split(',').map(r => r.trim()).filter(Boolean)
202
+ : [];
203
+
204
+ // Extract scope ID from refs or message
205
+ let scopeId: number | null = null;
206
+ for (const ref of refs) {
207
+ const m = SCOPE_BRANCH_RE.exec(ref);
208
+ if (m) { scopeId = parseInt(m[1]); break; }
209
+ }
210
+ if (!scopeId) {
211
+ const m = SCOPE_BRANCH_RE.exec(message);
212
+ if (m) scopeId = parseInt(m[1]);
213
+ }
214
+
215
+ // Derive branch from first ref that looks like a branch
216
+ let branchName = '';
217
+ for (const ref of refs) {
218
+ const cleaned = ref.replace(/^HEAD -> /, '').replace(/^origin\//, '');
219
+ if (cleaned && !cleaned.startsWith('tag:')) {
220
+ branchName = cleaned;
221
+ break;
222
+ }
223
+ }
224
+
225
+ commits.push({ sha, shortSha, message, author, date, branch: branchName, scopeId, refs });
226
+ }
227
+
228
+ setCache(this.cache as Map<string, CacheEntry<CommitEntry[]>>, cacheKey, commits);
229
+ return commits;
230
+ }
231
+
232
+ // ─── Branches ──────────────────────────────────────────────
233
+
234
+ async getBranches(): Promise<BranchInfo[]> {
235
+ const hit = cached<BranchInfo[]>(this.cache as Map<string, CacheEntry<BranchInfo[]>>, 'branches');
236
+ if (hit) return hit;
237
+
238
+ let raw: string;
239
+ try {
240
+ raw = await this.git([
241
+ 'branch', '-a',
242
+ '--format=%(HEAD)|%(refname:short)|%(objectname:short)|%(committerdate:iso-strict)|%(subject)',
243
+ ]);
244
+ } catch {
245
+ return [];
246
+ }
247
+
248
+ const now = Date.now();
249
+ const STALE_MS = 7 * 24 * 60 * 60 * 1000;
250
+ const branches: BranchInfo[] = [];
251
+
252
+ for (const line of raw.trim().split('\n')) {
253
+ if (!line) continue;
254
+ const [headMarker, name, headSha, headDate, ...msgParts] = line.split('|');
255
+ if (!name || name.includes('HEAD')) continue;
256
+
257
+ const isCurrent = headMarker === '*';
258
+ const isRemote = name.startsWith('remotes/') || name.startsWith('origin/');
259
+ const cleanName = name.replace(/^remotes\//, '');
260
+
261
+ // Skip remote duplicates of local branches
262
+ if (isRemote) {
263
+ const localName = cleanName.replace(/^origin\//, '');
264
+ if (branches.some(b => !b.isRemote && b.name === localName)) continue;
265
+ }
266
+
267
+ const scopeMatch = SCOPE_BRANCH_RE.exec(cleanName);
268
+ const scopeId = scopeMatch ? parseInt(scopeMatch[1]) : null;
269
+ const isStale = headDate ? (now - new Date(headDate).getTime() > STALE_MS) : false;
270
+
271
+ // Ahead/behind relative to origin/main
272
+ let aheadBehind: BranchInfo['aheadBehind'] = null;
273
+ if (!isRemote) {
274
+ try {
275
+ const countRaw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${name}`]);
276
+ const [behind, ahead] = countRaw.trim().split('\t').map(Number);
277
+ aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
278
+ } catch { /* no remote */ }
279
+ }
280
+
281
+ branches.push({
282
+ name: cleanName,
283
+ isRemote,
284
+ isCurrent,
285
+ headSha: headSha ?? '',
286
+ headMessage: msgParts.join('|'),
287
+ headDate: headDate ?? '',
288
+ aheadBehind,
289
+ scopeId,
290
+ isStale,
291
+ });
292
+ }
293
+
294
+ setCache(this.cache as Map<string, CacheEntry<BranchInfo[]>>, 'branches', branches);
295
+ return branches;
296
+ }
297
+
298
+ // ─── Enhanced Worktrees ────────────────────────────────────
299
+
300
+ async getEnhancedWorktrees(): Promise<WorktreeDetail[]> {
301
+ const hit = cached<WorktreeDetail[]>(this.cache as Map<string, CacheEntry<WorktreeDetail[]>>, 'worktrees-enhanced');
302
+ if (hit) return hit;
303
+
304
+ let wts: Array<{ path: string; branch: string; scopeId: number }>;
305
+ try {
306
+ wts = await listWorktrees(this.projectRoot);
307
+ } catch {
308
+ return [];
309
+ }
310
+
311
+ const results: WorktreeDetail[] = [];
312
+ for (const wt of wts) {
313
+ let head = '';
314
+ try {
315
+ head = (await this.git(['rev-parse', '--short', 'HEAD'], wt.path)).trim();
316
+ } catch { /* ok */ }
317
+
318
+ let dirty = false;
319
+ try {
320
+ const status = (await this.git(['status', '--porcelain'], wt.path)).trim();
321
+ dirty = status.length > 0;
322
+ } catch { /* ok */ }
323
+
324
+ let aheadBehind: WorktreeDetail['aheadBehind'] = null;
325
+ try {
326
+ const branchName = wt.branch.replace(/^refs\/heads\//, '');
327
+ const countRaw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${branchName}`], wt.path);
328
+ const [behind, ahead] = countRaw.trim().split('\t').map(Number);
329
+ aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
330
+ } catch { /* ok */ }
331
+
332
+ const scope = wt.scopeId ? this.scopeCache.getById(wt.scopeId) : null;
333
+
334
+ results.push({
335
+ path: wt.path,
336
+ branch: wt.branch.replace(/^refs\/heads\//, ''),
337
+ head,
338
+ scopeId: wt.scopeId,
339
+ scopeTitle: scope?.title ?? null,
340
+ scopeStatus: scope?.status ?? null,
341
+ dirty,
342
+ aheadBehind,
343
+ });
344
+ }
345
+
346
+ setCache(this.cache as Map<string, CacheEntry<WorktreeDetail[]>>, 'worktrees-enhanced', results);
347
+ return results;
348
+ }
349
+
350
+ // ─── Dynamic Drift ─────────────────────────────────────────
351
+
352
+ async getDrift(gitBranches: Array<{ from: string; to: string }>): Promise<DriftPair[]> {
353
+ const cacheKey = `drift:${gitBranches.map(b => `${b.from}-${b.to}`).join(',')}`;
354
+ const hit = cached<DriftPair[]>(this.cache as Map<string, CacheEntry<DriftPair[]>>, cacheKey);
355
+ if (hit) return hit;
356
+
357
+ const pairs: DriftPair[] = [];
358
+ for (const { from, to } of gitBranches) {
359
+ try {
360
+ const raw = await this.git([
361
+ 'log', `origin/${from}`, '--not', `origin/${to}`,
362
+ '--reverse', '--format=%H|%aI|%s|%an',
363
+ ]);
364
+ const commits = raw.trim().split('\n').filter(Boolean).map(line => {
365
+ const [sha, date, ...rest] = line.split('|');
366
+ return { sha, date, message: rest.slice(0, -1).join('|'), author: rest[rest.length - 1] };
367
+ });
368
+ pairs.push({ from, to, count: commits.length, commits });
369
+ } catch {
370
+ pairs.push({ from, to, count: 0, commits: [] });
371
+ }
372
+ }
373
+
374
+ setCache(this.cache as Map<string, CacheEntry<DriftPair[]>>, cacheKey, pairs);
375
+ return pairs;
376
+ }
377
+
378
+ // ─── Git Status Polling ────────────────────────────────────
379
+
380
+ async getStatusHash(): Promise<string> {
381
+ const [head, dirty] = await Promise.all([
382
+ this.git(['rev-parse', 'HEAD']).catch(() => 'none'),
383
+ this.git(['status', '--porcelain']).catch(() => ''),
384
+ ]);
385
+ return `${head.trim()}:${dirty.trim().length > 0 ? 'dirty' : 'clean'}`;
386
+ }
387
+
388
+ clearCache(): void {
389
+ this.cache.clear();
390
+ }
391
+ }