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,183 @@
1
+ import { execFile as execFileCb } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ // Uses execFile (not exec) — safe against shell injection
5
+ const execFile = promisify(execFileCb);
6
+
7
+ // ─── Types ──────────────────────────────────────────────────
8
+
9
+ export interface GitHubStatus {
10
+ connected: boolean;
11
+ authUser: string | null;
12
+ repo: {
13
+ owner: string;
14
+ name: string;
15
+ fullName: string;
16
+ defaultBranch: string;
17
+ visibility: string;
18
+ url: string;
19
+ } | null;
20
+ openPRs: number;
21
+ error: string | null;
22
+ }
23
+
24
+ export interface PullRequestInfo {
25
+ number: number;
26
+ title: string;
27
+ author: string;
28
+ branch: string;
29
+ baseBranch: string;
30
+ state: string;
31
+ url: string;
32
+ createdAt: string;
33
+ scopeIds: number[];
34
+ }
35
+
36
+ // ─── Service ────────────────────────────────────────────────
37
+
38
+ const CACHE_TTL = 30_000; // 30 seconds
39
+ const SCOPE_ID_RE = /(?:scope|feat)[/-](\d+)/gi;
40
+
41
+ export class GitHubService {
42
+ private statusCache: { data: GitHubStatus; ts: number } | null = null;
43
+ private prCache: { data: PullRequestInfo[]; ts: number } | null = null;
44
+
45
+ constructor(private projectRoot: string) {}
46
+
47
+ private async gh(args: string[]): Promise<string> {
48
+ const { stdout } = await execFile('gh', args, {
49
+ cwd: this.projectRoot,
50
+ timeout: 10_000,
51
+ });
52
+ return stdout;
53
+ }
54
+
55
+ private async ghAvailable(): Promise<boolean> {
56
+ try {
57
+ await execFile('which', ['gh']);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ async getStatus(): Promise<GitHubStatus> {
65
+ if (this.statusCache && Date.now() - this.statusCache.ts < CACHE_TTL) {
66
+ return this.statusCache.data;
67
+ }
68
+
69
+ const available = await this.ghAvailable();
70
+ if (!available) {
71
+ const result: GitHubStatus = {
72
+ connected: false,
73
+ authUser: null,
74
+ repo: null,
75
+ openPRs: 0,
76
+ error: 'gh CLI not installed',
77
+ };
78
+ this.statusCache = { data: result, ts: Date.now() };
79
+ return result;
80
+ }
81
+
82
+ // Check auth
83
+ let authUser: string | null = null;
84
+ try {
85
+ const whoami = await this.gh(['api', 'user', '--jq', '.login']);
86
+ authUser = whoami.trim() || null;
87
+ } catch {
88
+ const result: GitHubStatus = {
89
+ connected: false,
90
+ authUser: null,
91
+ repo: null,
92
+ openPRs: 0,
93
+ error: 'gh not authenticated — run `gh auth login`',
94
+ };
95
+ this.statusCache = { data: result, ts: Date.now() };
96
+ return result;
97
+ }
98
+
99
+ // Get repo info
100
+ let repo: GitHubStatus['repo'] = null;
101
+ try {
102
+ const raw = await this.gh([
103
+ 'repo', 'view', '--json', 'owner,name,defaultBranchRef,visibility,url',
104
+ ]);
105
+ const parsed = JSON.parse(raw);
106
+ repo = {
107
+ owner: parsed.owner?.login ?? '',
108
+ name: parsed.name ?? '',
109
+ fullName: `${parsed.owner?.login ?? ''}/${parsed.name ?? ''}`,
110
+ defaultBranch: parsed.defaultBranchRef?.name ?? 'main',
111
+ visibility: (parsed.visibility ?? 'private').toLowerCase(),
112
+ url: parsed.url ?? '',
113
+ };
114
+ } catch {
115
+ const result: GitHubStatus = {
116
+ connected: false,
117
+ authUser,
118
+ repo: null,
119
+ openPRs: 0,
120
+ error: 'Not a GitHub repository',
121
+ };
122
+ this.statusCache = { data: result, ts: Date.now() };
123
+ return result;
124
+ }
125
+
126
+ // Get open PR count
127
+ let openPRs = 0;
128
+ try {
129
+ const raw = await this.gh(['pr', 'list', '--state', 'open', '--json', 'number', '--limit', '100']);
130
+ const parsed = JSON.parse(raw);
131
+ openPRs = Array.isArray(parsed) ? parsed.length : 0;
132
+ } catch { /* ok */ }
133
+
134
+ const result: GitHubStatus = { connected: true, authUser, repo, openPRs, error: null };
135
+ this.statusCache = { data: result, ts: Date.now() };
136
+ return result;
137
+ }
138
+
139
+ async getOpenPRs(): Promise<PullRequestInfo[]> {
140
+ if (this.prCache && Date.now() - this.prCache.ts < CACHE_TTL) {
141
+ return this.prCache.data;
142
+ }
143
+
144
+ try {
145
+ const raw = await this.gh([
146
+ 'pr', 'list', '--state', 'open', '--json',
147
+ 'number,title,author,headRefName,baseRefName,state,url,createdAt',
148
+ '--limit', '30',
149
+ ]);
150
+ const parsed = JSON.parse(raw);
151
+
152
+ const prs: PullRequestInfo[] = (parsed as Array<Record<string, unknown>>).map(pr => {
153
+ const title = String(pr.title ?? '');
154
+ const branch = String(pr.headRefName ?? '');
155
+ const scopeIds: number[] = [];
156
+ const sources = `${title} ${branch}`;
157
+ let m: RegExpExecArray | null;
158
+ SCOPE_ID_RE.lastIndex = 0;
159
+ while ((m = SCOPE_ID_RE.exec(sources)) !== null) {
160
+ const id = parseInt(m[1]);
161
+ if (!scopeIds.includes(id)) scopeIds.push(id);
162
+ }
163
+
164
+ return {
165
+ number: Number(pr.number),
166
+ title,
167
+ author: typeof pr.author === 'object' && pr.author ? String((pr.author as Record<string, unknown>).login ?? '') : '',
168
+ branch,
169
+ baseBranch: String(pr.baseRefName ?? ''),
170
+ state: String(pr.state ?? ''),
171
+ url: String(pr.url ?? ''),
172
+ createdAt: String(pr.createdAt ?? ''),
173
+ scopeIds,
174
+ };
175
+ });
176
+
177
+ this.prCache = { data: prs, ts: Date.now() };
178
+ return prs;
179
+ } catch {
180
+ return [];
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,250 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import type { ParsedScope } from '../parsers/scope-parser.js';
4
+ import type { ScopeService } from './scope-service.js';
5
+ import type { GateService, GateRow } from './gate-service.js';
6
+ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
7
+ import type { HookCategory, HookEnforcement, WorkflowEdge } from '../../shared/workflow-config.js';
8
+
9
+ // ─── Types ──────────────────────────────────────────────
10
+
11
+ export type HookReadiness = 'pass' | 'fail' | 'unknown';
12
+
13
+ export interface HookStatus {
14
+ id: string;
15
+ label: string;
16
+ category: HookCategory;
17
+ enforcement: HookEnforcement;
18
+ status: HookReadiness;
19
+ reason: string | null;
20
+ }
21
+
22
+ export interface TransitionReadiness {
23
+ from: string;
24
+ to: string;
25
+ edge: WorkflowEdge;
26
+ hooks: HookStatus[];
27
+ gates: Array<{
28
+ gate_name: string;
29
+ status: string;
30
+ details: string | null;
31
+ duration_ms: number | null;
32
+ run_at: string;
33
+ }>;
34
+ ready: boolean;
35
+ blockers: string[];
36
+ }
37
+
38
+ export interface ScopeReadiness {
39
+ scope_id: number;
40
+ current_status: string;
41
+ transitions: TransitionReadiness[];
42
+ }
43
+
44
+ // ─── ReadinessService ───────────────────────────────────
45
+
46
+ export class ReadinessService {
47
+ constructor(
48
+ private scopeService: ScopeService,
49
+ private gateService: GateService,
50
+ private engine: WorkflowEngine,
51
+ private projectRoot: string,
52
+ ) {}
53
+
54
+ getReadiness(scopeId: number): ScopeReadiness | null {
55
+ const scope = this.scopeService.getById(scopeId);
56
+ if (!scope) return null;
57
+
58
+ const targets = this.engine.getValidTargets(scope.status);
59
+ const transitions: TransitionReadiness[] = [];
60
+
61
+ for (const to of targets) {
62
+ const edge = this.engine.findEdge(scope.status, to);
63
+ if (!edge) continue;
64
+
65
+ // Only show forward and shortcut transitions (not backward)
66
+ if (edge.direction === 'backward') continue;
67
+
68
+ const hooks = this.evaluateHooks(scope, edge);
69
+ const gates = this.getGatesForScope(scopeId);
70
+ const blockers = this.computeBlockers(hooks, scope);
71
+
72
+ transitions.push({
73
+ from: scope.status,
74
+ to,
75
+ edge,
76
+ hooks,
77
+ gates,
78
+ ready: blockers.length === 0,
79
+ blockers,
80
+ });
81
+ }
82
+
83
+ return {
84
+ scope_id: scopeId,
85
+ current_status: scope.status,
86
+ transitions,
87
+ };
88
+ }
89
+
90
+ private evaluateHooks(scope: ParsedScope, edge: WorkflowEdge): HookStatus[] {
91
+ const hooks = this.engine.getHooksForEdge(edge.from, edge.to);
92
+ return hooks.map((hook) => {
93
+ const enforcement = this.engine.getHookEnforcement(hook);
94
+ const { status, reason } = this.evaluateHook(hook.id, scope, edge);
95
+ return {
96
+ id: hook.id,
97
+ label: hook.label,
98
+ category: hook.category,
99
+ enforcement,
100
+ status,
101
+ reason,
102
+ };
103
+ });
104
+ }
105
+
106
+ private evaluateHook(
107
+ hookId: string,
108
+ scope: ParsedScope,
109
+ edge: WorkflowEdge,
110
+ ): { status: HookReadiness; reason: string | null } {
111
+ switch (hookId) {
112
+ case 'session-enforcer':
113
+ return this.checkSessionEnforcer(scope, edge);
114
+ case 'review-gate-check':
115
+ return this.checkReviewGate(scope);
116
+ case 'completion-checklist':
117
+ return this.checkCompletionChecklist(scope);
118
+ case 'blocker-check':
119
+ return this.checkBlockers(scope);
120
+ case 'dependency-check':
121
+ return this.checkDependencies(scope);
122
+ case 'scope-create-gate':
123
+ return this.checkScopeStructure(scope);
124
+ case 'scope-transition':
125
+ return { status: 'pass', reason: 'Lifecycle hook (runs on transition)' };
126
+ case 'orbital-scope-update':
127
+ case 'scope-commit-logger':
128
+ return { status: 'pass', reason: 'Observer (post-transition)' };
129
+ default:
130
+ return { status: 'unknown', reason: 'No pre-check available' };
131
+ }
132
+ }
133
+
134
+ private checkSessionEnforcer(
135
+ scope: ParsedScope,
136
+ edge: WorkflowEdge,
137
+ ): { status: HookReadiness; reason: string | null } {
138
+ const targetList = this.engine.getList(edge.to);
139
+ const sessionKey = targetList?.sessionKey;
140
+ if (!sessionKey) return { status: 'pass', reason: 'No session key required' };
141
+
142
+ const sessions = scope.sessions ?? {};
143
+ const recorded = sessions[sessionKey];
144
+ if (Array.isArray(recorded) && recorded.length > 0) {
145
+ return { status: 'pass', reason: `Session recorded (${recorded.length} session(s))` };
146
+ }
147
+ return { status: 'fail', reason: `No '${sessionKey}' session recorded in scope frontmatter` };
148
+ }
149
+
150
+ private checkReviewGate(scope: ParsedScope): { status: HookReadiness; reason: string | null } {
151
+ const paddedId = String(scope.id).padStart(3, '0');
152
+ const verdictPath = path.join(this.projectRoot, '.claude', 'review-verdicts', `${paddedId}.json`);
153
+
154
+ if (!fs.existsSync(verdictPath)) {
155
+ return { status: 'fail', reason: 'No review verdict file found' };
156
+ }
157
+
158
+ try {
159
+ const verdict = JSON.parse(fs.readFileSync(verdictPath, 'utf-8'));
160
+ if (verdict.verdict === 'PASS') {
161
+ return { status: 'pass', reason: 'Review verdict: PASS' };
162
+ }
163
+ return { status: 'fail', reason: `Review verdict: ${verdict.verdict ?? 'unknown'}` };
164
+ } catch {
165
+ return { status: 'fail', reason: 'Failed to parse review verdict file' };
166
+ }
167
+ }
168
+
169
+ private checkCompletionChecklist(scope: ParsedScope): { status: HookReadiness; reason: string | null } {
170
+ const content = scope.raw_content ?? '';
171
+ const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
172
+ const checked = (content.match(/^- \[x\]/gim) ?? []).length;
173
+
174
+ if (checked + unchecked === 0) {
175
+ return { status: 'unknown', reason: 'No checklist items found in scope' };
176
+ }
177
+ if (unchecked > 0) {
178
+ return { status: 'fail', reason: `${unchecked} unchecked item(s) in DoD checklist` };
179
+ }
180
+ return { status: 'pass', reason: `All ${checked} checklist item(s) complete` };
181
+ }
182
+
183
+ private checkBlockers(scope: ParsedScope): { status: HookReadiness; reason: string | null } {
184
+ const blockedBy = scope.blocked_by ?? [];
185
+ if (blockedBy.length === 0) {
186
+ return { status: 'pass', reason: 'No blockers' };
187
+ }
188
+
189
+ const unresolved: number[] = [];
190
+ for (const blockerId of blockedBy) {
191
+ const blocker = this.scopeService.getById(blockerId);
192
+ if (blocker && !this.engine.isTerminalStatus(blocker.status)) {
193
+ unresolved.push(blockerId);
194
+ }
195
+ }
196
+
197
+ if (unresolved.length === 0) {
198
+ return { status: 'pass', reason: `All ${blockedBy.length} blocker(s) resolved` };
199
+ }
200
+ return {
201
+ status: 'fail',
202
+ reason: `Blocked by unresolved scope(s): ${unresolved.join(', ')}`,
203
+ };
204
+ }
205
+
206
+ private checkDependencies(scope: ParsedScope): { status: HookReadiness; reason: string | null } {
207
+ // Same check as blockers — dependency-check and blocker-check serve similar roles
208
+ return this.checkBlockers(scope);
209
+ }
210
+
211
+ private checkScopeStructure(scope: ParsedScope): { status: HookReadiness; reason: string | null } {
212
+ if (!scope.title || scope.title.trim() === '') {
213
+ return { status: 'fail', reason: 'Scope has no title' };
214
+ }
215
+ if (!scope.raw_content || scope.raw_content.trim() === '') {
216
+ return { status: 'fail', reason: 'Scope has no content body' };
217
+ }
218
+ return { status: 'pass', reason: 'Scope structure valid' };
219
+ }
220
+
221
+ private getGatesForScope(scopeId: number): GateRow[] {
222
+ const scoped = this.gateService.getLatestForScope(scopeId);
223
+ if (scoped.length > 0) return scoped;
224
+
225
+ // Fall back to global latest run if no scope-specific gates exist
226
+ return this.gateService.getLatestRun();
227
+ }
228
+
229
+ private computeBlockers(hooks: HookStatus[], scope: ParsedScope): string[] {
230
+ const blockers: string[] = [];
231
+
232
+ // Only guards (blockers) actually prevent transitions
233
+ for (const hook of hooks) {
234
+ if (hook.enforcement === 'blocker' && hook.status === 'fail') {
235
+ blockers.push(`${hook.label}: ${hook.reason}`);
236
+ }
237
+ }
238
+
239
+ // Check for unresolved scope blockers
240
+ const blockedBy = scope.blocked_by ?? [];
241
+ for (const blockerId of blockedBy) {
242
+ const blocker = this.scopeService.getById(blockerId);
243
+ if (blocker && !this.engine.isTerminalStatus(blocker.status)) {
244
+ blockers.push(`Blocked by scope ${blockerId} (${blocker.status})`);
245
+ }
246
+ }
247
+
248
+ return blockers;
249
+ }
250
+ }
@@ -0,0 +1,81 @@
1
+ import type { ParsedScope } from '../parsers/scope-parser.js';
2
+
3
+ /**
4
+ * In-memory cache for parsed scopes.
5
+ * Dual-indexed: ID lookups (API, sprint orchestrator) and file-path reverse index (watcher deletions).
6
+ * Replaces the SQLite `scopes` table — filesystem frontmatter is the single source of truth.
7
+ */
8
+ export class ScopeCache {
9
+ private byId = new Map<number, ParsedScope>();
10
+ private filePathToId = new Map<string, number>();
11
+
12
+ /** Bulk-load all scopes (called at startup from parseAllScopes result) */
13
+ loadAll(scopes: ParsedScope[]): void {
14
+ this.byId.clear();
15
+ this.filePathToId.clear();
16
+ for (const scope of scopes) {
17
+ this.byId.set(scope.id, scope);
18
+ this.filePathToId.set(scope.file_path, scope.id);
19
+ }
20
+ }
21
+
22
+ /** Insert or update a single scope */
23
+ set(scope: ParsedScope): void {
24
+ // Clean up old file_path mapping if the scope moved directories
25
+ const existing = this.byId.get(scope.id);
26
+ if (existing && existing.file_path !== scope.file_path) {
27
+ this.filePathToId.delete(existing.file_path);
28
+ }
29
+ this.byId.set(scope.id, scope);
30
+ this.filePathToId.set(scope.file_path, scope.id);
31
+ }
32
+
33
+ /** Remove a scope by its file path (used by watcher on file deletion) */
34
+ removeByFilePath(filePath: string): number | undefined {
35
+ const id = this.filePathToId.get(filePath);
36
+ if (id !== undefined) {
37
+ this.byId.delete(id);
38
+ this.filePathToId.delete(filePath);
39
+ }
40
+ return id;
41
+ }
42
+
43
+ /** Look up scope ID by file path (used before removal to stash status) */
44
+ idByFilePath(filePath: string): number | undefined {
45
+ return this.filePathToId.get(filePath);
46
+ }
47
+
48
+ /** Check if scope exists by ID */
49
+ has(id: number): boolean {
50
+ return this.byId.has(id);
51
+ }
52
+
53
+ /** Get a scope by ID */
54
+ getById(id: number): ParsedScope | undefined {
55
+ return this.byId.get(id);
56
+ }
57
+
58
+ /** Get all scopes sorted by ID */
59
+ getAll(): ParsedScope[] {
60
+ return [...this.byId.values()].sort((a, b) => a.id - b.id);
61
+ }
62
+
63
+ /** Get the maximum raw scope number excluding icebox scopes (for next-ID generation).
64
+ * Cache keys use encoded IDs (suffixed scopes like 047a → 1047, 075x → 9075),
65
+ * but next-ID generation needs the raw scope number (047, 075, 087). */
66
+ maxNonIceboxId(): number {
67
+ let max = 0;
68
+ for (const [id, scope] of this.byId) {
69
+ if (scope.status === 'icebox') continue;
70
+ // Decode: encoded IDs ≥1000 have a suffix offset — raw number is id % 1000
71
+ const raw = id >= 1000 ? id % 1000 : id;
72
+ if (raw > max) max = raw;
73
+ }
74
+ return max;
75
+ }
76
+
77
+ /** Total number of cached scopes */
78
+ get size(): number {
79
+ return this.byId.size;
80
+ }
81
+ }