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
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ScopeCache } from './scope-cache.js';
3
+ function makeScope(overrides) {
4
+ return {
5
+ title: `Scope ${overrides.id}`,
6
+ slug: undefined,
7
+ status: 'backlog',
8
+ priority: null,
9
+ effort_estimate: null,
10
+ category: null,
11
+ tags: [],
12
+ blocked_by: [],
13
+ blocks: [],
14
+ file_path: `/scopes/backlog/${String(overrides.id).padStart(3, '0')}-test.md`,
15
+ created_at: null,
16
+ updated_at: null,
17
+ raw_content: '',
18
+ sessions: {},
19
+ is_ghost: false,
20
+ favourite: false,
21
+ ...overrides,
22
+ };
23
+ }
24
+ describe('ScopeCache', () => {
25
+ let cache;
26
+ beforeEach(() => {
27
+ cache = new ScopeCache();
28
+ });
29
+ describe('loadAll()', () => {
30
+ it('populates both indexes', () => {
31
+ const scopes = [makeScope({ id: 1 }), makeScope({ id: 2 })];
32
+ cache.loadAll(scopes);
33
+ expect(cache.size).toBe(2);
34
+ expect(cache.getById(1)).toBeDefined();
35
+ expect(cache.getById(2)).toBeDefined();
36
+ });
37
+ it('clears previous data on re-load', () => {
38
+ cache.loadAll([makeScope({ id: 1 })]);
39
+ expect(cache.size).toBe(1);
40
+ cache.loadAll([makeScope({ id: 5 }), makeScope({ id: 6 })]);
41
+ expect(cache.size).toBe(2);
42
+ expect(cache.has(1)).toBe(false);
43
+ expect(cache.has(5)).toBe(true);
44
+ });
45
+ });
46
+ describe('set()', () => {
47
+ it('adds new scope', () => {
48
+ cache.set(makeScope({ id: 10 }));
49
+ expect(cache.has(10)).toBe(true);
50
+ expect(cache.size).toBe(1);
51
+ });
52
+ it('updates existing scope', () => {
53
+ cache.set(makeScope({ id: 10, title: 'Original' }));
54
+ cache.set(makeScope({ id: 10, title: 'Updated' }));
55
+ expect(cache.getById(10)?.title).toBe('Updated');
56
+ expect(cache.size).toBe(1);
57
+ });
58
+ it('cleans up old file_path index when scope moves', () => {
59
+ const oldPath = '/scopes/backlog/010-test.md';
60
+ const newPath = '/scopes/implementing/010-test.md';
61
+ cache.set(makeScope({ id: 10, file_path: oldPath }));
62
+ expect(cache.idByFilePath(oldPath)).toBe(10);
63
+ cache.set(makeScope({ id: 10, file_path: newPath }));
64
+ expect(cache.idByFilePath(oldPath)).toBeUndefined();
65
+ expect(cache.idByFilePath(newPath)).toBe(10);
66
+ });
67
+ });
68
+ describe('removeByFilePath()', () => {
69
+ it('removes from both indexes and returns removed ID', () => {
70
+ const scope = makeScope({ id: 10 });
71
+ cache.set(scope);
72
+ const removedId = cache.removeByFilePath(scope.file_path);
73
+ expect(removedId).toBe(10);
74
+ expect(cache.has(10)).toBe(false);
75
+ expect(cache.idByFilePath(scope.file_path)).toBeUndefined();
76
+ });
77
+ it('returns undefined for unknown path', () => {
78
+ expect(cache.removeByFilePath('/nonexistent')).toBeUndefined();
79
+ });
80
+ });
81
+ describe('read operations', () => {
82
+ beforeEach(() => {
83
+ cache.loadAll([
84
+ makeScope({ id: 3, title: 'Three' }),
85
+ makeScope({ id: 1, title: 'One' }),
86
+ makeScope({ id: 2, title: 'Two' }),
87
+ ]);
88
+ });
89
+ it('getById() returns scope or undefined', () => {
90
+ expect(cache.getById(1)?.title).toBe('One');
91
+ expect(cache.getById(999)).toBeUndefined();
92
+ });
93
+ it('getAll() returns sorted by ID', () => {
94
+ const all = cache.getAll();
95
+ expect(all.map(s => s.id)).toEqual([1, 2, 3]);
96
+ });
97
+ it('has() returns boolean', () => {
98
+ expect(cache.has(1)).toBe(true);
99
+ expect(cache.has(999)).toBe(false);
100
+ });
101
+ it('idByFilePath() returns ID or undefined', () => {
102
+ const scope = cache.getById(1);
103
+ expect(cache.idByFilePath(scope.file_path)).toBe(1);
104
+ expect(cache.idByFilePath('/nonexistent')).toBeUndefined();
105
+ });
106
+ });
107
+ describe('maxNonIceboxId()', () => {
108
+ it('returns 0 for empty cache', () => {
109
+ expect(cache.maxNonIceboxId()).toBe(0);
110
+ });
111
+ it('returns highest raw ID', () => {
112
+ cache.loadAll([
113
+ makeScope({ id: 10, status: 'backlog' }),
114
+ makeScope({ id: 20, status: 'implementing' }),
115
+ makeScope({ id: 5, status: 'review' }),
116
+ ]);
117
+ expect(cache.maxNonIceboxId()).toBe(20);
118
+ });
119
+ it('ignores icebox-status scopes', () => {
120
+ cache.loadAll([
121
+ makeScope({ id: 10, status: 'backlog' }),
122
+ makeScope({ id: 50, status: 'icebox' }),
123
+ ]);
124
+ expect(cache.maxNonIceboxId()).toBe(10);
125
+ });
126
+ it('decodes encoded IDs (>= 1000) to raw numbers', () => {
127
+ // 1047 → suffix-encoded → raw is 1047 % 1000 = 47
128
+ cache.loadAll([
129
+ makeScope({ id: 1047, status: 'backlog' }),
130
+ makeScope({ id: 10, status: 'implementing' }),
131
+ ]);
132
+ expect(cache.maxNonIceboxId()).toBe(47);
133
+ });
134
+ it('skips raw IDs >= 500 (legacy icebox-origin)', () => {
135
+ cache.loadAll([
136
+ makeScope({ id: 501, status: 'backlog' }),
137
+ makeScope({ id: 30, status: 'implementing' }),
138
+ ]);
139
+ expect(cache.maxNonIceboxId()).toBe(30);
140
+ });
141
+ });
142
+ });
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import matter from 'gray-matter';
4
- import { normalizeStatus, parseAllScopes, parseScopeFile, setValidStatuses } from '../parsers/scope-parser.js';
4
+ import { parseAllScopes, parseScopeFile, setValidStatuses } from '../parsers/scope-parser.js';
5
5
  import { createLogger } from '../utils/logger.js';
6
6
  const log = createLogger('scope');
7
7
  export class ScopeService {
@@ -38,6 +38,44 @@ export class ScopeService {
38
38
  this.cache.loadAll(scopes);
39
39
  return scopes.length;
40
40
  }
41
+ /** Reconcile files whose directory doesn't match their frontmatter status.
42
+ * Frontmatter is the authoritative source — files are moved to match it.
43
+ * Called once at startup after syncFromFilesystem(). */
44
+ reconcileDirectories() {
45
+ let moved = 0;
46
+ for (const scope of this.cache.getAll()) {
47
+ if (scope.id < 0)
48
+ continue; // slug-only icebox items (negative IDs)
49
+ const currentDir = path.basename(path.dirname(scope.file_path));
50
+ if (currentDir === scope.status)
51
+ continue;
52
+ if (!this.engine.isValidStatus(scope.status))
53
+ continue;
54
+ const targetDir = path.join(this.scopesDir, scope.status);
55
+ if (!fs.existsSync(targetDir))
56
+ fs.mkdirSync(targetDir, { recursive: true });
57
+ const newPath = path.join(targetDir, path.basename(scope.file_path));
58
+ this.suppressedPaths.add(scope.file_path);
59
+ this.suppressedPaths.add(newPath);
60
+ try {
61
+ fs.renameSync(scope.file_path, newPath);
62
+ this.updateFromFile(newPath);
63
+ this.removeByFilePath(scope.file_path);
64
+ moved++;
65
+ log.warn('Reconciled directory mismatch', {
66
+ id: scope.id, frontmatter: scope.status, directory: currentDir,
67
+ });
68
+ }
69
+ catch (err) {
70
+ log.error('Failed to reconcile scope directory', { id: scope.id, error: String(err) });
71
+ }
72
+ setTimeout(() => {
73
+ this.suppressedPaths.delete(scope.file_path);
74
+ this.suppressedPaths.delete(newPath);
75
+ }, 2000);
76
+ }
77
+ return moved;
78
+ }
41
79
  /** Check if a path is suppressed from watcher processing (during programmatic moves) */
42
80
  isSuppressed(filePath) {
43
81
  return this.suppressedPaths.has(filePath);
@@ -74,7 +112,7 @@ export class ScopeService {
74
112
  if (id !== undefined) {
75
113
  if (previous)
76
114
  this.recentlyRemoved.set(id, previous.status);
77
- this.io.emit('scope:deleted', id);
115
+ this.io.emit('scope:deleted', { id });
78
116
  // Clean up stash after a short window (if add never fires, this was a real delete)
79
117
  setTimeout(() => this.recentlyRemoved.delete(id), 5000);
80
118
  }
@@ -89,6 +127,7 @@ export class ScopeService {
89
127
  }
90
128
  /** Update a scope's status with transition validation.
91
129
  * Writes the new status to the frontmatter file and updates the cache.
130
+ * This is the SINGLE validation point — all status changes must flow through here.
92
131
  * @param context - caller trust level: 'patch', 'dispatch', 'event', 'bulk-sync', 'rollback' */
93
132
  updateStatus(id, status, context = 'patch') {
94
133
  if (!this.engine.isValidStatus(status)) {
@@ -111,12 +150,11 @@ export class ScopeService {
111
150
  if (!check.ok)
112
151
  return check;
113
152
  }
114
- // Write to filesystem via updateScopeFrontmatter (which updates cache + emits)
115
- const current = context === 'bulk-sync' || context === 'rollback'
116
- ? this.cache.getById(id)
117
- : this.cache.getById(id); // already fetched above for validation, but may be null in bulk-sync
153
+ // Fetch current scope for fromStatus logging. In bulk-sync/rollback contexts
154
+ // the validation block above is skipped, so this may be the first lookup.
155
+ const current = this.cache.getById(id);
118
156
  const fromStatus = current?.status ?? 'unknown';
119
- const result = this.updateScopeFrontmatter(id, { status }, context);
157
+ const result = this._writeFrontmatter(id, { status });
120
158
  if (result.ok) {
121
159
  log.info('Status updated', { id, from: fromStatus, to: status, context });
122
160
  for (const cb of this.onStatusChangeCallbacks)
@@ -124,8 +162,31 @@ export class ScopeService {
124
162
  }
125
163
  return result;
126
164
  }
165
+ /** Update scope fields via a public API (e.g. PATCH route).
166
+ * Status changes are routed through updateStatus for validation.
167
+ * Non-status fields are written directly via _writeFrontmatter. */
168
+ updateFields(id, fields) {
169
+ const { status, ...nonStatusFields } = fields;
170
+ // Status changes go through updateStatus (validates transition, fires callbacks)
171
+ if (status) {
172
+ const current = this.cache.getById(id);
173
+ if (!current)
174
+ return { ok: false, error: 'Scope not found', code: 'NOT_FOUND' };
175
+ if (status !== current.status) {
176
+ const result = this.updateStatus(id, status);
177
+ if (!result.ok)
178
+ return result;
179
+ }
180
+ }
181
+ // Non-status field updates written directly
182
+ if (Object.keys(nonStatusFields).length > 0) {
183
+ return this._writeFrontmatter(id, nonStatusFields);
184
+ }
185
+ return { ok: true, moved: !!status };
186
+ }
127
187
  /** Compute the next sequential scope ID by scanning all non-icebox scopes.
128
- * Checks both filesystem (all subdirs except icebox) and cache to prevent collisions. */
188
+ * Checks both filesystem (all subdirs except icebox) and cache to prevent collisions.
189
+ * Skips IDs >= 500 to handle legacy icebox-origin files during migration. */
129
190
  getNextScopeId() {
130
191
  let maxId = 0;
131
192
  // Scan all scope subdirectories except icebox
@@ -136,8 +197,13 @@ export class ScopeService {
136
197
  const dirPath = path.join(this.scopesDir, dir.name);
137
198
  for (const file of fs.readdirSync(dirPath)) {
138
199
  const m = file.match(/^(\d+)-/);
139
- if (m)
140
- maxId = Math.max(maxId, parseInt(m[1], 10));
200
+ if (!m)
201
+ continue;
202
+ const id = parseInt(m[1], 10);
203
+ // Skip legacy icebox-origin IDs (500+) to prevent namespace pollution
204
+ if (id >= 500)
205
+ continue;
206
+ maxId = Math.max(maxId, id);
141
207
  }
142
208
  }
143
209
  }
@@ -147,50 +213,61 @@ export class ScopeService {
147
213
  return maxId + 1;
148
214
  }
149
215
  // ─── Idea CRUD (filesystem-backed icebox cards) ────────────
150
- /** Get the next available icebox ID (starts at 501, increments from max found) */
151
- getNextIceboxId() {
152
- const iceboxDir = path.join(this.scopesDir, 'icebox');
153
- if (!fs.existsSync(iceboxDir))
154
- return 501;
155
- let maxId = 500;
156
- for (const file of fs.readdirSync(iceboxDir)) {
157
- const m = file.match(/^(\d+)-/);
158
- if (m)
159
- maxId = Math.max(maxId, parseInt(m[1], 10));
216
+ /** Normalize Date objects in gray-matter frontmatter to YYYY-MM-DD strings */
217
+ normalizeFrontmatterDates(data) {
218
+ for (const key of Object.keys(data)) {
219
+ if (data[key] instanceof Date) {
220
+ data[key] = data[key].toISOString().split('T')[0];
221
+ }
160
222
  }
161
- return maxId + 1;
162
223
  }
163
- /** Find an icebox file by its ID prefix.
164
- * Matches both padded (091-) and unpadded (91-) filenames
165
- * since demoted scopes keep their 3-digit-padded names. */
166
- findIdeaFile(iceboxDir, id) {
224
+ /** Generate a slug from a title */
225
+ slugify(title) {
226
+ const slug = title
227
+ .toLowerCase()
228
+ .replace(/[^a-z0-9]+/g, '-')
229
+ .replace(/^-|-$/g, '')
230
+ .slice(0, 60);
231
+ if (!slug)
232
+ return 'untitled';
233
+ return slug;
234
+ }
235
+ /** Find an icebox file by its slug.
236
+ * Matches slug-only files ({slug}.md) and legacy numeric-prefixed files ({NNN}-{slug}.md). */
237
+ findIdeaFile(iceboxDir, slug) {
167
238
  if (!fs.existsSync(iceboxDir))
168
239
  return null;
169
240
  const match = fs.readdirSync(iceboxDir).find((f) => {
170
241
  if (!f.endsWith('.md'))
171
242
  return false;
172
- const m = f.match(/^(\d+)-/);
173
- return m != null && parseInt(m[1], 10) === id;
243
+ // Match slug-only: {slug}.md
244
+ if (f === `${slug}.md`)
245
+ return true;
246
+ // Match legacy numeric-prefixed: {NNN}-{slug}.md
247
+ return f.match(/^\d+-/) && f.slice(f.indexOf('-') + 1) === `${slug}.md`;
174
248
  });
175
249
  return match ? path.join(iceboxDir, match) : null;
176
250
  }
177
- /** Create an icebox idea as a markdown file. IDs start at 501. */
251
+ /** Create an icebox idea as a slug-only markdown file. */
178
252
  createIdeaFile(title, description) {
179
253
  const iceboxDir = path.join(this.scopesDir, 'icebox');
180
254
  if (!fs.existsSync(iceboxDir))
181
255
  fs.mkdirSync(iceboxDir, { recursive: true });
182
- const nextId = this.getNextIceboxId();
183
- const slug = title
184
- .toLowerCase()
185
- .replace(/[^a-z0-9]+/g, '-')
186
- .replace(/^-|-$/g, '')
187
- .slice(0, 60);
188
- const fileName = `${nextId}-${slug}.md`;
189
- const filePath = path.join(iceboxDir, fileName);
256
+ const slug = this.slugify(title);
257
+ let fileName = `${slug}.md`;
258
+ let filePath = path.join(iceboxDir, fileName);
259
+ // Handle slug collisions by appending -2, -3, etc.
260
+ if (fs.existsSync(filePath)) {
261
+ let suffix = 2;
262
+ while (fs.existsSync(path.join(iceboxDir, `${slug}-${suffix}.md`)))
263
+ suffix++;
264
+ fileName = `${slug}-${suffix}.md`;
265
+ filePath = path.join(iceboxDir, fileName);
266
+ }
267
+ const finalSlug = fileName.replace(/\.md$/, '');
190
268
  const now = new Date().toISOString().split('T')[0];
191
269
  const content = [
192
270
  '---',
193
- `id: ${nextId}`,
194
271
  `title: "${title.replace(/"/g, '\\"')}"`,
195
272
  'status: icebox',
196
273
  `created: ${now}`,
@@ -206,105 +283,124 @@ export class ScopeService {
206
283
  fs.writeFileSync(filePath, content, 'utf-8');
207
284
  // Eagerly sync to cache + emit scope:created
208
285
  this.updateFromFile(filePath);
209
- log.info('Idea created', { id: nextId, title });
210
- return { id: nextId, title };
286
+ log.info('Idea created', { slug: finalSlug, title });
287
+ return { slug: finalSlug, title };
211
288
  }
212
- /** Update an icebox idea's title and description by rewriting its file */
213
- updateIdeaFile(id, title, description) {
289
+ /** Update an icebox idea's title and description in-place. Renames the file if the title slug changes. */
290
+ updateIdeaFile(slug, title, description) {
214
291
  const iceboxDir = path.join(this.scopesDir, 'icebox');
215
- const filePath = this.findIdeaFile(iceboxDir, id);
292
+ const filePath = this.findIdeaFile(iceboxDir, slug);
216
293
  if (!filePath)
217
294
  return false;
218
295
  // Preserve the original created date from existing frontmatter
219
296
  const existing = fs.readFileSync(filePath, 'utf-8');
220
- const createdMatch = existing.match(/^created:\s*(.+)$/m);
221
- const created = createdMatch?.[1]?.trim() ?? new Date().toISOString().split('T')[0];
297
+ const parsed = matter(existing);
298
+ const created = parsed.data.created ? String(parsed.data.created) : new Date().toISOString().split('T')[0];
222
299
  const now = new Date().toISOString().split('T')[0];
223
- const content = [
224
- '---',
225
- `id: ${id}`,
226
- `title: "${title.replace(/"/g, '\\"')}"`,
227
- 'status: icebox',
228
- `created: ${created}`,
229
- `updated: ${now}`,
230
- 'blocked_by: []',
231
- 'blocks: []',
232
- 'tags: []',
233
- '---',
234
- '',
235
- description || '',
236
- '',
237
- ].join('\n');
238
- fs.writeFileSync(filePath, content, 'utf-8');
239
- // Watcher handles cache sync + scope:updated event
300
+ // Update frontmatter fields while preserving other data (like ghost)
301
+ parsed.data.title = title;
302
+ parsed.data.updated = now;
303
+ parsed.data.created = created;
304
+ this.normalizeFrontmatterDates(parsed.data);
305
+ const newContent = matter.stringify(description ? `\n${description}\n` : '\n', parsed.data);
306
+ fs.writeFileSync(filePath, newContent, 'utf-8');
307
+ // If title changed, rename file to new slug
308
+ const newSlug = this.slugify(title);
309
+ if (newSlug !== slug) {
310
+ const newFileName = `${newSlug}.md`;
311
+ const newPath = path.join(iceboxDir, newFileName);
312
+ if (!fs.existsSync(newPath)) {
313
+ this.suppressedPaths.add(filePath);
314
+ this.suppressedPaths.add(newPath);
315
+ this.removeByFilePath(filePath);
316
+ fs.renameSync(filePath, newPath);
317
+ this.updateFromFile(newPath);
318
+ setTimeout(() => {
319
+ this.suppressedPaths.delete(filePath);
320
+ this.suppressedPaths.delete(newPath);
321
+ }, 2000);
322
+ }
323
+ else {
324
+ // Collision with existing slug — keep old filename, still sync content changes
325
+ log.warn('Slug collision during rename, keeping old filename', { slug, newSlug });
326
+ this.updateFromFile(filePath);
327
+ }
328
+ }
329
+ else {
330
+ // Eagerly sync content changes to cache
331
+ this.updateFromFile(filePath);
332
+ }
240
333
  return true;
241
334
  }
242
335
  /** Delete an icebox idea by removing its file */
243
- deleteIdeaFile(id) {
336
+ deleteIdeaFile(slug) {
244
337
  const iceboxDir = path.join(this.scopesDir, 'icebox');
245
- const filePath = this.findIdeaFile(iceboxDir, id);
338
+ const filePath = this.findIdeaFile(iceboxDir, slug);
246
339
  if (!filePath)
247
340
  return false;
248
341
  fs.unlinkSync(filePath);
249
342
  // Eagerly remove from cache + emit scope:deleted
250
343
  this.removeByFilePath(filePath);
251
- log.info('Idea deleted', { id });
344
+ log.info('Idea deleted', { slug });
252
345
  return true;
253
346
  }
254
347
  /** Promote an icebox idea to planning — assigns a proper sequential scope ID,
255
348
  * moves the file, and syncs cache. Returns the new scope ID. */
256
- promoteIdea(id) {
349
+ promoteIdea(slug, targetStatus = 'planning') {
257
350
  const iceboxDir = path.join(this.scopesDir, 'icebox');
258
- const oldPath = this.findIdeaFile(iceboxDir, id);
351
+ const oldPath = this.findIdeaFile(iceboxDir, slug);
259
352
  if (!oldPath)
260
353
  return null;
261
354
  // Read existing file for metadata
262
- const content = fs.readFileSync(oldPath, 'utf-8');
263
- const titleMatch = content.match(/^title:\s*"?([^"\n]+)"?\s*$/m);
264
- const createdMatch = content.match(/^created:\s*(.+)$/m);
265
- const title = titleMatch?.[1]?.trim() ?? 'Untitled';
266
- const created = createdMatch?.[1]?.trim() ?? new Date().toISOString().split('T')[0];
267
- // Extract body after frontmatter
268
- const fmEnd = content.indexOf('---', content.indexOf('---') + 3);
269
- const description = fmEnd !== -1 ? content.slice(fmEnd + 3).trim() : '';
355
+ const raw = fs.readFileSync(oldPath, 'utf-8');
356
+ const parsed = matter(raw);
357
+ const title = parsed.data.title ? String(parsed.data.title) : 'Untitled';
358
+ const created = parsed.data.created ? String(parsed.data.created) : new Date().toISOString().split('T')[0];
359
+ const description = parsed.content.trim();
270
360
  // Assign the next sequential scope ID (excludes icebox items)
271
361
  const newId = this.getNextScopeId();
272
362
  const paddedId = String(newId).padStart(3, '0');
273
- // Build slug and new path
274
- const slug = title
275
- .toLowerCase()
276
- .replace(/[^a-z0-9]+/g, '-')
277
- .replace(/^-|-$/g, '')
278
- .slice(0, 60);
279
- const planningDir = path.join(this.scopesDir, 'planning');
280
- if (!fs.existsSync(planningDir))
281
- fs.mkdirSync(planningDir, { recursive: true });
282
- const newFileName = `${paddedId}-${slug}.md`;
283
- const newPath = path.join(planningDir, newFileName);
363
+ // Build new path
364
+ const titleSlug = this.slugify(title);
365
+ const targetDir = path.join(this.scopesDir, targetStatus);
366
+ if (!fs.existsSync(targetDir))
367
+ fs.mkdirSync(targetDir, { recursive: true });
368
+ const newFileName = `${paddedId}-${titleSlug}.md`;
369
+ const newPath = path.join(targetDir, newFileName);
284
370
  const now = new Date().toISOString().split('T')[0];
285
- // Write new file with planning status and new sequential ID
286
- const newContent = [
287
- '---',
288
- `id: ${paddedId}`,
289
- `title: "${title.replace(/"/g, '\\"')}"`,
290
- 'status: planning',
291
- `created: ${created}`,
292
- `updated: ${now}`,
293
- 'blocked_by: []',
294
- 'blocks: []',
295
- 'tags: []',
296
- '---',
297
- '',
298
- description || '',
299
- '',
300
- ].join('\n');
301
- fs.writeFileSync(newPath, newContent, 'utf-8');
302
- // Sync cache before deleting old file (avoids window where scope is missing)
371
+ // Update frontmatter in-place: assign ID and change status (preserve other fields)
372
+ parsed.data.id = newId;
373
+ parsed.data.status = targetStatus;
374
+ parsed.data.updated = now;
375
+ parsed.data.created = created;
376
+ delete parsed.data.ghost;
377
+ this.normalizeFrontmatterDates(parsed.data);
378
+ const newContent = matter.stringify(parsed.content, parsed.data);
379
+ // Write updated content to old path, then rename/move (no intermediate missing state)
380
+ const originalContent = fs.readFileSync(oldPath, 'utf-8');
381
+ fs.writeFileSync(oldPath, newContent, 'utf-8');
382
+ // Suppress watcher events during programmatic move
383
+ this.suppressedPaths.add(oldPath);
384
+ this.suppressedPaths.add(newPath);
385
+ try {
386
+ fs.renameSync(oldPath, newPath);
387
+ }
388
+ catch (err) {
389
+ // Restore original content on rename failure
390
+ fs.writeFileSync(oldPath, originalContent, 'utf-8');
391
+ this.suppressedPaths.delete(oldPath);
392
+ this.suppressedPaths.delete(newPath);
393
+ log.error('Failed to rename during promote', { oldPath, newPath, error: String(err) });
394
+ return null;
395
+ }
303
396
  this.updateFromFile(newPath);
304
- fs.unlinkSync(oldPath);
305
397
  this.removeByFilePath(oldPath);
398
+ setTimeout(() => {
399
+ this.suppressedPaths.delete(oldPath);
400
+ this.suppressedPaths.delete(newPath);
401
+ }, 2000);
306
402
  const relPath = path.relative(path.resolve(this.scopesDir, '..'), newPath);
307
- log.info('Idea promoted', { oldId: id, newId, title });
403
+ log.info('Idea promoted', { slug, newId, title });
308
404
  return { id: newId, filePath: relPath, title, description };
309
405
  }
310
406
  /** Find a scope file by its numeric ID prefix across all status directories */
@@ -325,10 +421,11 @@ export class ScopeService {
325
421
  }
326
422
  return null;
327
423
  }
328
- /** Update a scope's frontmatter fields and write back to the .md file.
329
- * If status changes, validates the transition and moves the file to the new status directory.
330
- * @param context - transition context for validation (default 'patch') */
331
- updateScopeFrontmatter(id, fields, context = 'patch') {
424
+ /** Write frontmatter fields to a scope's .md file.
425
+ * If the effective status differs from the current directory, moves the file.
426
+ * This is a trusted write operation callers must validate transitions
427
+ * via updateStatus() before calling this method with status changes. */
428
+ _writeFrontmatter(id, fields) {
332
429
  const filePath = this.findScopeFile(id);
333
430
  if (!filePath) {
334
431
  return { ok: false, error: 'Scope file not found', code: 'NOT_FOUND' };
@@ -336,25 +433,19 @@ export class ScopeService {
336
433
  const raw = fs.readFileSync(filePath, 'utf-8');
337
434
  const parsed = matter(raw);
338
435
  const today = new Date().toISOString().split('T')[0];
339
- // Validate status transition before any writes
436
+ // Determine if the file needs to move to a different directory.
437
+ // Compare against the DIRECTORY name (not frontmatter status) since the question
438
+ // is whether the physical file location matches the desired status.
340
439
  const newStatus = fields.status;
341
- const rawOldStatus = String(parsed.data.status ?? 'planning');
342
- const oldStatus = normalizeStatus(rawOldStatus);
343
- let needsMove = false;
344
- if (newStatus && newStatus !== oldStatus) {
345
- if (!this.engine.isValidStatus(newStatus)) {
346
- return { ok: false, error: `Invalid status: '${newStatus}'`, code: 'INVALID_STATUS' };
347
- }
348
- const check = this.engine.validateTransition(oldStatus, newStatus, context);
349
- if (!check.ok)
350
- return check;
351
- needsMove = true;
352
- // Auto-unlock spec when reverting backlog → planning
353
- if (newStatus === 'planning' && oldStatus === 'backlog')
354
- fields.spec_locked = false;
440
+ const dirName = path.basename(path.dirname(filePath));
441
+ const effectiveStatus = newStatus ?? String(parsed.data.status ?? dirName);
442
+ const needsMove = effectiveStatus !== dirName && this.engine.isValidStatus(effectiveStatus);
443
+ // Auto-unlock spec when reverting backlog → planning
444
+ if (newStatus === 'planning' && dirName === 'backlog') {
445
+ fields = { ...fields, spec_locked: false };
355
446
  }
356
447
  // Merge editable fields into frontmatter
357
- const editableKeys = ['title', 'status', 'priority', 'effort_estimate', 'category', 'tags', 'blocked_by', 'blocks', 'spec_locked'];
448
+ const editableKeys = ['title', 'status', 'priority', 'effort_estimate', 'category', 'tags', 'blocked_by', 'blocks', 'spec_locked', 'favourite'];
358
449
  for (const key of editableKeys) {
359
450
  if (key in fields) {
360
451
  const val = fields[key];
@@ -384,8 +475,8 @@ export class ScopeService {
384
475
  log.info('Frontmatter updated', { id, fields: Object.keys(fields) });
385
476
  return { ok: true };
386
477
  }
387
- // Status change → move file to new directory
388
- const targetDir = path.join(this.scopesDir, newStatus);
478
+ // Status differs from directory → move file to correct directory
479
+ const targetDir = path.join(this.scopesDir, effectiveStatus);
389
480
  if (!fs.existsSync(targetDir))
390
481
  fs.mkdirSync(targetDir, { recursive: true });
391
482
  const fileName = path.basename(filePath);
@@ -403,13 +494,13 @@ export class ScopeService {
403
494
  setTimeout(() => {
404
495
  this.suppressedPaths.delete(filePath);
405
496
  this.suppressedPaths.delete(newPath);
406
- }, 500);
497
+ }, 2000);
407
498
  return { ok: true, moved: true };
408
499
  }
409
500
  /** Approve a ghost idea — removes ghost:true from frontmatter and refreshes cache */
410
- approveGhostIdea(id) {
501
+ approveGhostIdea(slug) {
411
502
  const iceboxDir = path.join(this.scopesDir, 'icebox');
412
- const filePath = this.findIdeaFile(iceboxDir, id);
503
+ const filePath = this.findIdeaFile(iceboxDir, slug);
413
504
  if (!filePath)
414
505
  return false;
415
506
  const content = fs.readFileSync(filePath, 'utf-8');
@@ -418,7 +509,7 @@ export class ScopeService {
418
509
  fs.writeFileSync(filePath, updated, 'utf-8');
419
510
  // Re-parse file to refresh cache with is_ghost=false
420
511
  this.updateFromFile(filePath);
421
- log.info('Ghost approved', { id });
512
+ log.info('Ghost approved', { slug });
422
513
  return true;
423
514
  }
424
515
  }