orbital-command 0.2.0 → 1.0.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 (431) hide show
  1. package/README.md +67 -42
  2. package/bin/commands/config.js +19 -0
  3. package/bin/commands/events.js +40 -0
  4. package/bin/commands/launch.js +126 -0
  5. package/bin/commands/manifest.js +283 -0
  6. package/bin/commands/registry.js +104 -0
  7. package/bin/commands/update.js +24 -0
  8. package/bin/lib/helpers.js +229 -0
  9. package/bin/orbital.js +147 -319
  10. package/dist/assets/Landing-CfQdHR0N.js +11 -0
  11. package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
  12. package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
  13. package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
  14. package/dist/assets/Settings-DLcZwbCT.js +12 -0
  15. package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
  16. package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
  17. package/dist/assets/arrow-down-DVPp6_qp.js +6 -0
  18. package/dist/assets/bot-NFaJBDn_.js +6 -0
  19. package/dist/assets/charts-LGLb8hyU.js +68 -0
  20. package/dist/assets/circle-x-IsFCkBZu.js +6 -0
  21. package/dist/assets/file-text-J1cebZXF.js +6 -0
  22. package/dist/assets/globe-WzeyHsUc.js +6 -0
  23. package/dist/assets/index-BdJ57EhC.css +1 -0
  24. package/dist/assets/index-o4ScMAuR.js +349 -0
  25. package/dist/assets/key-CKR8JJSj.js +6 -0
  26. package/dist/assets/minus-CHBsJyjp.js +6 -0
  27. package/dist/assets/radio-xqZaR-Uk.js +6 -0
  28. package/dist/assets/rocket-D_xvvNG6.js +6 -0
  29. package/dist/assets/shield-TdB1yv_a.js +6 -0
  30. package/dist/assets/ui-BmsSg9jU.js +53 -0
  31. package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
  32. package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
  33. package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
  34. package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
  35. package/dist/assets/zap-C9wqYMpl.js +6 -0
  36. package/dist/favicon.svg +1 -0
  37. package/dist/index.html +6 -5
  38. package/dist/server/server/__tests__/data-routes.test.js +126 -0
  39. package/dist/server/server/__tests__/helpers/db.js +17 -0
  40. package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
  41. package/dist/server/server/__tests__/scope-routes.test.js +138 -0
  42. package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
  43. package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
  44. package/dist/server/server/config-migrator.js +135 -0
  45. package/dist/server/server/config.js +51 -7
  46. package/dist/server/server/database.js +21 -28
  47. package/dist/server/server/global-config.js +143 -0
  48. package/dist/server/server/index.js +118 -276
  49. package/dist/server/server/init.js +243 -225
  50. package/dist/server/server/launch.js +29 -0
  51. package/dist/server/server/manifest-types.js +8 -0
  52. package/dist/server/server/manifest.js +454 -0
  53. package/dist/server/server/migrate-legacy.js +229 -0
  54. package/dist/server/server/parsers/event-parser.js +4 -1
  55. package/dist/server/server/parsers/event-parser.test.js +117 -0
  56. package/dist/server/server/parsers/scope-parser.js +74 -28
  57. package/dist/server/server/parsers/scope-parser.test.js +230 -0
  58. package/dist/server/server/project-context.js +265 -0
  59. package/dist/server/server/project-emitter.js +41 -0
  60. package/dist/server/server/project-manager.js +297 -0
  61. package/dist/server/server/routes/aggregate-routes.js +871 -0
  62. package/dist/server/server/routes/config-routes.js +41 -90
  63. package/dist/server/server/routes/data-routes.js +25 -123
  64. package/dist/server/server/routes/dispatch-routes.js +37 -15
  65. package/dist/server/server/routes/git-routes.js +74 -0
  66. package/dist/server/server/routes/manifest-routes.js +319 -0
  67. package/dist/server/server/routes/scope-routes.js +45 -28
  68. package/dist/server/server/routes/sync-routes.js +134 -0
  69. package/dist/server/server/routes/version-routes.js +1 -15
  70. package/dist/server/server/routes/workflow-routes.js +9 -3
  71. package/dist/server/server/schema.js +3 -0
  72. package/dist/server/server/services/batch-orchestrator.js +41 -17
  73. package/dist/server/server/services/claude-session-service.js +17 -14
  74. package/dist/server/server/services/config-service.js +10 -1
  75. package/dist/server/server/services/deploy-service.test.js +119 -0
  76. package/dist/server/server/services/event-service.js +64 -1
  77. package/dist/server/server/services/event-service.test.js +191 -0
  78. package/dist/server/server/services/gate-service.test.js +105 -0
  79. package/dist/server/server/services/git-service.js +108 -4
  80. package/dist/server/server/services/github-service.js +110 -2
  81. package/dist/server/server/services/readiness-service.test.js +190 -0
  82. package/dist/server/server/services/scope-cache.js +5 -1
  83. package/dist/server/server/services/scope-cache.test.js +142 -0
  84. package/dist/server/server/services/scope-service.js +222 -131
  85. package/dist/server/server/services/scope-service.test.js +137 -0
  86. package/dist/server/server/services/sprint-orchestrator.js +29 -15
  87. package/dist/server/server/services/sprint-service.js +23 -3
  88. package/dist/server/server/services/sprint-service.test.js +238 -0
  89. package/dist/server/server/services/sync-service.js +434 -0
  90. package/dist/server/server/services/sync-types.js +2 -0
  91. package/dist/server/server/services/workflow-service.js +26 -5
  92. package/dist/server/server/services/workflow-service.test.js +159 -0
  93. package/dist/server/server/settings-sync.js +284 -0
  94. package/dist/server/server/uninstall.js +195 -0
  95. package/dist/server/server/update-planner.js +279 -0
  96. package/dist/server/server/update.js +212 -0
  97. package/dist/server/server/utils/cc-hooks-parser.js +3 -0
  98. package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
  99. package/dist/server/server/utils/dispatch-utils.js +83 -24
  100. package/dist/server/server/utils/dispatch-utils.test.js +182 -0
  101. package/dist/server/server/utils/flag-builder.js +54 -0
  102. package/dist/server/server/utils/json-fields.js +14 -0
  103. package/dist/server/server/utils/json-fields.test.js +73 -0
  104. package/dist/server/server/utils/logger.js +37 -3
  105. package/dist/server/server/utils/package-info.js +30 -0
  106. package/dist/server/server/utils/route-helpers.js +47 -0
  107. package/dist/server/server/utils/route-helpers.test.js +115 -0
  108. package/dist/server/server/utils/terminal-launcher.js +79 -25
  109. package/dist/server/server/utils/worktree-manager.js +13 -4
  110. package/dist/server/server/validator.js +230 -0
  111. package/dist/server/server/watchers/event-watcher.js +28 -13
  112. package/dist/server/server/watchers/global-watcher.js +63 -0
  113. package/dist/server/server/watchers/scope-watcher.js +27 -12
  114. package/dist/server/server/wizard/config-editor.js +237 -0
  115. package/dist/server/server/wizard/detect.js +96 -0
  116. package/dist/server/server/wizard/doctor.js +115 -0
  117. package/dist/server/server/wizard/index.js +340 -0
  118. package/dist/server/server/wizard/phases/confirm.js +39 -0
  119. package/dist/server/server/wizard/phases/project-setup.js +90 -0
  120. package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
  121. package/dist/server/server/wizard/phases/welcome.js +32 -0
  122. package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
  123. package/dist/server/server/wizard/types.js +29 -0
  124. package/dist/server/server/wizard/ui.js +73 -0
  125. package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
  126. package/dist/server/shared/api-types.js +80 -1
  127. package/dist/server/shared/default-workflow.json +65 -0
  128. package/dist/server/shared/onboarding-tour.test.js +81 -0
  129. package/dist/server/shared/project-colors.js +24 -0
  130. package/dist/server/shared/workflow-config.test.js +84 -0
  131. package/dist/server/shared/workflow-engine.js +1 -1
  132. package/dist/server/shared/workflow-engine.test.js +302 -0
  133. package/dist/server/shared/workflow-normalizer.js +101 -0
  134. package/dist/server/shared/workflow-normalizer.test.js +100 -0
  135. package/dist/server/src/components/onboarding/tour-steps.js +84 -0
  136. package/package.json +34 -29
  137. package/schemas/orbital.config.schema.json +2 -5
  138. package/scripts/postinstall.js +18 -6
  139. package/scripts/release.sh +53 -0
  140. package/server/__tests__/data-routes.test.ts +151 -0
  141. package/server/__tests__/helpers/db.ts +19 -0
  142. package/server/__tests__/helpers/mock-emitter.ts +10 -0
  143. package/server/__tests__/scope-routes.test.ts +158 -0
  144. package/server/__tests__/sprint-routes.test.ts +118 -0
  145. package/server/__tests__/workflow-routes.test.ts +120 -0
  146. package/server/config-migrator.ts +160 -0
  147. package/server/config.ts +64 -12
  148. package/server/database.ts +22 -31
  149. package/server/global-config.ts +204 -0
  150. package/server/index.ts +139 -316
  151. package/server/init.ts +266 -234
  152. package/server/launch.ts +32 -0
  153. package/server/manifest-types.ts +145 -0
  154. package/server/manifest.ts +494 -0
  155. package/server/migrate-legacy.ts +290 -0
  156. package/server/parsers/event-parser.test.ts +135 -0
  157. package/server/parsers/event-parser.ts +4 -1
  158. package/server/parsers/scope-parser.test.ts +270 -0
  159. package/server/parsers/scope-parser.ts +79 -31
  160. package/server/project-context.ts +325 -0
  161. package/server/project-emitter.ts +50 -0
  162. package/server/project-manager.ts +368 -0
  163. package/server/routes/aggregate-routes.ts +968 -0
  164. package/server/routes/config-routes.ts +43 -85
  165. package/server/routes/data-routes.ts +34 -156
  166. package/server/routes/dispatch-routes.ts +46 -17
  167. package/server/routes/git-routes.ts +77 -0
  168. package/server/routes/manifest-routes.ts +388 -0
  169. package/server/routes/scope-routes.ts +39 -30
  170. package/server/routes/sync-routes.ts +175 -0
  171. package/server/routes/version-routes.ts +1 -16
  172. package/server/routes/workflow-routes.ts +9 -3
  173. package/server/schema.ts +3 -0
  174. package/server/services/batch-orchestrator.ts +41 -17
  175. package/server/services/claude-session-service.ts +16 -14
  176. package/server/services/config-service.ts +10 -1
  177. package/server/services/deploy-service.test.ts +145 -0
  178. package/server/services/deploy-service.ts +2 -2
  179. package/server/services/event-service.test.ts +242 -0
  180. package/server/services/event-service.ts +92 -3
  181. package/server/services/gate-service.test.ts +131 -0
  182. package/server/services/gate-service.ts +2 -2
  183. package/server/services/git-service.ts +137 -4
  184. package/server/services/github-service.ts +120 -2
  185. package/server/services/readiness-service.test.ts +217 -0
  186. package/server/services/scope-cache.test.ts +167 -0
  187. package/server/services/scope-cache.ts +4 -1
  188. package/server/services/scope-service.test.ts +169 -0
  189. package/server/services/scope-service.ts +224 -130
  190. package/server/services/sprint-orchestrator.ts +30 -15
  191. package/server/services/sprint-service.test.ts +271 -0
  192. package/server/services/sprint-service.ts +29 -5
  193. package/server/services/sync-service.ts +482 -0
  194. package/server/services/sync-types.ts +77 -0
  195. package/server/services/workflow-service.test.ts +190 -0
  196. package/server/services/workflow-service.ts +29 -9
  197. package/server/settings-sync.ts +359 -0
  198. package/server/uninstall.ts +214 -0
  199. package/server/update-planner.ts +346 -0
  200. package/server/update.ts +263 -0
  201. package/server/utils/cc-hooks-parser.test.ts +96 -0
  202. package/server/utils/cc-hooks-parser.ts +4 -0
  203. package/server/utils/dispatch-utils.test.ts +245 -0
  204. package/server/utils/dispatch-utils.ts +102 -30
  205. package/server/utils/flag-builder.ts +56 -0
  206. package/server/utils/json-fields.test.ts +83 -0
  207. package/server/utils/json-fields.ts +14 -0
  208. package/server/utils/logger.ts +40 -3
  209. package/server/utils/package-info.ts +32 -0
  210. package/server/utils/route-helpers.test.ts +144 -0
  211. package/server/utils/route-helpers.ts +50 -0
  212. package/server/utils/terminal-launcher.ts +85 -25
  213. package/server/utils/worktree-manager.ts +9 -4
  214. package/server/validator.ts +270 -0
  215. package/server/watchers/event-watcher.ts +24 -12
  216. package/server/watchers/global-watcher.ts +77 -0
  217. package/server/watchers/scope-watcher.ts +21 -9
  218. package/server/wizard/config-editor.ts +248 -0
  219. package/server/wizard/detect.ts +104 -0
  220. package/server/wizard/doctor.ts +114 -0
  221. package/server/wizard/index.ts +438 -0
  222. package/server/wizard/phases/confirm.ts +45 -0
  223. package/server/wizard/phases/project-setup.ts +106 -0
  224. package/server/wizard/phases/setup-wizard.ts +78 -0
  225. package/server/wizard/phases/welcome.ts +39 -0
  226. package/server/wizard/phases/workflow-setup.ts +28 -0
  227. package/server/wizard/types.ts +56 -0
  228. package/server/wizard/ui.ts +92 -0
  229. package/shared/__fixtures__/workflow-configs.ts +80 -0
  230. package/shared/api-types.ts +106 -0
  231. package/shared/onboarding-tour.test.ts +94 -0
  232. package/shared/project-colors.ts +24 -0
  233. package/shared/workflow-config.test.ts +111 -0
  234. package/shared/workflow-config.ts +7 -0
  235. package/shared/workflow-engine.test.ts +388 -0
  236. package/shared/workflow-engine.ts +1 -1
  237. package/shared/workflow-normalizer.test.ts +119 -0
  238. package/shared/workflow-normalizer.ts +118 -0
  239. package/templates/agents/QUICK-REFERENCE.md +1 -0
  240. package/templates/agents/README.md +1 -0
  241. package/templates/agents/SKILL-TRIGGERS.md +11 -0
  242. package/templates/agents/green-team/deep-dive.md +361 -0
  243. package/templates/hooks/end-session.sh +4 -1
  244. package/templates/hooks/init-session.sh +1 -0
  245. package/templates/hooks/orbital-emit.sh +2 -2
  246. package/templates/hooks/orbital-report-deploy.sh +4 -4
  247. package/templates/hooks/orbital-report-gates.sh +4 -4
  248. package/templates/hooks/orbital-scope-update.sh +1 -1
  249. package/templates/hooks/scope-commit-logger.sh +2 -2
  250. package/templates/hooks/scope-create-cleanup.sh +2 -2
  251. package/templates/hooks/scope-create-gate.sh +2 -5
  252. package/templates/hooks/scope-gate.sh +4 -6
  253. package/templates/hooks/scope-helpers.sh +28 -1
  254. package/templates/hooks/scope-lifecycle-gate.sh +14 -5
  255. package/templates/hooks/scope-prepare.sh +67 -12
  256. package/templates/hooks/scope-transition.sh +14 -6
  257. package/templates/hooks/time-tracker.sh +2 -5
  258. package/templates/migrations/renames.json +1 -0
  259. package/templates/orbital.config.json +8 -6
  260. package/{shared/default-workflow.json → templates/presets/default.json} +65 -0
  261. package/templates/presets/development.json +4 -4
  262. package/templates/presets/gitflow.json +7 -0
  263. package/templates/prompts/README.md +23 -0
  264. package/templates/prompts/deep-dive-audit.md +94 -0
  265. package/templates/quick/rules.md +56 -5
  266. package/templates/settings-hooks.json +1 -1
  267. package/templates/skills/git-commit/SKILL.md +27 -7
  268. package/templates/skills/git-dev/SKILL.md +13 -4
  269. package/templates/skills/git-main/SKILL.md +13 -3
  270. package/templates/skills/git-production/SKILL.md +9 -2
  271. package/templates/skills/git-staging/SKILL.md +11 -3
  272. package/templates/skills/scope-create/SKILL.md +17 -3
  273. package/templates/skills/scope-fix-review/SKILL.md +14 -7
  274. package/templates/skills/scope-implement/SKILL.md +15 -4
  275. package/templates/skills/scope-post-review/SKILL.md +77 -7
  276. package/templates/skills/scope-pre-review/SKILL.md +11 -4
  277. package/templates/skills/scope-verify/SKILL.md +5 -3
  278. package/templates/skills/test-code-review/SKILL.md +41 -33
  279. package/templates/skills/test-scaffold/SKILL.md +222 -0
  280. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
  281. package/dist/assets/charts-D__PA1zp.js +0 -72
  282. package/dist/assets/index-D1G6i0nS.css +0 -1
  283. package/dist/assets/index-DpItvKpf.js +0 -419
  284. package/dist/assets/ui-BvF022GT.js +0 -53
  285. package/index.html +0 -15
  286. package/postcss.config.js +0 -6
  287. package/src/App.tsx +0 -33
  288. package/src/components/AgentBadge.tsx +0 -40
  289. package/src/components/BatchPreflightModal.tsx +0 -115
  290. package/src/components/CardDisplayToggle.tsx +0 -74
  291. package/src/components/ColumnHeaderActions.tsx +0 -55
  292. package/src/components/ColumnMenu.tsx +0 -99
  293. package/src/components/DeployHistory.tsx +0 -141
  294. package/src/components/DispatchModal.tsx +0 -164
  295. package/src/components/DispatchPopover.tsx +0 -139
  296. package/src/components/DragOverlay.tsx +0 -25
  297. package/src/components/DriftSidebar.tsx +0 -140
  298. package/src/components/EnvironmentStrip.tsx +0 -88
  299. package/src/components/ErrorBoundary.tsx +0 -62
  300. package/src/components/FilterChip.tsx +0 -105
  301. package/src/components/GateIndicator.tsx +0 -33
  302. package/src/components/IdeaDetailModal.tsx +0 -190
  303. package/src/components/IdeaFormDialog.tsx +0 -113
  304. package/src/components/KanbanColumn.tsx +0 -201
  305. package/src/components/MarkdownRenderer.tsx +0 -114
  306. package/src/components/NeonGrid.tsx +0 -128
  307. package/src/components/PromotionQueue.tsx +0 -89
  308. package/src/components/ScopeCard.tsx +0 -234
  309. package/src/components/ScopeDetailModal.tsx +0 -255
  310. package/src/components/ScopeFilterBar.tsx +0 -152
  311. package/src/components/SearchInput.tsx +0 -102
  312. package/src/components/SessionPanel.tsx +0 -335
  313. package/src/components/SprintContainer.tsx +0 -303
  314. package/src/components/SprintDependencyDialog.tsx +0 -78
  315. package/src/components/SprintPreflightModal.tsx +0 -138
  316. package/src/components/StatusBar.tsx +0 -168
  317. package/src/components/SwimCell.tsx +0 -67
  318. package/src/components/SwimLaneRow.tsx +0 -94
  319. package/src/components/SwimlaneBoardView.tsx +0 -108
  320. package/src/components/VersionBadge.tsx +0 -139
  321. package/src/components/ViewModeSelector.tsx +0 -114
  322. package/src/components/config/AgentChip.tsx +0 -53
  323. package/src/components/config/AgentCreateDialog.tsx +0 -321
  324. package/src/components/config/AgentEditor.tsx +0 -175
  325. package/src/components/config/DirectoryTree.tsx +0 -582
  326. package/src/components/config/FileEditor.tsx +0 -550
  327. package/src/components/config/HookChip.tsx +0 -50
  328. package/src/components/config/StageCard.tsx +0 -198
  329. package/src/components/config/TransitionZone.tsx +0 -173
  330. package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
  331. package/src/components/config/WorkflowPipeline.tsx +0 -161
  332. package/src/components/source-control/BranchList.tsx +0 -93
  333. package/src/components/source-control/BranchPanel.tsx +0 -105
  334. package/src/components/source-control/CommitLog.tsx +0 -100
  335. package/src/components/source-control/CommitRow.tsx +0 -47
  336. package/src/components/source-control/GitHubPanel.tsx +0 -110
  337. package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
  338. package/src/components/source-control/GitOverviewBar.tsx +0 -101
  339. package/src/components/source-control/PullRequestList.tsx +0 -69
  340. package/src/components/source-control/WorktreeList.tsx +0 -80
  341. package/src/components/ui/badge.tsx +0 -41
  342. package/src/components/ui/button.tsx +0 -55
  343. package/src/components/ui/card.tsx +0 -78
  344. package/src/components/ui/dialog.tsx +0 -94
  345. package/src/components/ui/popover.tsx +0 -33
  346. package/src/components/ui/scroll-area.tsx +0 -54
  347. package/src/components/ui/separator.tsx +0 -28
  348. package/src/components/ui/tabs.tsx +0 -52
  349. package/src/components/ui/toggle-switch.tsx +0 -35
  350. package/src/components/ui/tooltip.tsx +0 -27
  351. package/src/components/workflow/AddEdgeDialog.tsx +0 -217
  352. package/src/components/workflow/AddListDialog.tsx +0 -201
  353. package/src/components/workflow/ChecklistEditor.tsx +0 -239
  354. package/src/components/workflow/CommandPrefixManager.tsx +0 -118
  355. package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
  356. package/src/components/workflow/DirectionSelector.tsx +0 -133
  357. package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
  358. package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
  359. package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
  360. package/src/components/workflow/EditToolbar.tsx +0 -138
  361. package/src/components/workflow/HookDetailPanel.tsx +0 -250
  362. package/src/components/workflow/HookExecutionLog.tsx +0 -24
  363. package/src/components/workflow/HookSourceModal.tsx +0 -129
  364. package/src/components/workflow/HooksDashboard.tsx +0 -363
  365. package/src/components/workflow/ListPropertyEditor.tsx +0 -251
  366. package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
  367. package/src/components/workflow/MovementRulesPanel.tsx +0 -188
  368. package/src/components/workflow/NodeDetailPanel.tsx +0 -245
  369. package/src/components/workflow/PresetSelector.tsx +0 -414
  370. package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
  371. package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
  372. package/src/components/workflow/WorkflowNode.tsx +0 -147
  373. package/src/components/workflow/graphLayout.ts +0 -186
  374. package/src/components/workflow/mergeHooks.ts +0 -85
  375. package/src/components/workflow/useEditHistory.ts +0 -88
  376. package/src/components/workflow/useWorkflowEditor.ts +0 -262
  377. package/src/components/workflow/validateConfig.ts +0 -70
  378. package/src/hooks/useActiveDispatches.ts +0 -198
  379. package/src/hooks/useBoardSettings.ts +0 -170
  380. package/src/hooks/useCardDisplay.ts +0 -57
  381. package/src/hooks/useCcHooks.ts +0 -24
  382. package/src/hooks/useConfigTree.ts +0 -51
  383. package/src/hooks/useEnforcementRules.ts +0 -46
  384. package/src/hooks/useEvents.ts +0 -59
  385. package/src/hooks/useFileEditor.ts +0 -165
  386. package/src/hooks/useGates.ts +0 -57
  387. package/src/hooks/useIdeaActions.ts +0 -53
  388. package/src/hooks/useKanbanDnd.ts +0 -410
  389. package/src/hooks/useOrbitalConfig.ts +0 -54
  390. package/src/hooks/usePipeline.ts +0 -47
  391. package/src/hooks/usePipelineData.ts +0 -338
  392. package/src/hooks/useReconnect.ts +0 -25
  393. package/src/hooks/useScopeFilters.ts +0 -125
  394. package/src/hooks/useScopeSessions.ts +0 -44
  395. package/src/hooks/useScopes.ts +0 -67
  396. package/src/hooks/useSearch.ts +0 -67
  397. package/src/hooks/useSettings.tsx +0 -187
  398. package/src/hooks/useSocket.ts +0 -25
  399. package/src/hooks/useSourceControl.ts +0 -105
  400. package/src/hooks/useSprintPreflight.ts +0 -55
  401. package/src/hooks/useSprints.ts +0 -154
  402. package/src/hooks/useStatusBarHighlight.ts +0 -18
  403. package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
  404. package/src/hooks/useTheme.ts +0 -9
  405. package/src/hooks/useTransitionReadiness.ts +0 -53
  406. package/src/hooks/useVersion.ts +0 -155
  407. package/src/hooks/useViolations.ts +0 -65
  408. package/src/hooks/useWorkflow.tsx +0 -125
  409. package/src/hooks/useZoomModifier.ts +0 -19
  410. package/src/index.css +0 -797
  411. package/src/layouts/DashboardLayout.tsx +0 -113
  412. package/src/lib/collisionDetection.ts +0 -20
  413. package/src/lib/scope-fields.ts +0 -61
  414. package/src/lib/swimlane.ts +0 -146
  415. package/src/lib/utils.ts +0 -15
  416. package/src/main.tsx +0 -19
  417. package/src/socket.ts +0 -11
  418. package/src/types/index.ts +0 -497
  419. package/src/views/AgentFeed.tsx +0 -339
  420. package/src/views/DeployPipeline.tsx +0 -59
  421. package/src/views/EnforcementView.tsx +0 -378
  422. package/src/views/PrimitivesConfig.tsx +0 -500
  423. package/src/views/QualityGates.tsx +0 -1012
  424. package/src/views/ScopeBoard.tsx +0 -454
  425. package/src/views/SessionTimeline.tsx +0 -516
  426. package/src/views/Settings.tsx +0 -183
  427. package/src/views/SourceControl.tsx +0 -95
  428. package/src/views/WorkflowVisualizer.tsx +0 -382
  429. package/tailwind.config.js +0 -161
  430. package/tsconfig.json +0 -25
  431. package/vite.config.ts +0 -38
@@ -1,9 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import matter from 'gray-matter';
4
- import type { Server } from 'socket.io';
4
+ import type { Emitter } from '../project-emitter.js';
5
5
  import type { ParsedScope } from '../parsers/scope-parser.js';
6
- import { normalizeStatus, parseAllScopes, parseScopeFile, setValidStatuses } from '../parsers/scope-parser.js';
6
+ import { parseAllScopes, parseScopeFile, setValidStatuses } from '../parsers/scope-parser.js';
7
7
  import type { WorkflowEngine } from '../../shared/workflow-engine.js';
8
8
  import type { TransitionContext, TransitionResult } from '../../shared/workflow-config.js';
9
9
  import type { ScopeCache } from './scope-cache.js';
@@ -20,7 +20,7 @@ export class ScopeService {
20
20
 
21
21
  constructor(
22
22
  private cache: ScopeCache,
23
- private io: Server,
23
+ private io: Emitter,
24
24
  private scopesDir: string,
25
25
  private engine: WorkflowEngine,
26
26
  ) {}
@@ -46,6 +46,42 @@ export class ScopeService {
46
46
  return scopes.length;
47
47
  }
48
48
 
49
+ /** Reconcile files whose directory doesn't match their frontmatter status.
50
+ * Frontmatter is the authoritative source — files are moved to match it.
51
+ * Called once at startup after syncFromFilesystem(). */
52
+ reconcileDirectories(): number {
53
+ let moved = 0;
54
+ for (const scope of this.cache.getAll()) {
55
+ if (scope.id < 0) continue; // slug-only icebox items (negative IDs)
56
+ const currentDir = path.basename(path.dirname(scope.file_path));
57
+ if (currentDir === scope.status) continue;
58
+ if (!this.engine.isValidStatus(scope.status)) continue;
59
+
60
+ const targetDir = path.join(this.scopesDir, scope.status);
61
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
62
+ const newPath = path.join(targetDir, path.basename(scope.file_path));
63
+
64
+ this.suppressedPaths.add(scope.file_path);
65
+ this.suppressedPaths.add(newPath);
66
+ try {
67
+ fs.renameSync(scope.file_path, newPath);
68
+ this.updateFromFile(newPath);
69
+ this.removeByFilePath(scope.file_path);
70
+ moved++;
71
+ log.warn('Reconciled directory mismatch', {
72
+ id: scope.id, frontmatter: scope.status, directory: currentDir,
73
+ });
74
+ } catch (err) {
75
+ log.error('Failed to reconcile scope directory', { id: scope.id, error: String(err) });
76
+ }
77
+ setTimeout(() => {
78
+ this.suppressedPaths.delete(scope.file_path);
79
+ this.suppressedPaths.delete(newPath);
80
+ }, 2000);
81
+ }
82
+ return moved;
83
+ }
84
+
49
85
  /** Check if a path is suppressed from watcher processing (during programmatic moves) */
50
86
  isSuppressed(filePath: string): boolean {
51
87
  return this.suppressedPaths.has(filePath);
@@ -84,7 +120,7 @@ export class ScopeService {
84
120
  const id = this.cache.removeByFilePath(filePath);
85
121
  if (id !== undefined) {
86
122
  if (previous) this.recentlyRemoved.set(id, previous.status);
87
- this.io.emit('scope:deleted', id);
123
+ this.io.emit('scope:deleted', { id });
88
124
  // Clean up stash after a short window (if add never fires, this was a real delete)
89
125
  setTimeout(() => this.recentlyRemoved.delete(id), 5000);
90
126
  }
@@ -102,6 +138,7 @@ export class ScopeService {
102
138
 
103
139
  /** Update a scope's status with transition validation.
104
140
  * Writes the new status to the frontmatter file and updates the cache.
141
+ * This is the SINGLE validation point — all status changes must flow through here.
105
142
  * @param context - caller trust level: 'patch', 'dispatch', 'event', 'bulk-sync', 'rollback' */
106
143
  updateStatus(
107
144
  id: number,
@@ -131,12 +168,11 @@ export class ScopeService {
131
168
  if (!check.ok) return check;
132
169
  }
133
170
 
134
- // Write to filesystem via updateScopeFrontmatter (which updates cache + emits)
135
- const current = context === 'bulk-sync' || context === 'rollback'
136
- ? this.cache.getById(id)
137
- : this.cache.getById(id); // already fetched above for validation, but may be null in bulk-sync
171
+ // Fetch current scope for fromStatus logging. In bulk-sync/rollback contexts
172
+ // the validation block above is skipped, so this may be the first lookup.
173
+ const current = this.cache.getById(id);
138
174
  const fromStatus = current?.status ?? 'unknown';
139
- const result = this.updateScopeFrontmatter(id, { status }, context);
175
+ const result = this._writeFrontmatter(id, { status });
140
176
  if (result.ok) {
141
177
  log.info('Status updated', { id, from: fromStatus, to: status, context });
142
178
  for (const cb of this.onStatusChangeCallbacks) cb(id, status);
@@ -144,8 +180,36 @@ export class ScopeService {
144
180
  return result;
145
181
  }
146
182
 
183
+ /** Update scope fields via a public API (e.g. PATCH route).
184
+ * Status changes are routed through updateStatus for validation.
185
+ * Non-status fields are written directly via _writeFrontmatter. */
186
+ updateFields(
187
+ id: number,
188
+ fields: Record<string, unknown>,
189
+ ): TransitionResult & { moved?: boolean } {
190
+ const { status, ...nonStatusFields } = fields as { status?: string; [k: string]: unknown };
191
+
192
+ // Status changes go through updateStatus (validates transition, fires callbacks)
193
+ if (status) {
194
+ const current = this.cache.getById(id);
195
+ if (!current) return { ok: false, error: 'Scope not found', code: 'NOT_FOUND' };
196
+ if (status !== current.status) {
197
+ const result = this.updateStatus(id, status);
198
+ if (!result.ok) return result;
199
+ }
200
+ }
201
+
202
+ // Non-status field updates written directly
203
+ if (Object.keys(nonStatusFields).length > 0) {
204
+ return this._writeFrontmatter(id, nonStatusFields);
205
+ }
206
+
207
+ return { ok: true, moved: !!status };
208
+ }
209
+
147
210
  /** Compute the next sequential scope ID by scanning all non-icebox scopes.
148
- * Checks both filesystem (all subdirs except icebox) and cache to prevent collisions. */
211
+ * Checks both filesystem (all subdirs except icebox) and cache to prevent collisions.
212
+ * Skips IDs >= 500 to handle legacy icebox-origin files during migration. */
149
213
  private getNextScopeId(): number {
150
214
  let maxId = 0;
151
215
 
@@ -156,7 +220,11 @@ export class ScopeService {
156
220
  const dirPath = path.join(this.scopesDir, dir.name);
157
221
  for (const file of fs.readdirSync(dirPath)) {
158
222
  const m = file.match(/^(\d+)-/);
159
- if (m) maxId = Math.max(maxId, parseInt(m[1], 10));
223
+ if (!m) continue;
224
+ const id = parseInt(m[1], 10);
225
+ // Skip legacy icebox-origin IDs (500+) to prevent namespace pollution
226
+ if (id >= 500) continue;
227
+ maxId = Math.max(maxId, id);
160
228
  }
161
229
  }
162
230
  }
@@ -170,50 +238,62 @@ export class ScopeService {
170
238
 
171
239
  // ─── Idea CRUD (filesystem-backed icebox cards) ────────────
172
240
 
173
- /** Get the next available icebox ID (starts at 501, increments from max found) */
174
- getNextIceboxId(): number {
175
- const iceboxDir = path.join(this.scopesDir, 'icebox');
176
- if (!fs.existsSync(iceboxDir)) return 501;
177
- let maxId = 500;
178
- for (const file of fs.readdirSync(iceboxDir)) {
179
- const m = file.match(/^(\d+)-/);
180
- if (m) maxId = Math.max(maxId, parseInt(m[1], 10));
241
+ /** Normalize Date objects in gray-matter frontmatter to YYYY-MM-DD strings */
242
+ private normalizeFrontmatterDates(data: Record<string, unknown>): void {
243
+ for (const key of Object.keys(data)) {
244
+ if (data[key] instanceof Date) {
245
+ data[key] = (data[key] as Date).toISOString().split('T')[0];
246
+ }
181
247
  }
182
- return maxId + 1;
183
248
  }
184
249
 
185
- /** Find an icebox file by its ID prefix.
186
- * Matches both padded (091-) and unpadded (91-) filenames
187
- * since demoted scopes keep their 3-digit-padded names. */
188
- private findIdeaFile(iceboxDir: string, id: number): string | null {
250
+ /** Generate a slug from a title */
251
+ private slugify(title: string): string {
252
+ const slug = title
253
+ .toLowerCase()
254
+ .replace(/[^a-z0-9]+/g, '-')
255
+ .replace(/^-|-$/g, '')
256
+ .slice(0, 60);
257
+ if (!slug) return 'untitled';
258
+ return slug;
259
+ }
260
+
261
+ /** Find an icebox file by its slug.
262
+ * Matches slug-only files ({slug}.md) and legacy numeric-prefixed files ({NNN}-{slug}.md). */
263
+ private findIdeaFile(iceboxDir: string, slug: string): string | null {
189
264
  if (!fs.existsSync(iceboxDir)) return null;
190
265
  const match = fs.readdirSync(iceboxDir).find((f) => {
191
266
  if (!f.endsWith('.md')) return false;
192
- const m = f.match(/^(\d+)-/);
193
- return m != null && parseInt(m[1], 10) === id;
267
+ // Match slug-only: {slug}.md
268
+ if (f === `${slug}.md`) return true;
269
+ // Match legacy numeric-prefixed: {NNN}-{slug}.md
270
+ return f.match(/^\d+-/) && f.slice(f.indexOf('-') + 1) === `${slug}.md`;
194
271
  });
195
272
  return match ? path.join(iceboxDir, match) : null;
196
273
  }
197
274
 
198
- /** Create an icebox idea as a markdown file. IDs start at 501. */
199
- createIdeaFile(title: string, description: string): { id: number; title: string } {
275
+ /** Create an icebox idea as a slug-only markdown file. */
276
+ createIdeaFile(title: string, description: string): { slug: string; title: string } {
200
277
  const iceboxDir = path.join(this.scopesDir, 'icebox');
201
278
  if (!fs.existsSync(iceboxDir)) fs.mkdirSync(iceboxDir, { recursive: true });
202
279
 
203
- const nextId = this.getNextIceboxId();
280
+ const slug = this.slugify(title);
281
+ let fileName = `${slug}.md`;
282
+ let filePath = path.join(iceboxDir, fileName);
204
283
 
205
- const slug = title
206
- .toLowerCase()
207
- .replace(/[^a-z0-9]+/g, '-')
208
- .replace(/^-|-$/g, '')
209
- .slice(0, 60);
210
- const fileName = `${nextId}-${slug}.md`;
211
- const filePath = path.join(iceboxDir, fileName);
284
+ // Handle slug collisions by appending -2, -3, etc.
285
+ if (fs.existsSync(filePath)) {
286
+ let suffix = 2;
287
+ while (fs.existsSync(path.join(iceboxDir, `${slug}-${suffix}.md`))) suffix++;
288
+ fileName = `${slug}-${suffix}.md`;
289
+ filePath = path.join(iceboxDir, fileName);
290
+ }
291
+
292
+ const finalSlug = fileName.replace(/\.md$/, '');
212
293
  const now = new Date().toISOString().split('T')[0];
213
294
 
214
295
  const content = [
215
296
  '---',
216
- `id: ${nextId}`,
217
297
  `title: "${title.replace(/"/g, '\\"')}"`,
218
298
  'status: icebox',
219
299
  `created: ${now}`,
@@ -231,116 +311,134 @@ export class ScopeService {
231
311
 
232
312
  // Eagerly sync to cache + emit scope:created
233
313
  this.updateFromFile(filePath);
234
- log.info('Idea created', { id: nextId, title });
235
- return { id: nextId, title };
314
+ log.info('Idea created', { slug: finalSlug, title });
315
+ return { slug: finalSlug, title };
236
316
  }
237
317
 
238
- /** Update an icebox idea's title and description by rewriting its file */
239
- updateIdeaFile(id: number, title: string, description: string): boolean {
318
+ /** Update an icebox idea's title and description in-place. Renames the file if the title slug changes. */
319
+ updateIdeaFile(slug: string, title: string, description: string): boolean {
240
320
  const iceboxDir = path.join(this.scopesDir, 'icebox');
241
- const filePath = this.findIdeaFile(iceboxDir, id);
321
+ const filePath = this.findIdeaFile(iceboxDir, slug);
242
322
  if (!filePath) return false;
243
323
 
244
324
  // Preserve the original created date from existing frontmatter
245
325
  const existing = fs.readFileSync(filePath, 'utf-8');
246
- const createdMatch = existing.match(/^created:\s*(.+)$/m);
247
- const created = createdMatch?.[1]?.trim() ?? new Date().toISOString().split('T')[0];
326
+ const parsed = matter(existing);
327
+ const created = parsed.data.created ? String(parsed.data.created) : new Date().toISOString().split('T')[0];
248
328
  const now = new Date().toISOString().split('T')[0];
249
329
 
250
- const content = [
251
- '---',
252
- `id: ${id}`,
253
- `title: "${title.replace(/"/g, '\\"')}"`,
254
- 'status: icebox',
255
- `created: ${created}`,
256
- `updated: ${now}`,
257
- 'blocked_by: []',
258
- 'blocks: []',
259
- 'tags: []',
260
- '---',
261
- '',
262
- description || '',
263
- '',
264
- ].join('\n');
330
+ // Update frontmatter fields while preserving other data (like ghost)
331
+ parsed.data.title = title;
332
+ parsed.data.updated = now;
333
+ parsed.data.created = created;
334
+ this.normalizeFrontmatterDates(parsed.data);
335
+
336
+ const newContent = matter.stringify(description ? `\n${description}\n` : '\n', parsed.data);
337
+ fs.writeFileSync(filePath, newContent, 'utf-8');
338
+
339
+ // If title changed, rename file to new slug
340
+ const newSlug = this.slugify(title);
341
+ if (newSlug !== slug) {
342
+ const newFileName = `${newSlug}.md`;
343
+ const newPath = path.join(iceboxDir, newFileName);
344
+ if (!fs.existsSync(newPath)) {
345
+ this.suppressedPaths.add(filePath);
346
+ this.suppressedPaths.add(newPath);
347
+ this.removeByFilePath(filePath);
348
+ fs.renameSync(filePath, newPath);
349
+ this.updateFromFile(newPath);
350
+ setTimeout(() => {
351
+ this.suppressedPaths.delete(filePath);
352
+ this.suppressedPaths.delete(newPath);
353
+ }, 2000);
354
+ } else {
355
+ // Collision with existing slug — keep old filename, still sync content changes
356
+ log.warn('Slug collision during rename, keeping old filename', { slug, newSlug });
357
+ this.updateFromFile(filePath);
358
+ }
359
+ } else {
360
+ // Eagerly sync content changes to cache
361
+ this.updateFromFile(filePath);
362
+ }
265
363
 
266
- fs.writeFileSync(filePath, content, 'utf-8');
267
- // Watcher handles cache sync + scope:updated event
268
364
  return true;
269
365
  }
270
366
 
271
367
  /** Delete an icebox idea by removing its file */
272
- deleteIdeaFile(id: number): boolean {
368
+ deleteIdeaFile(slug: string): boolean {
273
369
  const iceboxDir = path.join(this.scopesDir, 'icebox');
274
- const filePath = this.findIdeaFile(iceboxDir, id);
370
+ const filePath = this.findIdeaFile(iceboxDir, slug);
275
371
  if (!filePath) return false;
276
372
 
277
373
  fs.unlinkSync(filePath);
278
374
  // Eagerly remove from cache + emit scope:deleted
279
375
  this.removeByFilePath(filePath);
280
- log.info('Idea deleted', { id });
376
+ log.info('Idea deleted', { slug });
281
377
  return true;
282
378
  }
283
379
 
284
380
  /** Promote an icebox idea to planning — assigns a proper sequential scope ID,
285
381
  * moves the file, and syncs cache. Returns the new scope ID. */
286
- promoteIdea(id: number): { id: number; filePath: string; title: string; description: string } | null {
382
+ promoteIdea(slug: string, targetStatus = 'planning'): { id: number; filePath: string; title: string; description: string } | null {
287
383
  const iceboxDir = path.join(this.scopesDir, 'icebox');
288
- const oldPath = this.findIdeaFile(iceboxDir, id);
384
+ const oldPath = this.findIdeaFile(iceboxDir, slug);
289
385
  if (!oldPath) return null;
290
386
 
291
387
  // Read existing file for metadata
292
- const content = fs.readFileSync(oldPath, 'utf-8');
293
- const titleMatch = content.match(/^title:\s*"?([^"\n]+)"?\s*$/m);
294
- const createdMatch = content.match(/^created:\s*(.+)$/m);
295
- const title = titleMatch?.[1]?.trim() ?? 'Untitled';
296
- const created = createdMatch?.[1]?.trim() ?? new Date().toISOString().split('T')[0];
297
-
298
- // Extract body after frontmatter
299
- const fmEnd = content.indexOf('---', content.indexOf('---') + 3);
300
- const description = fmEnd !== -1 ? content.slice(fmEnd + 3).trim() : '';
388
+ const raw = fs.readFileSync(oldPath, 'utf-8');
389
+ const parsed = matter(raw);
390
+ const title = parsed.data.title ? String(parsed.data.title) : 'Untitled';
391
+ const created = parsed.data.created ? String(parsed.data.created) : new Date().toISOString().split('T')[0];
392
+ const description = parsed.content.trim();
301
393
 
302
394
  // Assign the next sequential scope ID (excludes icebox items)
303
395
  const newId = this.getNextScopeId();
304
396
  const paddedId = String(newId).padStart(3, '0');
305
397
 
306
- // Build slug and new path
307
- const slug = title
308
- .toLowerCase()
309
- .replace(/[^a-z0-9]+/g, '-')
310
- .replace(/^-|-$/g, '')
311
- .slice(0, 60);
312
- const planningDir = path.join(this.scopesDir, 'planning');
313
- if (!fs.existsSync(planningDir)) fs.mkdirSync(planningDir, { recursive: true });
314
- const newFileName = `${paddedId}-${slug}.md`;
315
- const newPath = path.join(planningDir, newFileName);
398
+ // Build new path
399
+ const titleSlug = this.slugify(title);
400
+ const targetDir = path.join(this.scopesDir, targetStatus);
401
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
402
+ const newFileName = `${paddedId}-${titleSlug}.md`;
403
+ const newPath = path.join(targetDir, newFileName);
316
404
  const now = new Date().toISOString().split('T')[0];
317
405
 
318
- // Write new file with planning status and new sequential ID
319
- const newContent = [
320
- '---',
321
- `id: ${paddedId}`,
322
- `title: "${title.replace(/"/g, '\\"')}"`,
323
- 'status: planning',
324
- `created: ${created}`,
325
- `updated: ${now}`,
326
- 'blocked_by: []',
327
- 'blocks: []',
328
- 'tags: []',
329
- '---',
330
- '',
331
- description || '',
332
- '',
333
- ].join('\n');
406
+ // Update frontmatter in-place: assign ID and change status (preserve other fields)
407
+ parsed.data.id = newId;
408
+ parsed.data.status = targetStatus;
409
+ parsed.data.updated = now;
410
+ parsed.data.created = created;
411
+ delete parsed.data.ghost;
412
+ this.normalizeFrontmatterDates(parsed.data);
334
413
 
335
- fs.writeFileSync(newPath, newContent, 'utf-8');
414
+ const newContent = matter.stringify(parsed.content, parsed.data);
415
+
416
+ // Write updated content to old path, then rename/move (no intermediate missing state)
417
+ const originalContent = fs.readFileSync(oldPath, 'utf-8');
418
+ fs.writeFileSync(oldPath, newContent, 'utf-8');
336
419
 
337
- // Sync cache before deleting old file (avoids window where scope is missing)
420
+ // Suppress watcher events during programmatic move
421
+ this.suppressedPaths.add(oldPath);
422
+ this.suppressedPaths.add(newPath);
423
+ try {
424
+ fs.renameSync(oldPath, newPath);
425
+ } catch (err) {
426
+ // Restore original content on rename failure
427
+ fs.writeFileSync(oldPath, originalContent, 'utf-8');
428
+ this.suppressedPaths.delete(oldPath);
429
+ this.suppressedPaths.delete(newPath);
430
+ log.error('Failed to rename during promote', { oldPath, newPath, error: String(err) });
431
+ return null;
432
+ }
338
433
  this.updateFromFile(newPath);
339
- fs.unlinkSync(oldPath);
340
434
  this.removeByFilePath(oldPath);
435
+ setTimeout(() => {
436
+ this.suppressedPaths.delete(oldPath);
437
+ this.suppressedPaths.delete(newPath);
438
+ }, 2000);
341
439
 
342
440
  const relPath = path.relative(path.resolve(this.scopesDir, '..'), newPath);
343
- log.info('Idea promoted', { oldId: id, newId, title });
441
+ log.info('Idea promoted', { slug, newId, title });
344
442
  return { id: newId, filePath: relPath, title, description };
345
443
  }
346
444
 
@@ -362,13 +460,13 @@ export class ScopeService {
362
460
  return null;
363
461
  }
364
462
 
365
- /** Update a scope's frontmatter fields and write back to the .md file.
366
- * If status changes, validates the transition and moves the file to the new status directory.
367
- * @param context - transition context for validation (default 'patch') */
368
- updateScopeFrontmatter(
463
+ /** Write frontmatter fields to a scope's .md file.
464
+ * If the effective status differs from the current directory, moves the file.
465
+ * This is a trusted write operation callers must validate transitions
466
+ * via updateStatus() before calling this method with status changes. */
467
+ private _writeFrontmatter(
369
468
  id: number,
370
469
  fields: Record<string, unknown>,
371
- context: TransitionContext = 'patch',
372
470
  ): TransitionResult & { moved?: boolean } {
373
471
  const filePath = this.findScopeFile(id);
374
472
  if (!filePath) {
@@ -379,25 +477,21 @@ export class ScopeService {
379
477
  const parsed = matter(raw);
380
478
  const today = new Date().toISOString().split('T')[0];
381
479
 
382
- // Validate status transition before any writes
480
+ // Determine if the file needs to move to a different directory.
481
+ // Compare against the DIRECTORY name (not frontmatter status) since the question
482
+ // is whether the physical file location matches the desired status.
383
483
  const newStatus = fields.status as string | undefined;
384
- const rawOldStatus = String(parsed.data.status ?? 'planning');
385
- const oldStatus = normalizeStatus(rawOldStatus);
386
- let needsMove = false;
484
+ const dirName = path.basename(path.dirname(filePath));
485
+ const effectiveStatus = newStatus ?? String(parsed.data.status ?? dirName);
486
+ const needsMove = effectiveStatus !== dirName && this.engine.isValidStatus(effectiveStatus);
387
487
 
388
- if (newStatus && newStatus !== oldStatus) {
389
- if (!this.engine.isValidStatus(newStatus)) {
390
- return { ok: false, error: `Invalid status: '${newStatus}'`, code: 'INVALID_STATUS' };
391
- }
392
- const check = this.engine.validateTransition(oldStatus, newStatus, context);
393
- if (!check.ok) return check;
394
- needsMove = true;
395
- // Auto-unlock spec when reverting backlog → planning
396
- if (newStatus === 'planning' && oldStatus === 'backlog') fields.spec_locked = false;
488
+ // Auto-unlock spec when reverting backlog → planning
489
+ if (newStatus === 'planning' && dirName === 'backlog') {
490
+ fields = { ...fields, spec_locked: false };
397
491
  }
398
492
 
399
493
  // Merge editable fields into frontmatter
400
- const editableKeys = ['title', 'status', 'priority', 'effort_estimate', 'category', 'tags', 'blocked_by', 'blocks', 'spec_locked'];
494
+ const editableKeys = ['title', 'status', 'priority', 'effort_estimate', 'category', 'tags', 'blocked_by', 'blocks', 'spec_locked', 'favourite'];
401
495
  for (const key of editableKeys) {
402
496
  if (key in fields) {
403
497
  const val = fields[key];
@@ -429,8 +523,8 @@ export class ScopeService {
429
523
  return { ok: true };
430
524
  }
431
525
 
432
- // Status change → move file to new directory
433
- const targetDir = path.join(this.scopesDir, newStatus!);
526
+ // Status differs from directory → move file to correct directory
527
+ const targetDir = path.join(this.scopesDir, effectiveStatus);
434
528
  if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
435
529
 
436
530
  const fileName = path.basename(filePath);
@@ -451,15 +545,15 @@ export class ScopeService {
451
545
  setTimeout(() => {
452
546
  this.suppressedPaths.delete(filePath);
453
547
  this.suppressedPaths.delete(newPath);
454
- }, 500);
548
+ }, 2000);
455
549
 
456
550
  return { ok: true, moved: true };
457
551
  }
458
552
 
459
553
  /** Approve a ghost idea — removes ghost:true from frontmatter and refreshes cache */
460
- approveGhostIdea(id: number): boolean {
554
+ approveGhostIdea(slug: string): boolean {
461
555
  const iceboxDir = path.join(this.scopesDir, 'icebox');
462
- const filePath = this.findIdeaFile(iceboxDir, id);
556
+ const filePath = this.findIdeaFile(iceboxDir, slug);
463
557
  if (!filePath) return false;
464
558
 
465
559
  const content = fs.readFileSync(filePath, 'utf-8');
@@ -469,7 +563,7 @@ export class ScopeService {
469
563
 
470
564
  // Re-parse file to refresh cache with is_ghost=false
471
565
  this.updateFromFile(filePath);
472
- log.info('Ghost approved', { id });
566
+ log.info('Ghost approved', { slug });
473
567
 
474
568
  return true;
475
569
  }
@@ -1,11 +1,12 @@
1
1
  import type Database from 'better-sqlite3';
2
- import type { Server } from 'socket.io';
2
+ import type { Emitter } from '../project-emitter.js';
3
3
  import { SprintService } from './sprint-service.js';
4
4
  import { ScopeService } from './scope-service.js';
5
- import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
5
+ import { launchInCategorizedTerminal, escapeForAnsiC, shellQuote, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
6
6
  import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
7
+ import { buildClaudeFlags, buildEnvVarPrefix } from '../utils/flag-builder.js';
7
8
  import type { WorkflowEngine } from '../../shared/workflow-engine.js';
8
- import { getConfig } from '../config.js';
9
+ import type { OrbitalConfig } from '../config.js';
9
10
  import { createLogger } from '../utils/logger.js';
10
11
 
11
12
  const log = createLogger('sprint');
@@ -20,10 +21,12 @@ function sleep(ms: number): Promise<void> {
20
21
  export class SprintOrchestrator {
21
22
  constructor(
22
23
  private db: Database.Database,
23
- private io: Server,
24
+ private io: Emitter,
24
25
  private sprintService: SprintService,
25
26
  private scopeService: ScopeService,
26
27
  private engine: WorkflowEngine,
28
+ private projectRoot: string,
29
+ private config: OrbitalConfig,
27
30
  ) {}
28
31
 
29
32
  /** Build execution layers using Kahn's topological sort */
@@ -112,14 +115,16 @@ export class SprintOrchestrator {
112
115
  async onScopeReachedDev(scopeId: number): Promise<void> {
113
116
  const match = this.sprintService.findActiveSprintForScope(scopeId);
114
117
  if (!match) return;
115
- log.debug('Scope reached dev', { scopeId, sprintId: match.sprint_id });
116
118
 
119
+ // Batches are managed by BatchOrchestrator — don't dispatch individual scopes
117
120
  const sprintId = match.sprint_id;
121
+ const sprint = this.sprintService.getById(sprintId);
122
+ if (!sprint || sprint.group_type === 'batch') return;
123
+
124
+ log.debug('Scope reached dev', { scopeId, sprintId });
118
125
  this.sprintService.updateScopeStatus(sprintId, scopeId, 'completed');
119
126
 
120
127
  // Ensure sprint is in 'in_progress' state
121
- const sprint = this.sprintService.getById(sprintId);
122
- if (!sprint) return;
123
128
  if (sprint.status === 'dispatched') {
124
129
  this.sprintService.updateStatus(sprintId, 'in_progress');
125
130
  }
@@ -196,7 +201,7 @@ export class SprintOrchestrator {
196
201
  const sprint = this.sprintService.getById(sprintId);
197
202
  if (!sprint) return null;
198
203
 
199
- const layers = sprint.layers ?? [];
204
+ const layers = sprint.layers ?? this.buildExecutionLayers(sprint.scope_ids).layers;
200
205
  const sprintSet = new Set(sprint.scope_ids);
201
206
  const edges: Array<{ from: number; to: number }> = [];
202
207
 
@@ -225,9 +230,15 @@ export class SprintOrchestrator {
225
230
  const currentScope = this.scopeService.getById(scopeId);
226
231
  const previousStatus = currentScope?.status ?? 'implementing';
227
232
 
233
+ // Resolve command and target status from workflow engine
234
+ const sprint = this.sprintService.getById(sprintId);
235
+ const targetColumn = sprint?.target_column ?? 'backlog';
236
+ const edgeCommand = this.engine.getBatchCommand(targetColumn);
237
+ const targetStatus = this.engine.getBatchTargetStatus(targetColumn);
238
+
228
239
  // Record DISPATCH event
229
240
  const eventId = crypto.randomUUID();
230
- const command = `/scope implement ${scopeId}`;
241
+ const command = edgeCommand ?? `/scope-implement ${scopeId}`;
231
242
  this.db.prepare(
232
243
  `INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
233
244
  VALUES (?, 'DISPATCH', ?, NULL, 'sprint-orchestrator', ?, ?)`,
@@ -241,26 +252,30 @@ export class SprintOrchestrator {
241
252
  });
242
253
 
243
254
  // Update scope + sprint_scope status
244
- this.scopeService.updateStatus(scopeId, 'implementing', 'dispatch');
255
+ if (targetStatus) {
256
+ this.scopeService.updateStatus(scopeId, targetStatus, 'dispatch');
257
+ }
245
258
  this.sprintService.updateScopeStatus(sprintId, scopeId, 'dispatched');
246
259
 
247
260
  // Build scope-aware session name and snapshot PIDs
248
261
  const scopeRow = this.scopeService.getById(scopeId);
249
262
  const sessionName = buildSessionName({ scopeId, title: scopeRow?.title, command });
250
- const beforePids = snapshotSessionPids(getConfig().projectRoot);
263
+ const beforePids = snapshotSessionPids(this.projectRoot);
251
264
 
252
- // Launch in iTerm — interactive TUI mode (no -p) for full visibility
265
+ // Launch in iTerm — interactive TUI mode for full visibility
253
266
  const escaped = escapeForAnsiC(command);
254
- const fullCmd = `cd '${getConfig().projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
267
+ const flagsStr = buildClaudeFlags(this.config.claude.dispatchFlags);
268
+ const envPrefix = buildEnvVarPrefix(this.config.dispatch.envVars);
269
+ const fullCmd = `cd '${shellQuote(this.projectRoot)}' && ${envPrefix}ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude ${flagsStr} $'${escaped}'`;
255
270
  try {
256
271
  await launchInCategorizedTerminal(command, fullCmd, sessionName);
257
272
 
258
273
  // Fire-and-forget: discover session PID, link to dispatch, and rename
259
- discoverNewSession(getConfig().projectRoot, beforePids)
274
+ discoverNewSession(this.projectRoot, beforePids)
260
275
  .then((session) => {
261
276
  if (!session) return;
262
277
  linkPidToDispatch(this.db, eventId, session.pid);
263
- if (sessionName) renameSession(getConfig().projectRoot, session.sessionId, sessionName);
278
+ if (sessionName) renameSession(this.projectRoot, session.sessionId, sessionName);
264
279
  })
265
280
  .catch(err => log.error('PID discovery failed', { error: err.message }));
266
281
  } catch (err) {