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,74 @@
1
+ import fs from 'fs';
2
+ import { createLogger } from '../utils/logger.js';
3
+
4
+ const log = createLogger('event');
5
+
6
+ export interface RawEvent {
7
+ id: string;
8
+ type: string;
9
+ scope_id?: number | null;
10
+ session_id?: string | null;
11
+ agent?: string | null;
12
+ data?: Record<string, unknown>;
13
+ timestamp: string;
14
+ }
15
+
16
+ /**
17
+ * Parse a JSON event file from .claude/orbital-events/
18
+ *
19
+ * Handles two formats:
20
+ * - Full format: top-level scope_id, agent, session_id fields
21
+ * - Minimal format: all info nested inside `data` (from orbital-emit.sh)
22
+ *
23
+ * When top-level fields are missing, extracts them from `data`:
24
+ * - data.agent or data.agents[0] → agent
25
+ * - data.scope_id → scope_id
26
+ * - data.session_id → session_id
27
+ */
28
+ export function parseEventFile(filePath: string): RawEvent | null {
29
+ try {
30
+ const content = fs.readFileSync(filePath, 'utf-8');
31
+ const parsed = JSON.parse(content) as Record<string, unknown>;
32
+
33
+ if (!parsed.id || !parsed.type || !parsed.timestamp) {
34
+ return null;
35
+ }
36
+
37
+ const data = (parsed.data ?? {}) as Record<string, unknown>;
38
+
39
+ return {
40
+ id: String(parsed.id),
41
+ type: String(parsed.type),
42
+ scope_id: extractScopeId(parsed.scope_id, data),
43
+ session_id: extractString(parsed.session_id, data.session_id),
44
+ agent: extractAgent(parsed.agent, data),
45
+ data,
46
+ timestamp: String(parsed.timestamp),
47
+ };
48
+ } catch (err) {
49
+ log.warn('Failed to parse event file', { file: filePath, error: (err as Error).message });
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /** Extract scope_id from top-level or data payload */
55
+ function extractScopeId(topLevel: unknown, data: Record<string, unknown>): number | null {
56
+ if (topLevel != null && topLevel !== '') return Number(topLevel);
57
+ if (data.scope_id != null && data.scope_id !== '') return Number(data.scope_id);
58
+ return null;
59
+ }
60
+
61
+ /** Extract agent name from top-level or data.agent / data.agents[0] */
62
+ function extractAgent(topLevel: unknown, data: Record<string, unknown>): string | null {
63
+ if (typeof topLevel === 'string' && topLevel !== '') return topLevel;
64
+ if (typeof data.agent === 'string' && data.agent !== '') return data.agent;
65
+ if (Array.isArray(data.agents) && data.agents.length > 0) return String(data.agents[0]);
66
+ return null;
67
+ }
68
+
69
+ /** Extract a string value from top-level or data fallback */
70
+ function extractString(topLevel: unknown, fallback: unknown): string | null {
71
+ if (typeof topLevel === 'string' && topLevel !== '') return topLevel;
72
+ if (typeof fallback === 'string' && fallback !== '') return fallback;
73
+ return null;
74
+ }
@@ -0,0 +1,240 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { createLogger } from '../utils/logger.js';
5
+
6
+ const log = createLogger('scope');
7
+
8
+ export interface ParsedScope {
9
+ id: number;
10
+ title: string;
11
+ status: string;
12
+ priority: string | null;
13
+ effort_estimate: string | null;
14
+ category: string | null;
15
+ tags: string[];
16
+ blocked_by: number[];
17
+ blocks: number[];
18
+ file_path: string;
19
+ created_at: string | null;
20
+ updated_at: string | null;
21
+ raw_content: string;
22
+ sessions: Record<string, string[]>;
23
+ is_ghost: boolean;
24
+ }
25
+
26
+ const VALID_PRIORITIES = new Set(['critical', 'high', 'medium', 'low']);
27
+
28
+ const VALID_SESSION_KEYS = new Set([
29
+ 'createScope', 'reviewScope', 'implementScope',
30
+ 'verifyScope', 'reviewGate', 'fixReview', 'commit',
31
+ 'pushToMain', 'pushToDev', 'pushToStaging', 'pushToProduction',
32
+ ]);
33
+
34
+ /** Parse and validate the sessions frontmatter field */
35
+ function parseSessions(raw: unknown): Record<string, string[]> {
36
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
37
+ const result: Record<string, string[]> = {};
38
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
39
+ if (VALID_SESSION_KEYS.has(key) && Array.isArray(value)) {
40
+ result[key] = value.filter((v): v is string => typeof v === 'string');
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+
46
+ // Map frontmatter statuses to 9-column board states
47
+ export const STATUS_MAP: Record<string, string> = {
48
+ 'icebox': 'icebox',
49
+ 'exploring': 'planning',
50
+ 'planning': 'planning',
51
+ 'ready': 'backlog',
52
+ 'backlog': 'backlog',
53
+ 'blocked': 'backlog',
54
+ 'in_progress': 'implementing',
55
+ 'in-progress': 'implementing',
56
+ 'implementing': 'implementing',
57
+ 'testing': 'review',
58
+ 'review': 'review',
59
+ 'complete': 'completed',
60
+ 'completed': 'completed',
61
+ 'done': 'production',
62
+ 'dev': 'dev',
63
+ 'staging': 'staging',
64
+ 'production': 'production',
65
+ };
66
+
67
+ /** Normalize a raw frontmatter status to a valid board status */
68
+ export function normalizeStatus(raw: string): string {
69
+ return STATUS_MAP[raw] ?? raw;
70
+ }
71
+
72
+ /**
73
+ * Parse a scope markdown file into structured data.
74
+ * Handles both YAML frontmatter and plain markdown formats.
75
+ */
76
+ export function parseScopeFile(filePath: string): ParsedScope | null {
77
+ const content = fs.readFileSync(filePath, 'utf-8');
78
+ const fileName = path.basename(filePath, '.md');
79
+ const dirName = path.basename(path.dirname(filePath));
80
+
81
+ // Extract ID from filename pattern: NNN[suffix]-description.md
82
+ // Suffixes (a-d, X) encode as thousands offset for unique DB keys
83
+ const idMatch = fileName.match(/^(\d+)([a-dA-DxX])?/);
84
+ const fileId = idMatch ? scopeFileId(parseInt(idMatch[1], 10), idMatch[2]) : 0;
85
+
86
+ // Skip non-scope files
87
+ if (fileId === 0 && !fileName.startsWith('0')) {
88
+ // Files like _template.md, technical-debt.md, backlog_plan.md
89
+ if (fileName.startsWith('_') || !idMatch) return null;
90
+ }
91
+
92
+ // Try YAML frontmatter first
93
+ const { data: frontmatter, content: markdownBody } = matter(content);
94
+
95
+ if (frontmatter && Object.keys(frontmatter).length > 0) {
96
+ return parseFrontmatterScope(frontmatter, markdownBody, filePath, fileId, dirName);
97
+ }
98
+
99
+ // Fallback: extract from markdown structure
100
+ return parseMarkdownScope(content, filePath, fileId, dirName);
101
+ }
102
+
103
+ function parseFrontmatterScope(
104
+ fm: Record<string, unknown>,
105
+ body: string,
106
+ filePath: string,
107
+ fallbackId: number,
108
+ dirName: string
109
+ ): ParsedScope {
110
+ // Prefer filename-derived ID (includes suffix encoding) over frontmatter
111
+ const id = fallbackId || ((fm.id as number) ?? 0);
112
+ const rawStatus = String(fm.status ?? inferStatusFromDir(dirName));
113
+ const status = STATUS_MAP[rawStatus] ?? rawStatus;
114
+
115
+ return {
116
+ id,
117
+ title: String(fm.title ?? `Scope ${id}`),
118
+ status,
119
+ priority: fm.priority && VALID_PRIORITIES.has(String(fm.priority)) ? String(fm.priority) : null,
120
+ effort_estimate: fm.effort_estimate ? String(fm.effort_estimate) : null,
121
+ category: fm.category ? String(fm.category) : null,
122
+ tags: Array.isArray(fm.tags) ? fm.tags.map(String) : [],
123
+ blocked_by: Array.isArray(fm.blocked_by) ? fm.blocked_by.map(Number) : [],
124
+ blocks: Array.isArray(fm.blocks) ? fm.blocks.map(Number) : [],
125
+ file_path: filePath,
126
+ created_at: fm.created ? String(fm.created) : null,
127
+ updated_at: fm.updated ? String(fm.updated) : null,
128
+ raw_content: body.trim(),
129
+ sessions: parseSessions(fm.sessions),
130
+ is_ghost: fm.ghost === true,
131
+ };
132
+ }
133
+
134
+ function parseMarkdownScope(
135
+ content: string,
136
+ filePath: string,
137
+ id: number,
138
+ dirName: string
139
+ ): ParsedScope {
140
+ // Extract title from first # heading
141
+ const titleMatch = content.match(/^#\s+(?:Scope\s+\d+:\s*)?(.+)/m);
142
+ const title = titleMatch ? titleMatch[1].trim() : `Scope ${id}`;
143
+
144
+ // Extract priority from markdown
145
+ const priorityMatch = content.match(/##\s*Priority:\s*(?:[🔴🟡🟢⚪]\s*)?(\w+)/i);
146
+ const rawPriority = priorityMatch ? priorityMatch[1].toLowerCase() : null;
147
+ const priority = rawPriority && VALID_PRIORITIES.has(rawPriority) ? rawPriority : null;
148
+
149
+ // Extract effort estimate
150
+ const effortMatch = content.match(/##\s*(?:Estimated\s+)?Effort:\s*(.+)/i);
151
+ const effort_estimate = effortMatch ? effortMatch[1].trim() : null;
152
+
153
+ // Extract category
154
+ const categoryMatch = content.match(/##\s*Category:\s*(.+)/i);
155
+ const category = categoryMatch ? categoryMatch[1].trim() : null;
156
+
157
+ // Determine status from directory or content
158
+ const status = inferStatusFromDir(dirName);
159
+
160
+ return {
161
+ id,
162
+ title,
163
+ status,
164
+ priority,
165
+ effort_estimate,
166
+ category,
167
+ tags: [],
168
+ blocked_by: [],
169
+ blocks: [],
170
+ file_path: filePath,
171
+ created_at: null,
172
+ updated_at: null,
173
+ raw_content: content,
174
+ sessions: {},
175
+ is_ghost: false,
176
+ };
177
+ }
178
+
179
+ /** Map filename suffix (a-d, X) to a thousands-digit offset for unique IDs */
180
+ function scopeFileId(base: number, suffix?: string): number {
181
+ if (!suffix) return base;
182
+ const lower = suffix.toLowerCase();
183
+ if (lower === 'x') return 9000 + base;
184
+ // a=1000, b=2000, c=3000, d=4000
185
+ const offset = (lower.charCodeAt(0) - 96) * 1000;
186
+ return offset + base;
187
+ }
188
+
189
+ /** Valid directory statuses — updated at startup from the workflow engine */
190
+ let validDirStatuses: Set<string> | null = null;
191
+
192
+ /** Initialize the valid status set from the workflow engine's list IDs */
193
+ export function setValidStatuses(statuses: Iterable<string>): void {
194
+ validDirStatuses = new Set(statuses);
195
+ }
196
+
197
+ function inferStatusFromDir(dirName: string): string {
198
+ if (validDirStatuses) {
199
+ return validDirStatuses.has(dirName) ? dirName : 'planning';
200
+ }
201
+ // Fallback for when engine hasn't initialized yet (shouldn't happen in practice)
202
+ return dirName;
203
+ }
204
+
205
+ /**
206
+ * Scan all scope directories and parse all scope files.
207
+ */
208
+ export function parseAllScopes(scopesDir: string): ParsedScope[] {
209
+ const scopes: ParsedScope[] = [];
210
+
211
+ if (!fs.existsSync(scopesDir)) return scopes;
212
+
213
+ // Recursively find all .md files
214
+ function scanDir(dir: string) {
215
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
216
+ for (const entry of entries) {
217
+ const fullPath = path.join(dir, entry.name);
218
+ if (entry.isDirectory()) {
219
+ scanDir(fullPath);
220
+ } else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
221
+ const parsed = parseScopeFile(fullPath);
222
+ if (parsed) scopes.push(parsed);
223
+ }
224
+ }
225
+ }
226
+
227
+ scanDir(scopesDir);
228
+
229
+ // Detect ID collisions — last-write-wins but warn on stderr
230
+ const seen = new Map<number, string>();
231
+ for (const scope of scopes) {
232
+ const existing = seen.get(scope.id);
233
+ if (existing) {
234
+ log.error('Scope ID collision — renumber one of them', { id: scope.id, existing, duplicate: scope.file_path });
235
+ }
236
+ seen.set(scope.id, scope.file_path);
237
+ }
238
+
239
+ return scopes.sort((a, b) => a.id - b.id);
240
+ }
@@ -0,0 +1,182 @@
1
+ import { Router } from 'express';
2
+ import type { Server } from 'socket.io';
3
+ import { ConfigService, isValidPrimitiveType } from '../services/config-service.js';
4
+ import type { ConfigPrimitiveType } from '../services/config-service.js';
5
+ import type { WorkflowService } from '../services/workflow-service.js';
6
+
7
+ interface ConfigRouteDeps {
8
+ projectRoot: string;
9
+ workflowService: WorkflowService;
10
+ io: Server;
11
+ }
12
+
13
+ export function createConfigRoutes({ projectRoot, workflowService: _workflowService, io }: ConfigRouteDeps): Router {
14
+ const router = Router();
15
+ const configService = new ConfigService(projectRoot);
16
+
17
+ /** Validate :type param and return the primitive type, or send 400 */
18
+ function parseType(typeParam: string, res: import('express').Response): ConfigPrimitiveType | null {
19
+ if (!isValidPrimitiveType(typeParam)) {
20
+ res.status(400).json({ success: false, error: `Invalid type "${typeParam}". Must be one of: agents, skills, hooks` });
21
+ return null;
22
+ }
23
+ return typeParam;
24
+ }
25
+
26
+ // GET /config/:type/tree — directory tree with frontmatter
27
+ router.get('/config/:type/tree', (req, res) => {
28
+ const type = parseType(req.params.type, res);
29
+ if (!type) return;
30
+
31
+ try {
32
+ const basePath = configService.getBasePath(type);
33
+ const tree = configService.scanDirectory(basePath);
34
+ res.json({ success: true, data: tree });
35
+ } catch (err) {
36
+ res.status(500).json({ success: false, error: errMsg(err) });
37
+ }
38
+ });
39
+
40
+ // GET /config/:type/file?path=<relative> — file content
41
+ router.get('/config/:type/file', (req, res) => {
42
+ const type = parseType(req.params.type, res);
43
+ if (!type) return;
44
+
45
+ const filePath = req.query.path as string | undefined;
46
+ if (!filePath) {
47
+ res.status(400).json({ success: false, error: 'path query parameter is required' });
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const basePath = configService.getBasePath(type);
53
+ const content = configService.readFile(basePath, filePath);
54
+ res.json({ success: true, data: { path: filePath, content } });
55
+ } catch (err) {
56
+ const msg = errMsg(err);
57
+ const status = msg.includes('traversal') ? 403 : msg.includes('ENOENT') || msg.includes('not found') ? 404 : 500;
58
+ res.status(status).json({ success: false, error: msg });
59
+ }
60
+ });
61
+
62
+ // PUT /config/:type/file — save file { path, content }
63
+ router.put('/config/:type/file', (req, res) => {
64
+ const type = parseType(req.params.type, res);
65
+ if (!type) return;
66
+
67
+ const { path: filePath, content } = req.body as { path?: string; content?: string };
68
+ if (!filePath || content === undefined) {
69
+ res.status(400).json({ success: false, error: 'path and content are required' });
70
+ return;
71
+ }
72
+
73
+ try {
74
+ const basePath = configService.getBasePath(type);
75
+ configService.writeFile(basePath, filePath, content);
76
+ io.emit(`config:${type}:changed`, { action: 'updated', path: filePath });
77
+ res.json({ success: true });
78
+ } catch (err) {
79
+ const msg = errMsg(err);
80
+ const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : 500;
81
+ res.status(status).json({ success: false, error: msg });
82
+ }
83
+ });
84
+
85
+ // POST /config/:type/file — create file { path, content }
86
+ router.post('/config/:type/file', (req, res) => {
87
+ const type = parseType(req.params.type, res);
88
+ if (!type) return;
89
+
90
+ const { path: filePath, content } = req.body as { path?: string; content?: string };
91
+ if (!filePath || content === undefined) {
92
+ res.status(400).json({ success: false, error: 'path and content are required' });
93
+ return;
94
+ }
95
+
96
+ try {
97
+ const basePath = configService.getBasePath(type);
98
+ configService.createFile(basePath, filePath, content);
99
+ io.emit(`config:${type}:changed`, { action: 'created', path: filePath });
100
+ res.status(201).json({ success: true });
101
+ } catch (err) {
102
+ const msg = errMsg(err);
103
+ const status = msg.includes('traversal') ? 403 : msg.includes('already exists') ? 409 : 500;
104
+ res.status(status).json({ success: false, error: msg });
105
+ }
106
+ });
107
+
108
+ // DELETE /config/:type/file?path=<relative> — delete file
109
+ router.delete('/config/:type/file', (req, res) => {
110
+ const type = parseType(req.params.type, res);
111
+ if (!type) return;
112
+
113
+ const filePath = req.query.path as string | undefined;
114
+ if (!filePath) {
115
+ res.status(400).json({ success: false, error: 'path query parameter is required' });
116
+ return;
117
+ }
118
+
119
+ try {
120
+ const basePath = configService.getBasePath(type);
121
+ configService.deleteFile(basePath, filePath);
122
+ io.emit(`config:${type}:changed`, { action: 'deleted', path: filePath });
123
+ res.json({ success: true });
124
+ } catch (err) {
125
+ const msg = errMsg(err);
126
+ const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : msg.includes('directory') ? 400 : 500;
127
+ res.status(status).json({ success: false, error: msg });
128
+ }
129
+ });
130
+
131
+ // POST /config/:type/rename — rename { oldPath, newPath }
132
+ router.post('/config/:type/rename', (req, res) => {
133
+ const type = parseType(req.params.type, res);
134
+ if (!type) return;
135
+
136
+ const { oldPath, newPath } = req.body as { oldPath?: string; newPath?: string };
137
+ if (!oldPath || !newPath) {
138
+ res.status(400).json({ success: false, error: 'oldPath and newPath are required' });
139
+ return;
140
+ }
141
+
142
+ try {
143
+ const basePath = configService.getBasePath(type);
144
+ configService.renameFile(basePath, oldPath, newPath);
145
+ io.emit(`config:${type}:changed`, { action: 'renamed', oldPath, newPath });
146
+ res.json({ success: true });
147
+ } catch (err) {
148
+ const msg = errMsg(err);
149
+ const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : msg.includes('already exists') ? 409 : 500;
150
+ res.status(status).json({ success: false, error: msg });
151
+ }
152
+ });
153
+
154
+ // POST /config/:type/folder — create folder { path }
155
+ router.post('/config/:type/folder', (req, res) => {
156
+ const type = parseType(req.params.type, res);
157
+ if (!type) return;
158
+
159
+ const { path: folderPath } = req.body as { path?: string };
160
+ if (!folderPath) {
161
+ res.status(400).json({ success: false, error: 'path is required' });
162
+ return;
163
+ }
164
+
165
+ try {
166
+ const basePath = configService.getBasePath(type);
167
+ configService.createFolder(basePath, folderPath);
168
+ io.emit(`config:${type}:changed`, { action: 'folder-created', path: folderPath });
169
+ res.status(201).json({ success: true });
170
+ } catch (err) {
171
+ const msg = errMsg(err);
172
+ const status = msg.includes('traversal') ? 403 : msg.includes('already exists') ? 409 : 500;
173
+ res.status(status).json({ success: false, error: msg });
174
+ }
175
+ });
176
+
177
+ return router;
178
+ }
179
+
180
+ function errMsg(err: unknown): string {
181
+ return err instanceof Error ? err.message : String(err);
182
+ }