orbital-command 0.2.0 → 0.3.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 (380) hide show
  1. package/bin/orbital.js +640 -37
  2. package/dist/assets/PrimitivesConfig-CrmQXYh4.js +32 -0
  3. package/dist/assets/QualityGates-BbasOsF3.js +21 -0
  4. package/dist/assets/SessionTimeline-CGeJsVvy.js +1 -0
  5. package/dist/assets/Settings-oiM496mc.js +12 -0
  6. package/dist/assets/SourceControl-B1fP2nJL.js +41 -0
  7. package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +74 -0
  8. package/dist/assets/arrow-down-CPy85_J6.js +6 -0
  9. package/dist/assets/charts-DbDg0Psc.js +68 -0
  10. package/dist/assets/circle-x-Cwz6ZQDV.js +6 -0
  11. package/dist/assets/file-text-C46Xr65c.js +6 -0
  12. package/dist/assets/formatDistanceToNow-BMqsSP44.js +1 -0
  13. package/dist/assets/globe-Cn2yNZUD.js +6 -0
  14. package/dist/assets/index-Aj4sV8Al.css +1 -0
  15. package/dist/assets/index-Bc9dK3MW.js +354 -0
  16. package/dist/assets/key-OPaNTWJ5.js +6 -0
  17. package/dist/assets/minus-GMsbpKym.js +6 -0
  18. package/dist/assets/shield-DwAFkDYI.js +6 -0
  19. package/dist/assets/ui-BmsSg9jU.js +53 -0
  20. package/dist/assets/useWorkflowEditor-BJkTX_NR.js +16 -0
  21. package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
  22. package/dist/assets/zap-DfbUoOty.js +11 -0
  23. package/dist/favicon.svg +1 -0
  24. package/dist/index.html +6 -5
  25. package/dist/server/server/__tests__/data-routes.test.js +124 -0
  26. package/dist/server/server/__tests__/helpers/db.js +17 -0
  27. package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
  28. package/dist/server/server/__tests__/scope-routes.test.js +137 -0
  29. package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
  30. package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
  31. package/dist/server/server/config-migrator.js +138 -0
  32. package/dist/server/server/config.js +17 -2
  33. package/dist/server/server/database.js +27 -12
  34. package/dist/server/server/global-config.js +143 -0
  35. package/dist/server/server/index.js +882 -252
  36. package/dist/server/server/init.js +579 -194
  37. package/dist/server/server/launch.js +29 -0
  38. package/dist/server/server/manifest-types.js +8 -0
  39. package/dist/server/server/manifest.js +454 -0
  40. package/dist/server/server/migrate-legacy.js +229 -0
  41. package/dist/server/server/parsers/event-parser.test.js +117 -0
  42. package/dist/server/server/parsers/scope-parser.js +74 -28
  43. package/dist/server/server/parsers/scope-parser.test.js +230 -0
  44. package/dist/server/server/project-context.js +255 -0
  45. package/dist/server/server/project-emitter.js +41 -0
  46. package/dist/server/server/project-manager.js +297 -0
  47. package/dist/server/server/routes/config-routes.js +1 -3
  48. package/dist/server/server/routes/data-routes.js +22 -110
  49. package/dist/server/server/routes/dispatch-routes.js +15 -9
  50. package/dist/server/server/routes/git-routes.js +74 -0
  51. package/dist/server/server/routes/manifest-routes.js +319 -0
  52. package/dist/server/server/routes/scope-routes.js +37 -23
  53. package/dist/server/server/routes/sync-routes.js +134 -0
  54. package/dist/server/server/routes/version-routes.js +1 -15
  55. package/dist/server/server/routes/workflow-routes.js +9 -3
  56. package/dist/server/server/schema.js +2 -0
  57. package/dist/server/server/services/batch-orchestrator.js +26 -16
  58. package/dist/server/server/services/claude-session-service.js +17 -14
  59. package/dist/server/server/services/deploy-service.test.js +119 -0
  60. package/dist/server/server/services/event-service.js +64 -1
  61. package/dist/server/server/services/event-service.test.js +191 -0
  62. package/dist/server/server/services/gate-service.test.js +105 -0
  63. package/dist/server/server/services/git-service.js +108 -4
  64. package/dist/server/server/services/github-service.js +110 -2
  65. package/dist/server/server/services/readiness-service.test.js +190 -0
  66. package/dist/server/server/services/scope-cache.js +5 -1
  67. package/dist/server/server/services/scope-cache.test.js +142 -0
  68. package/dist/server/server/services/scope-service.js +217 -126
  69. package/dist/server/server/services/scope-service.test.js +137 -0
  70. package/dist/server/server/services/sprint-orchestrator.js +7 -6
  71. package/dist/server/server/services/sprint-service.js +21 -1
  72. package/dist/server/server/services/sprint-service.test.js +238 -0
  73. package/dist/server/server/services/sync-service.js +434 -0
  74. package/dist/server/server/services/sync-types.js +2 -0
  75. package/dist/server/server/services/telemetry-service.js +143 -0
  76. package/dist/server/server/services/workflow-service.js +26 -5
  77. package/dist/server/server/services/workflow-service.test.js +159 -0
  78. package/dist/server/server/settings-sync.js +284 -0
  79. package/dist/server/server/update-planner.js +279 -0
  80. package/dist/server/server/utils/cc-hooks-parser.js +3 -0
  81. package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
  82. package/dist/server/server/utils/dispatch-utils.js +77 -20
  83. package/dist/server/server/utils/dispatch-utils.test.js +182 -0
  84. package/dist/server/server/utils/logger.js +37 -3
  85. package/dist/server/server/utils/package-info.js +30 -0
  86. package/dist/server/server/utils/route-helpers.js +10 -0
  87. package/dist/server/server/utils/terminal-launcher.js +79 -25
  88. package/dist/server/server/utils/worktree-manager.js +13 -4
  89. package/dist/server/server/validator.js +230 -0
  90. package/dist/server/server/watchers/global-watcher.js +63 -0
  91. package/dist/server/server/watchers/scope-watcher.js +27 -12
  92. package/dist/server/server/wizard/config-editor.js +237 -0
  93. package/dist/server/server/wizard/detect.js +96 -0
  94. package/dist/server/server/wizard/doctor.js +115 -0
  95. package/dist/server/server/wizard/index.js +155 -0
  96. package/dist/server/server/wizard/phases/confirm.js +39 -0
  97. package/dist/server/server/wizard/phases/project-setup.js +90 -0
  98. package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
  99. package/dist/server/server/wizard/phases/welcome.js +35 -0
  100. package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
  101. package/dist/server/server/wizard/types.js +29 -0
  102. package/dist/server/server/wizard/ui.js +74 -0
  103. package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
  104. package/dist/server/shared/default-workflow.json +65 -0
  105. package/dist/server/shared/onboarding-tour.test.js +81 -0
  106. package/dist/server/shared/project-colors.js +24 -0
  107. package/dist/server/shared/workflow-config.test.js +84 -0
  108. package/dist/server/shared/workflow-engine.test.js +302 -0
  109. package/dist/server/shared/workflow-normalizer.js +101 -0
  110. package/dist/server/shared/workflow-normalizer.test.js +100 -0
  111. package/dist/server/src/components/onboarding/tour-steps.js +84 -0
  112. package/package.json +20 -15
  113. package/schemas/orbital.config.schema.json +16 -1
  114. package/scripts/postinstall.js +55 -7
  115. package/server/__tests__/data-routes.test.ts +149 -0
  116. package/server/__tests__/helpers/db.ts +19 -0
  117. package/server/__tests__/helpers/mock-emitter.ts +10 -0
  118. package/server/__tests__/scope-routes.test.ts +157 -0
  119. package/server/__tests__/sprint-routes.test.ts +118 -0
  120. package/server/__tests__/workflow-routes.test.ts +120 -0
  121. package/server/config-migrator.ts +163 -0
  122. package/server/config.ts +26 -2
  123. package/server/database.ts +35 -18
  124. package/server/global-config.ts +200 -0
  125. package/server/index.ts +975 -287
  126. package/server/init.ts +625 -182
  127. package/server/launch.ts +32 -0
  128. package/server/manifest-types.ts +145 -0
  129. package/server/manifest.ts +494 -0
  130. package/server/migrate-legacy.ts +290 -0
  131. package/server/parsers/event-parser.test.ts +135 -0
  132. package/server/parsers/scope-parser.test.ts +270 -0
  133. package/server/parsers/scope-parser.ts +79 -31
  134. package/server/project-context.ts +309 -0
  135. package/server/project-emitter.ts +50 -0
  136. package/server/project-manager.ts +369 -0
  137. package/server/routes/config-routes.ts +3 -5
  138. package/server/routes/data-routes.ts +28 -141
  139. package/server/routes/dispatch-routes.ts +19 -11
  140. package/server/routes/git-routes.ts +77 -0
  141. package/server/routes/manifest-routes.ts +388 -0
  142. package/server/routes/scope-routes.ts +29 -25
  143. package/server/routes/sync-routes.ts +175 -0
  144. package/server/routes/version-routes.ts +1 -16
  145. package/server/routes/workflow-routes.ts +9 -3
  146. package/server/schema.ts +2 -0
  147. package/server/services/batch-orchestrator.ts +24 -16
  148. package/server/services/claude-session-service.ts +16 -14
  149. package/server/services/deploy-service.test.ts +145 -0
  150. package/server/services/deploy-service.ts +2 -2
  151. package/server/services/event-service.test.ts +242 -0
  152. package/server/services/event-service.ts +92 -3
  153. package/server/services/gate-service.test.ts +131 -0
  154. package/server/services/gate-service.ts +2 -2
  155. package/server/services/git-service.ts +137 -4
  156. package/server/services/github-service.ts +120 -2
  157. package/server/services/readiness-service.test.ts +217 -0
  158. package/server/services/scope-cache.test.ts +167 -0
  159. package/server/services/scope-cache.ts +4 -1
  160. package/server/services/scope-service.test.ts +169 -0
  161. package/server/services/scope-service.ts +220 -126
  162. package/server/services/sprint-orchestrator.ts +7 -7
  163. package/server/services/sprint-service.test.ts +271 -0
  164. package/server/services/sprint-service.ts +27 -3
  165. package/server/services/sync-service.ts +482 -0
  166. package/server/services/sync-types.ts +77 -0
  167. package/server/services/telemetry-service.ts +195 -0
  168. package/server/services/workflow-service.test.ts +190 -0
  169. package/server/services/workflow-service.ts +29 -9
  170. package/server/settings-sync.ts +359 -0
  171. package/server/update-planner.ts +346 -0
  172. package/server/utils/cc-hooks-parser.test.ts +96 -0
  173. package/server/utils/cc-hooks-parser.ts +4 -0
  174. package/server/utils/dispatch-utils.test.ts +245 -0
  175. package/server/utils/dispatch-utils.ts +97 -27
  176. package/server/utils/logger.ts +40 -3
  177. package/server/utils/package-info.ts +32 -0
  178. package/server/utils/route-helpers.ts +12 -0
  179. package/server/utils/terminal-launcher.ts +85 -25
  180. package/server/utils/worktree-manager.ts +9 -4
  181. package/server/validator.ts +270 -0
  182. package/server/watchers/global-watcher.ts +77 -0
  183. package/server/watchers/scope-watcher.ts +21 -9
  184. package/server/wizard/config-editor.ts +248 -0
  185. package/server/wizard/detect.ts +104 -0
  186. package/server/wizard/doctor.ts +114 -0
  187. package/server/wizard/index.ts +187 -0
  188. package/server/wizard/phases/confirm.ts +45 -0
  189. package/server/wizard/phases/project-setup.ts +106 -0
  190. package/server/wizard/phases/setup-wizard.ts +78 -0
  191. package/server/wizard/phases/welcome.ts +43 -0
  192. package/server/wizard/phases/workflow-setup.ts +28 -0
  193. package/server/wizard/types.ts +56 -0
  194. package/server/wizard/ui.ts +93 -0
  195. package/shared/__fixtures__/workflow-configs.ts +80 -0
  196. package/shared/default-workflow.json +65 -0
  197. package/shared/onboarding-tour.test.ts +94 -0
  198. package/shared/project-colors.ts +24 -0
  199. package/shared/workflow-config.test.ts +111 -0
  200. package/shared/workflow-config.ts +7 -0
  201. package/shared/workflow-engine.test.ts +388 -0
  202. package/shared/workflow-normalizer.test.ts +119 -0
  203. package/shared/workflow-normalizer.ts +118 -0
  204. package/templates/hooks/end-session.sh +3 -1
  205. package/templates/hooks/orbital-emit.sh +2 -2
  206. package/templates/hooks/orbital-report-deploy.sh +4 -4
  207. package/templates/hooks/orbital-report-gates.sh +4 -4
  208. package/templates/hooks/orbital-scope-update.sh +1 -1
  209. package/templates/hooks/scope-create-cleanup.sh +2 -2
  210. package/templates/hooks/scope-create-gate.sh +0 -1
  211. package/templates/hooks/scope-helpers.sh +18 -0
  212. package/templates/hooks/scope-prepare.sh +66 -11
  213. package/templates/migrations/renames.json +1 -0
  214. package/templates/orbital.config.json +7 -2
  215. package/templates/settings-hooks.json +1 -1
  216. package/templates/skills/git-commit/SKILL.md +9 -4
  217. package/templates/skills/git-dev/SKILL.md +8 -3
  218. package/templates/skills/git-main/SKILL.md +8 -2
  219. package/templates/skills/git-production/SKILL.md +6 -2
  220. package/templates/skills/git-staging/SKILL.md +8 -3
  221. package/templates/skills/scope-create/SKILL.md +17 -3
  222. package/templates/skills/scope-fix-review/SKILL.md +6 -3
  223. package/templates/skills/scope-implement/SKILL.md +4 -1
  224. package/templates/skills/scope-post-review/SKILL.md +63 -5
  225. package/templates/skills/scope-pre-review/SKILL.md +5 -2
  226. package/templates/skills/scope-verify/SKILL.md +5 -3
  227. package/templates/skills/test-code-review/SKILL.md +41 -33
  228. package/templates/skills/test-scaffold/SKILL.md +222 -0
  229. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
  230. package/dist/assets/charts-D__PA1zp.js +0 -72
  231. package/dist/assets/index-D1G6i0nS.css +0 -1
  232. package/dist/assets/index-DpItvKpf.js +0 -419
  233. package/dist/assets/ui-BvF022GT.js +0 -53
  234. package/index.html +0 -15
  235. package/postcss.config.js +0 -6
  236. package/src/App.tsx +0 -33
  237. package/src/components/AgentBadge.tsx +0 -40
  238. package/src/components/BatchPreflightModal.tsx +0 -115
  239. package/src/components/CardDisplayToggle.tsx +0 -74
  240. package/src/components/ColumnHeaderActions.tsx +0 -55
  241. package/src/components/ColumnMenu.tsx +0 -99
  242. package/src/components/DeployHistory.tsx +0 -141
  243. package/src/components/DispatchModal.tsx +0 -164
  244. package/src/components/DispatchPopover.tsx +0 -139
  245. package/src/components/DragOverlay.tsx +0 -25
  246. package/src/components/DriftSidebar.tsx +0 -140
  247. package/src/components/EnvironmentStrip.tsx +0 -88
  248. package/src/components/ErrorBoundary.tsx +0 -62
  249. package/src/components/FilterChip.tsx +0 -105
  250. package/src/components/GateIndicator.tsx +0 -33
  251. package/src/components/IdeaDetailModal.tsx +0 -190
  252. package/src/components/IdeaFormDialog.tsx +0 -113
  253. package/src/components/KanbanColumn.tsx +0 -201
  254. package/src/components/MarkdownRenderer.tsx +0 -114
  255. package/src/components/NeonGrid.tsx +0 -128
  256. package/src/components/PromotionQueue.tsx +0 -89
  257. package/src/components/ScopeCard.tsx +0 -234
  258. package/src/components/ScopeDetailModal.tsx +0 -255
  259. package/src/components/ScopeFilterBar.tsx +0 -152
  260. package/src/components/SearchInput.tsx +0 -102
  261. package/src/components/SessionPanel.tsx +0 -335
  262. package/src/components/SprintContainer.tsx +0 -303
  263. package/src/components/SprintDependencyDialog.tsx +0 -78
  264. package/src/components/SprintPreflightModal.tsx +0 -138
  265. package/src/components/StatusBar.tsx +0 -168
  266. package/src/components/SwimCell.tsx +0 -67
  267. package/src/components/SwimLaneRow.tsx +0 -94
  268. package/src/components/SwimlaneBoardView.tsx +0 -108
  269. package/src/components/VersionBadge.tsx +0 -139
  270. package/src/components/ViewModeSelector.tsx +0 -114
  271. package/src/components/config/AgentChip.tsx +0 -53
  272. package/src/components/config/AgentCreateDialog.tsx +0 -321
  273. package/src/components/config/AgentEditor.tsx +0 -175
  274. package/src/components/config/DirectoryTree.tsx +0 -582
  275. package/src/components/config/FileEditor.tsx +0 -550
  276. package/src/components/config/HookChip.tsx +0 -50
  277. package/src/components/config/StageCard.tsx +0 -198
  278. package/src/components/config/TransitionZone.tsx +0 -173
  279. package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
  280. package/src/components/config/WorkflowPipeline.tsx +0 -161
  281. package/src/components/source-control/BranchList.tsx +0 -93
  282. package/src/components/source-control/BranchPanel.tsx +0 -105
  283. package/src/components/source-control/CommitLog.tsx +0 -100
  284. package/src/components/source-control/CommitRow.tsx +0 -47
  285. package/src/components/source-control/GitHubPanel.tsx +0 -110
  286. package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
  287. package/src/components/source-control/GitOverviewBar.tsx +0 -101
  288. package/src/components/source-control/PullRequestList.tsx +0 -69
  289. package/src/components/source-control/WorktreeList.tsx +0 -80
  290. package/src/components/ui/badge.tsx +0 -41
  291. package/src/components/ui/button.tsx +0 -55
  292. package/src/components/ui/card.tsx +0 -78
  293. package/src/components/ui/dialog.tsx +0 -94
  294. package/src/components/ui/popover.tsx +0 -33
  295. package/src/components/ui/scroll-area.tsx +0 -54
  296. package/src/components/ui/separator.tsx +0 -28
  297. package/src/components/ui/tabs.tsx +0 -52
  298. package/src/components/ui/toggle-switch.tsx +0 -35
  299. package/src/components/ui/tooltip.tsx +0 -27
  300. package/src/components/workflow/AddEdgeDialog.tsx +0 -217
  301. package/src/components/workflow/AddListDialog.tsx +0 -201
  302. package/src/components/workflow/ChecklistEditor.tsx +0 -239
  303. package/src/components/workflow/CommandPrefixManager.tsx +0 -118
  304. package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
  305. package/src/components/workflow/DirectionSelector.tsx +0 -133
  306. package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
  307. package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
  308. package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
  309. package/src/components/workflow/EditToolbar.tsx +0 -138
  310. package/src/components/workflow/HookDetailPanel.tsx +0 -250
  311. package/src/components/workflow/HookExecutionLog.tsx +0 -24
  312. package/src/components/workflow/HookSourceModal.tsx +0 -129
  313. package/src/components/workflow/HooksDashboard.tsx +0 -363
  314. package/src/components/workflow/ListPropertyEditor.tsx +0 -251
  315. package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
  316. package/src/components/workflow/MovementRulesPanel.tsx +0 -188
  317. package/src/components/workflow/NodeDetailPanel.tsx +0 -245
  318. package/src/components/workflow/PresetSelector.tsx +0 -414
  319. package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
  320. package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
  321. package/src/components/workflow/WorkflowNode.tsx +0 -147
  322. package/src/components/workflow/graphLayout.ts +0 -186
  323. package/src/components/workflow/mergeHooks.ts +0 -85
  324. package/src/components/workflow/useEditHistory.ts +0 -88
  325. package/src/components/workflow/useWorkflowEditor.ts +0 -262
  326. package/src/components/workflow/validateConfig.ts +0 -70
  327. package/src/hooks/useActiveDispatches.ts +0 -198
  328. package/src/hooks/useBoardSettings.ts +0 -170
  329. package/src/hooks/useCardDisplay.ts +0 -57
  330. package/src/hooks/useCcHooks.ts +0 -24
  331. package/src/hooks/useConfigTree.ts +0 -51
  332. package/src/hooks/useEnforcementRules.ts +0 -46
  333. package/src/hooks/useEvents.ts +0 -59
  334. package/src/hooks/useFileEditor.ts +0 -165
  335. package/src/hooks/useGates.ts +0 -57
  336. package/src/hooks/useIdeaActions.ts +0 -53
  337. package/src/hooks/useKanbanDnd.ts +0 -410
  338. package/src/hooks/useOrbitalConfig.ts +0 -54
  339. package/src/hooks/usePipeline.ts +0 -47
  340. package/src/hooks/usePipelineData.ts +0 -338
  341. package/src/hooks/useReconnect.ts +0 -25
  342. package/src/hooks/useScopeFilters.ts +0 -125
  343. package/src/hooks/useScopeSessions.ts +0 -44
  344. package/src/hooks/useScopes.ts +0 -67
  345. package/src/hooks/useSearch.ts +0 -67
  346. package/src/hooks/useSettings.tsx +0 -187
  347. package/src/hooks/useSocket.ts +0 -25
  348. package/src/hooks/useSourceControl.ts +0 -105
  349. package/src/hooks/useSprintPreflight.ts +0 -55
  350. package/src/hooks/useSprints.ts +0 -154
  351. package/src/hooks/useStatusBarHighlight.ts +0 -18
  352. package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
  353. package/src/hooks/useTheme.ts +0 -9
  354. package/src/hooks/useTransitionReadiness.ts +0 -53
  355. package/src/hooks/useVersion.ts +0 -155
  356. package/src/hooks/useViolations.ts +0 -65
  357. package/src/hooks/useWorkflow.tsx +0 -125
  358. package/src/hooks/useZoomModifier.ts +0 -19
  359. package/src/index.css +0 -797
  360. package/src/layouts/DashboardLayout.tsx +0 -113
  361. package/src/lib/collisionDetection.ts +0 -20
  362. package/src/lib/scope-fields.ts +0 -61
  363. package/src/lib/swimlane.ts +0 -146
  364. package/src/lib/utils.ts +0 -15
  365. package/src/main.tsx +0 -19
  366. package/src/socket.ts +0 -11
  367. package/src/types/index.ts +0 -497
  368. package/src/views/AgentFeed.tsx +0 -339
  369. package/src/views/DeployPipeline.tsx +0 -59
  370. package/src/views/EnforcementView.tsx +0 -378
  371. package/src/views/PrimitivesConfig.tsx +0 -500
  372. package/src/views/QualityGates.tsx +0 -1012
  373. package/src/views/ScopeBoard.tsx +0 -454
  374. package/src/views/SessionTimeline.tsx +0 -516
  375. package/src/views/Settings.tsx +0 -183
  376. package/src/views/SourceControl.tsx +0 -95
  377. package/src/views/WorkflowVisualizer.tsx +0 -382
  378. package/tailwind.config.js +0 -161
  379. package/tsconfig.json +0 -25
  380. 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);
@@ -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): { 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);
398
+ // Build new path
399
+ const titleSlug = this.slugify(title);
312
400
  const planningDir = path.join(this.scopesDir, 'planning');
313
401
  if (!fs.existsSync(planningDir)) fs.mkdirSync(planningDir, { recursive: true });
314
- const newFileName = `${paddedId}-${slug}.md`;
402
+ const newFileName = `${paddedId}-${titleSlug}.md`;
315
403
  const newPath = path.join(planningDir, 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 = 'planning';
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,10 @@
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
5
  import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
6
6
  import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
7
7
  import type { WorkflowEngine } from '../../shared/workflow-engine.js';
8
- import { getConfig } from '../config.js';
9
8
  import { createLogger } from '../utils/logger.js';
10
9
 
11
10
  const log = createLogger('sprint');
@@ -20,10 +19,11 @@ function sleep(ms: number): Promise<void> {
20
19
  export class SprintOrchestrator {
21
20
  constructor(
22
21
  private db: Database.Database,
23
- private io: Server,
22
+ private io: Emitter,
24
23
  private sprintService: SprintService,
25
24
  private scopeService: ScopeService,
26
25
  private engine: WorkflowEngine,
26
+ private projectRoot: string,
27
27
  ) {}
28
28
 
29
29
  /** Build execution layers using Kahn's topological sort */
@@ -247,20 +247,20 @@ export class SprintOrchestrator {
247
247
  // Build scope-aware session name and snapshot PIDs
248
248
  const scopeRow = this.scopeService.getById(scopeId);
249
249
  const sessionName = buildSessionName({ scopeId, title: scopeRow?.title, command });
250
- const beforePids = snapshotSessionPids(getConfig().projectRoot);
250
+ const beforePids = snapshotSessionPids(this.projectRoot);
251
251
 
252
252
  // Launch in iTerm — interactive TUI mode (no -p) for full visibility
253
253
  const escaped = escapeForAnsiC(command);
254
- const fullCmd = `cd '${getConfig().projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
254
+ const fullCmd = `cd '${this.projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
255
255
  try {
256
256
  await launchInCategorizedTerminal(command, fullCmd, sessionName);
257
257
 
258
258
  // Fire-and-forget: discover session PID, link to dispatch, and rename
259
- discoverNewSession(getConfig().projectRoot, beforePids)
259
+ discoverNewSession(this.projectRoot, beforePids)
260
260
  .then((session) => {
261
261
  if (!session) return;
262
262
  linkPidToDispatch(this.db, eventId, session.pid);
263
- if (sessionName) renameSession(getConfig().projectRoot, session.sessionId, sessionName);
263
+ if (sessionName) renameSession(this.projectRoot, session.sessionId, sessionName);
264
264
  })
265
265
  .catch(err => log.error('PID discovery failed', { error: err.message }));
266
266
  } catch (err) {