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.
- package/README.md +67 -42
- package/bin/commands/config.js +19 -0
- package/bin/commands/events.js +40 -0
- package/bin/commands/launch.js +126 -0
- package/bin/commands/manifest.js +283 -0
- package/bin/commands/registry.js +104 -0
- package/bin/commands/update.js +24 -0
- package/bin/lib/helpers.js +229 -0
- package/bin/orbital.js +147 -319
- package/dist/assets/Landing-CfQdHR0N.js +11 -0
- package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
- package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
- package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
- package/dist/assets/Settings-DLcZwbCT.js +12 -0
- package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
- package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
- package/dist/assets/arrow-down-DVPp6_qp.js +6 -0
- package/dist/assets/bot-NFaJBDn_.js +6 -0
- package/dist/assets/charts-LGLb8hyU.js +68 -0
- package/dist/assets/circle-x-IsFCkBZu.js +6 -0
- package/dist/assets/file-text-J1cebZXF.js +6 -0
- package/dist/assets/globe-WzeyHsUc.js +6 -0
- package/dist/assets/index-BdJ57EhC.css +1 -0
- package/dist/assets/index-o4ScMAuR.js +349 -0
- package/dist/assets/key-CKR8JJSj.js +6 -0
- package/dist/assets/minus-CHBsJyjp.js +6 -0
- package/dist/assets/radio-xqZaR-Uk.js +6 -0
- package/dist/assets/rocket-D_xvvNG6.js +6 -0
- package/dist/assets/shield-TdB1yv_a.js +6 -0
- package/dist/assets/ui-BmsSg9jU.js +53 -0
- package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
- package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
- package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
- package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
- package/dist/assets/zap-C9wqYMpl.js +6 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +6 -5
- package/dist/server/server/__tests__/data-routes.test.js +126 -0
- package/dist/server/server/__tests__/helpers/db.js +17 -0
- package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
- package/dist/server/server/__tests__/scope-routes.test.js +138 -0
- package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
- package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
- package/dist/server/server/config-migrator.js +135 -0
- package/dist/server/server/config.js +51 -7
- package/dist/server/server/database.js +21 -28
- package/dist/server/server/global-config.js +143 -0
- package/dist/server/server/index.js +118 -276
- package/dist/server/server/init.js +243 -225
- package/dist/server/server/launch.js +29 -0
- package/dist/server/server/manifest-types.js +8 -0
- package/dist/server/server/manifest.js +454 -0
- package/dist/server/server/migrate-legacy.js +229 -0
- package/dist/server/server/parsers/event-parser.js +4 -1
- package/dist/server/server/parsers/event-parser.test.js +117 -0
- package/dist/server/server/parsers/scope-parser.js +74 -28
- package/dist/server/server/parsers/scope-parser.test.js +230 -0
- package/dist/server/server/project-context.js +265 -0
- package/dist/server/server/project-emitter.js +41 -0
- package/dist/server/server/project-manager.js +297 -0
- package/dist/server/server/routes/aggregate-routes.js +871 -0
- package/dist/server/server/routes/config-routes.js +41 -90
- package/dist/server/server/routes/data-routes.js +25 -123
- package/dist/server/server/routes/dispatch-routes.js +37 -15
- package/dist/server/server/routes/git-routes.js +74 -0
- package/dist/server/server/routes/manifest-routes.js +319 -0
- package/dist/server/server/routes/scope-routes.js +45 -28
- package/dist/server/server/routes/sync-routes.js +134 -0
- package/dist/server/server/routes/version-routes.js +1 -15
- package/dist/server/server/routes/workflow-routes.js +9 -3
- package/dist/server/server/schema.js +3 -0
- package/dist/server/server/services/batch-orchestrator.js +41 -17
- package/dist/server/server/services/claude-session-service.js +17 -14
- package/dist/server/server/services/config-service.js +10 -1
- package/dist/server/server/services/deploy-service.test.js +119 -0
- package/dist/server/server/services/event-service.js +64 -1
- package/dist/server/server/services/event-service.test.js +191 -0
- package/dist/server/server/services/gate-service.test.js +105 -0
- package/dist/server/server/services/git-service.js +108 -4
- package/dist/server/server/services/github-service.js +110 -2
- package/dist/server/server/services/readiness-service.test.js +190 -0
- package/dist/server/server/services/scope-cache.js +5 -1
- package/dist/server/server/services/scope-cache.test.js +142 -0
- package/dist/server/server/services/scope-service.js +222 -131
- package/dist/server/server/services/scope-service.test.js +137 -0
- package/dist/server/server/services/sprint-orchestrator.js +29 -15
- package/dist/server/server/services/sprint-service.js +23 -3
- package/dist/server/server/services/sprint-service.test.js +238 -0
- package/dist/server/server/services/sync-service.js +434 -0
- package/dist/server/server/services/sync-types.js +2 -0
- package/dist/server/server/services/workflow-service.js +26 -5
- package/dist/server/server/services/workflow-service.test.js +159 -0
- package/dist/server/server/settings-sync.js +284 -0
- package/dist/server/server/uninstall.js +195 -0
- package/dist/server/server/update-planner.js +279 -0
- package/dist/server/server/update.js +212 -0
- package/dist/server/server/utils/cc-hooks-parser.js +3 -0
- package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
- package/dist/server/server/utils/dispatch-utils.js +83 -24
- package/dist/server/server/utils/dispatch-utils.test.js +182 -0
- package/dist/server/server/utils/flag-builder.js +54 -0
- package/dist/server/server/utils/json-fields.js +14 -0
- package/dist/server/server/utils/json-fields.test.js +73 -0
- package/dist/server/server/utils/logger.js +37 -3
- package/dist/server/server/utils/package-info.js +30 -0
- package/dist/server/server/utils/route-helpers.js +47 -0
- package/dist/server/server/utils/route-helpers.test.js +115 -0
- package/dist/server/server/utils/terminal-launcher.js +79 -25
- package/dist/server/server/utils/worktree-manager.js +13 -4
- package/dist/server/server/validator.js +230 -0
- package/dist/server/server/watchers/event-watcher.js +28 -13
- package/dist/server/server/watchers/global-watcher.js +63 -0
- package/dist/server/server/watchers/scope-watcher.js +27 -12
- package/dist/server/server/wizard/config-editor.js +237 -0
- package/dist/server/server/wizard/detect.js +96 -0
- package/dist/server/server/wizard/doctor.js +115 -0
- package/dist/server/server/wizard/index.js +340 -0
- package/dist/server/server/wizard/phases/confirm.js +39 -0
- package/dist/server/server/wizard/phases/project-setup.js +90 -0
- package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
- package/dist/server/server/wizard/phases/welcome.js +32 -0
- package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
- package/dist/server/server/wizard/types.js +29 -0
- package/dist/server/server/wizard/ui.js +73 -0
- package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
- package/dist/server/shared/api-types.js +80 -1
- package/dist/server/shared/default-workflow.json +65 -0
- package/dist/server/shared/onboarding-tour.test.js +81 -0
- package/dist/server/shared/project-colors.js +24 -0
- package/dist/server/shared/workflow-config.test.js +84 -0
- package/dist/server/shared/workflow-engine.js +1 -1
- package/dist/server/shared/workflow-engine.test.js +302 -0
- package/dist/server/shared/workflow-normalizer.js +101 -0
- package/dist/server/shared/workflow-normalizer.test.js +100 -0
- package/dist/server/src/components/onboarding/tour-steps.js +84 -0
- package/package.json +34 -29
- package/schemas/orbital.config.schema.json +2 -5
- package/scripts/postinstall.js +18 -6
- package/scripts/release.sh +53 -0
- package/server/__tests__/data-routes.test.ts +151 -0
- package/server/__tests__/helpers/db.ts +19 -0
- package/server/__tests__/helpers/mock-emitter.ts +10 -0
- package/server/__tests__/scope-routes.test.ts +158 -0
- package/server/__tests__/sprint-routes.test.ts +118 -0
- package/server/__tests__/workflow-routes.test.ts +120 -0
- package/server/config-migrator.ts +160 -0
- package/server/config.ts +64 -12
- package/server/database.ts +22 -31
- package/server/global-config.ts +204 -0
- package/server/index.ts +139 -316
- package/server/init.ts +266 -234
- package/server/launch.ts +32 -0
- package/server/manifest-types.ts +145 -0
- package/server/manifest.ts +494 -0
- package/server/migrate-legacy.ts +290 -0
- package/server/parsers/event-parser.test.ts +135 -0
- package/server/parsers/event-parser.ts +4 -1
- package/server/parsers/scope-parser.test.ts +270 -0
- package/server/parsers/scope-parser.ts +79 -31
- package/server/project-context.ts +325 -0
- package/server/project-emitter.ts +50 -0
- package/server/project-manager.ts +368 -0
- package/server/routes/aggregate-routes.ts +968 -0
- package/server/routes/config-routes.ts +43 -85
- package/server/routes/data-routes.ts +34 -156
- package/server/routes/dispatch-routes.ts +46 -17
- package/server/routes/git-routes.ts +77 -0
- package/server/routes/manifest-routes.ts +388 -0
- package/server/routes/scope-routes.ts +39 -30
- package/server/routes/sync-routes.ts +175 -0
- package/server/routes/version-routes.ts +1 -16
- package/server/routes/workflow-routes.ts +9 -3
- package/server/schema.ts +3 -0
- package/server/services/batch-orchestrator.ts +41 -17
- package/server/services/claude-session-service.ts +16 -14
- package/server/services/config-service.ts +10 -1
- package/server/services/deploy-service.test.ts +145 -0
- package/server/services/deploy-service.ts +2 -2
- package/server/services/event-service.test.ts +242 -0
- package/server/services/event-service.ts +92 -3
- package/server/services/gate-service.test.ts +131 -0
- package/server/services/gate-service.ts +2 -2
- package/server/services/git-service.ts +137 -4
- package/server/services/github-service.ts +120 -2
- package/server/services/readiness-service.test.ts +217 -0
- package/server/services/scope-cache.test.ts +167 -0
- package/server/services/scope-cache.ts +4 -1
- package/server/services/scope-service.test.ts +169 -0
- package/server/services/scope-service.ts +224 -130
- package/server/services/sprint-orchestrator.ts +30 -15
- package/server/services/sprint-service.test.ts +271 -0
- package/server/services/sprint-service.ts +29 -5
- package/server/services/sync-service.ts +482 -0
- package/server/services/sync-types.ts +77 -0
- package/server/services/workflow-service.test.ts +190 -0
- package/server/services/workflow-service.ts +29 -9
- package/server/settings-sync.ts +359 -0
- package/server/uninstall.ts +214 -0
- package/server/update-planner.ts +346 -0
- package/server/update.ts +263 -0
- package/server/utils/cc-hooks-parser.test.ts +96 -0
- package/server/utils/cc-hooks-parser.ts +4 -0
- package/server/utils/dispatch-utils.test.ts +245 -0
- package/server/utils/dispatch-utils.ts +102 -30
- package/server/utils/flag-builder.ts +56 -0
- package/server/utils/json-fields.test.ts +83 -0
- package/server/utils/json-fields.ts +14 -0
- package/server/utils/logger.ts +40 -3
- package/server/utils/package-info.ts +32 -0
- package/server/utils/route-helpers.test.ts +144 -0
- package/server/utils/route-helpers.ts +50 -0
- package/server/utils/terminal-launcher.ts +85 -25
- package/server/utils/worktree-manager.ts +9 -4
- package/server/validator.ts +270 -0
- package/server/watchers/event-watcher.ts +24 -12
- package/server/watchers/global-watcher.ts +77 -0
- package/server/watchers/scope-watcher.ts +21 -9
- package/server/wizard/config-editor.ts +248 -0
- package/server/wizard/detect.ts +104 -0
- package/server/wizard/doctor.ts +114 -0
- package/server/wizard/index.ts +438 -0
- package/server/wizard/phases/confirm.ts +45 -0
- package/server/wizard/phases/project-setup.ts +106 -0
- package/server/wizard/phases/setup-wizard.ts +78 -0
- package/server/wizard/phases/welcome.ts +39 -0
- package/server/wizard/phases/workflow-setup.ts +28 -0
- package/server/wizard/types.ts +56 -0
- package/server/wizard/ui.ts +92 -0
- package/shared/__fixtures__/workflow-configs.ts +80 -0
- package/shared/api-types.ts +106 -0
- package/shared/onboarding-tour.test.ts +94 -0
- package/shared/project-colors.ts +24 -0
- package/shared/workflow-config.test.ts +111 -0
- package/shared/workflow-config.ts +7 -0
- package/shared/workflow-engine.test.ts +388 -0
- package/shared/workflow-engine.ts +1 -1
- package/shared/workflow-normalizer.test.ts +119 -0
- package/shared/workflow-normalizer.ts +118 -0
- package/templates/agents/QUICK-REFERENCE.md +1 -0
- package/templates/agents/README.md +1 -0
- package/templates/agents/SKILL-TRIGGERS.md +11 -0
- package/templates/agents/green-team/deep-dive.md +361 -0
- package/templates/hooks/end-session.sh +4 -1
- package/templates/hooks/init-session.sh +1 -0
- package/templates/hooks/orbital-emit.sh +2 -2
- package/templates/hooks/orbital-report-deploy.sh +4 -4
- package/templates/hooks/orbital-report-gates.sh +4 -4
- package/templates/hooks/orbital-scope-update.sh +1 -1
- package/templates/hooks/scope-commit-logger.sh +2 -2
- package/templates/hooks/scope-create-cleanup.sh +2 -2
- package/templates/hooks/scope-create-gate.sh +2 -5
- package/templates/hooks/scope-gate.sh +4 -6
- package/templates/hooks/scope-helpers.sh +28 -1
- package/templates/hooks/scope-lifecycle-gate.sh +14 -5
- package/templates/hooks/scope-prepare.sh +67 -12
- package/templates/hooks/scope-transition.sh +14 -6
- package/templates/hooks/time-tracker.sh +2 -5
- package/templates/migrations/renames.json +1 -0
- package/templates/orbital.config.json +8 -6
- package/{shared/default-workflow.json → templates/presets/default.json} +65 -0
- package/templates/presets/development.json +4 -4
- package/templates/presets/gitflow.json +7 -0
- package/templates/prompts/README.md +23 -0
- package/templates/prompts/deep-dive-audit.md +94 -0
- package/templates/quick/rules.md +56 -5
- package/templates/settings-hooks.json +1 -1
- package/templates/skills/git-commit/SKILL.md +27 -7
- package/templates/skills/git-dev/SKILL.md +13 -4
- package/templates/skills/git-main/SKILL.md +13 -3
- package/templates/skills/git-production/SKILL.md +9 -2
- package/templates/skills/git-staging/SKILL.md +11 -3
- package/templates/skills/scope-create/SKILL.md +17 -3
- package/templates/skills/scope-fix-review/SKILL.md +14 -7
- package/templates/skills/scope-implement/SKILL.md +15 -4
- package/templates/skills/scope-post-review/SKILL.md +77 -7
- package/templates/skills/scope-pre-review/SKILL.md +11 -4
- package/templates/skills/scope-verify/SKILL.md +5 -3
- package/templates/skills/test-code-review/SKILL.md +41 -33
- package/templates/skills/test-scaffold/SKILL.md +222 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
- package/dist/assets/charts-D__PA1zp.js +0 -72
- package/dist/assets/index-D1G6i0nS.css +0 -1
- package/dist/assets/index-DpItvKpf.js +0 -419
- package/dist/assets/ui-BvF022GT.js +0 -53
- package/index.html +0 -15
- package/postcss.config.js +0 -6
- package/src/App.tsx +0 -33
- package/src/components/AgentBadge.tsx +0 -40
- package/src/components/BatchPreflightModal.tsx +0 -115
- package/src/components/CardDisplayToggle.tsx +0 -74
- package/src/components/ColumnHeaderActions.tsx +0 -55
- package/src/components/ColumnMenu.tsx +0 -99
- package/src/components/DeployHistory.tsx +0 -141
- package/src/components/DispatchModal.tsx +0 -164
- package/src/components/DispatchPopover.tsx +0 -139
- package/src/components/DragOverlay.tsx +0 -25
- package/src/components/DriftSidebar.tsx +0 -140
- package/src/components/EnvironmentStrip.tsx +0 -88
- package/src/components/ErrorBoundary.tsx +0 -62
- package/src/components/FilterChip.tsx +0 -105
- package/src/components/GateIndicator.tsx +0 -33
- package/src/components/IdeaDetailModal.tsx +0 -190
- package/src/components/IdeaFormDialog.tsx +0 -113
- package/src/components/KanbanColumn.tsx +0 -201
- package/src/components/MarkdownRenderer.tsx +0 -114
- package/src/components/NeonGrid.tsx +0 -128
- package/src/components/PromotionQueue.tsx +0 -89
- package/src/components/ScopeCard.tsx +0 -234
- package/src/components/ScopeDetailModal.tsx +0 -255
- package/src/components/ScopeFilterBar.tsx +0 -152
- package/src/components/SearchInput.tsx +0 -102
- package/src/components/SessionPanel.tsx +0 -335
- package/src/components/SprintContainer.tsx +0 -303
- package/src/components/SprintDependencyDialog.tsx +0 -78
- package/src/components/SprintPreflightModal.tsx +0 -138
- package/src/components/StatusBar.tsx +0 -168
- package/src/components/SwimCell.tsx +0 -67
- package/src/components/SwimLaneRow.tsx +0 -94
- package/src/components/SwimlaneBoardView.tsx +0 -108
- package/src/components/VersionBadge.tsx +0 -139
- package/src/components/ViewModeSelector.tsx +0 -114
- package/src/components/config/AgentChip.tsx +0 -53
- package/src/components/config/AgentCreateDialog.tsx +0 -321
- package/src/components/config/AgentEditor.tsx +0 -175
- package/src/components/config/DirectoryTree.tsx +0 -582
- package/src/components/config/FileEditor.tsx +0 -550
- package/src/components/config/HookChip.tsx +0 -50
- package/src/components/config/StageCard.tsx +0 -198
- package/src/components/config/TransitionZone.tsx +0 -173
- package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
- package/src/components/config/WorkflowPipeline.tsx +0 -161
- package/src/components/source-control/BranchList.tsx +0 -93
- package/src/components/source-control/BranchPanel.tsx +0 -105
- package/src/components/source-control/CommitLog.tsx +0 -100
- package/src/components/source-control/CommitRow.tsx +0 -47
- package/src/components/source-control/GitHubPanel.tsx +0 -110
- package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
- package/src/components/source-control/GitOverviewBar.tsx +0 -101
- package/src/components/source-control/PullRequestList.tsx +0 -69
- package/src/components/source-control/WorktreeList.tsx +0 -80
- package/src/components/ui/badge.tsx +0 -41
- package/src/components/ui/button.tsx +0 -55
- package/src/components/ui/card.tsx +0 -78
- package/src/components/ui/dialog.tsx +0 -94
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -52
- package/src/components/ui/toggle-switch.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -27
- package/src/components/workflow/AddEdgeDialog.tsx +0 -217
- package/src/components/workflow/AddListDialog.tsx +0 -201
- package/src/components/workflow/ChecklistEditor.tsx +0 -239
- package/src/components/workflow/CommandPrefixManager.tsx +0 -118
- package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
- package/src/components/workflow/DirectionSelector.tsx +0 -133
- package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
- package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
- package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
- package/src/components/workflow/EditToolbar.tsx +0 -138
- package/src/components/workflow/HookDetailPanel.tsx +0 -250
- package/src/components/workflow/HookExecutionLog.tsx +0 -24
- package/src/components/workflow/HookSourceModal.tsx +0 -129
- package/src/components/workflow/HooksDashboard.tsx +0 -363
- package/src/components/workflow/ListPropertyEditor.tsx +0 -251
- package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
- package/src/components/workflow/MovementRulesPanel.tsx +0 -188
- package/src/components/workflow/NodeDetailPanel.tsx +0 -245
- package/src/components/workflow/PresetSelector.tsx +0 -414
- package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
- package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
- package/src/components/workflow/WorkflowNode.tsx +0 -147
- package/src/components/workflow/graphLayout.ts +0 -186
- package/src/components/workflow/mergeHooks.ts +0 -85
- package/src/components/workflow/useEditHistory.ts +0 -88
- package/src/components/workflow/useWorkflowEditor.ts +0 -262
- package/src/components/workflow/validateConfig.ts +0 -70
- package/src/hooks/useActiveDispatches.ts +0 -198
- package/src/hooks/useBoardSettings.ts +0 -170
- package/src/hooks/useCardDisplay.ts +0 -57
- package/src/hooks/useCcHooks.ts +0 -24
- package/src/hooks/useConfigTree.ts +0 -51
- package/src/hooks/useEnforcementRules.ts +0 -46
- package/src/hooks/useEvents.ts +0 -59
- package/src/hooks/useFileEditor.ts +0 -165
- package/src/hooks/useGates.ts +0 -57
- package/src/hooks/useIdeaActions.ts +0 -53
- package/src/hooks/useKanbanDnd.ts +0 -410
- package/src/hooks/useOrbitalConfig.ts +0 -54
- package/src/hooks/usePipeline.ts +0 -47
- package/src/hooks/usePipelineData.ts +0 -338
- package/src/hooks/useReconnect.ts +0 -25
- package/src/hooks/useScopeFilters.ts +0 -125
- package/src/hooks/useScopeSessions.ts +0 -44
- package/src/hooks/useScopes.ts +0 -67
- package/src/hooks/useSearch.ts +0 -67
- package/src/hooks/useSettings.tsx +0 -187
- package/src/hooks/useSocket.ts +0 -25
- package/src/hooks/useSourceControl.ts +0 -105
- package/src/hooks/useSprintPreflight.ts +0 -55
- package/src/hooks/useSprints.ts +0 -154
- package/src/hooks/useStatusBarHighlight.ts +0 -18
- package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
- package/src/hooks/useTheme.ts +0 -9
- package/src/hooks/useTransitionReadiness.ts +0 -53
- package/src/hooks/useVersion.ts +0 -155
- package/src/hooks/useViolations.ts +0 -65
- package/src/hooks/useWorkflow.tsx +0 -125
- package/src/hooks/useZoomModifier.ts +0 -19
- package/src/index.css +0 -797
- package/src/layouts/DashboardLayout.tsx +0 -113
- package/src/lib/collisionDetection.ts +0 -20
- package/src/lib/scope-fields.ts +0 -61
- package/src/lib/swimlane.ts +0 -146
- package/src/lib/utils.ts +0 -15
- package/src/main.tsx +0 -19
- package/src/socket.ts +0 -11
- package/src/types/index.ts +0 -497
- package/src/views/AgentFeed.tsx +0 -339
- package/src/views/DeployPipeline.tsx +0 -59
- package/src/views/EnforcementView.tsx +0 -378
- package/src/views/PrimitivesConfig.tsx +0 -500
- package/src/views/QualityGates.tsx +0 -1012
- package/src/views/ScopeBoard.tsx +0 -454
- package/src/views/SessionTimeline.tsx +0 -516
- package/src/views/Settings.tsx +0 -183
- package/src/views/SourceControl.tsx +0 -95
- package/src/views/WorkflowVisualizer.tsx +0 -382
- package/tailwind.config.js +0 -161
- package/tsconfig.json +0 -25
- 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 {
|
|
4
|
+
import type { Emitter } from '../project-emitter.js';
|
|
5
5
|
import type { ParsedScope } from '../parsers/scope-parser.js';
|
|
6
|
-
import {
|
|
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:
|
|
23
|
+
private io: Emitter,
|
|
24
24
|
private scopesDir: string,
|
|
25
25
|
private engine: WorkflowEngine,
|
|
26
26
|
) {}
|
|
@@ -46,6 +46,42 @@ export class ScopeService {
|
|
|
46
46
|
return scopes.length;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Reconcile files whose directory doesn't match their frontmatter status.
|
|
50
|
+
* Frontmatter is the authoritative source — files are moved to match it.
|
|
51
|
+
* Called once at startup after syncFromFilesystem(). */
|
|
52
|
+
reconcileDirectories(): number {
|
|
53
|
+
let moved = 0;
|
|
54
|
+
for (const scope of this.cache.getAll()) {
|
|
55
|
+
if (scope.id < 0) continue; // slug-only icebox items (negative IDs)
|
|
56
|
+
const currentDir = path.basename(path.dirname(scope.file_path));
|
|
57
|
+
if (currentDir === scope.status) continue;
|
|
58
|
+
if (!this.engine.isValidStatus(scope.status)) continue;
|
|
59
|
+
|
|
60
|
+
const targetDir = path.join(this.scopesDir, scope.status);
|
|
61
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
62
|
+
const newPath = path.join(targetDir, path.basename(scope.file_path));
|
|
63
|
+
|
|
64
|
+
this.suppressedPaths.add(scope.file_path);
|
|
65
|
+
this.suppressedPaths.add(newPath);
|
|
66
|
+
try {
|
|
67
|
+
fs.renameSync(scope.file_path, newPath);
|
|
68
|
+
this.updateFromFile(newPath);
|
|
69
|
+
this.removeByFilePath(scope.file_path);
|
|
70
|
+
moved++;
|
|
71
|
+
log.warn('Reconciled directory mismatch', {
|
|
72
|
+
id: scope.id, frontmatter: scope.status, directory: currentDir,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log.error('Failed to reconcile scope directory', { id: scope.id, error: String(err) });
|
|
76
|
+
}
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
this.suppressedPaths.delete(scope.file_path);
|
|
79
|
+
this.suppressedPaths.delete(newPath);
|
|
80
|
+
}, 2000);
|
|
81
|
+
}
|
|
82
|
+
return moved;
|
|
83
|
+
}
|
|
84
|
+
|
|
49
85
|
/** Check if a path is suppressed from watcher processing (during programmatic moves) */
|
|
50
86
|
isSuppressed(filePath: string): boolean {
|
|
51
87
|
return this.suppressedPaths.has(filePath);
|
|
@@ -84,7 +120,7 @@ export class ScopeService {
|
|
|
84
120
|
const id = this.cache.removeByFilePath(filePath);
|
|
85
121
|
if (id !== undefined) {
|
|
86
122
|
if (previous) this.recentlyRemoved.set(id, previous.status);
|
|
87
|
-
this.io.emit('scope:deleted', id);
|
|
123
|
+
this.io.emit('scope:deleted', { id });
|
|
88
124
|
// Clean up stash after a short window (if add never fires, this was a real delete)
|
|
89
125
|
setTimeout(() => this.recentlyRemoved.delete(id), 5000);
|
|
90
126
|
}
|
|
@@ -102,6 +138,7 @@ export class ScopeService {
|
|
|
102
138
|
|
|
103
139
|
/** Update a scope's status with transition validation.
|
|
104
140
|
* Writes the new status to the frontmatter file and updates the cache.
|
|
141
|
+
* This is the SINGLE validation point — all status changes must flow through here.
|
|
105
142
|
* @param context - caller trust level: 'patch', 'dispatch', 'event', 'bulk-sync', 'rollback' */
|
|
106
143
|
updateStatus(
|
|
107
144
|
id: number,
|
|
@@ -131,12 +168,11 @@ export class ScopeService {
|
|
|
131
168
|
if (!check.ok) return check;
|
|
132
169
|
}
|
|
133
170
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
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)
|
|
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
|
-
/**
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
/**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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.
|
|
199
|
-
createIdeaFile(title: string, description: 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
|
|
280
|
+
const slug = this.slugify(title);
|
|
281
|
+
let fileName = `${slug}.md`;
|
|
282
|
+
let filePath = path.join(iceboxDir, fileName);
|
|
204
283
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.
|
|
209
|
-
.
|
|
210
|
-
|
|
211
|
-
|
|
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', {
|
|
235
|
-
return {
|
|
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
|
|
239
|
-
updateIdeaFile(
|
|
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,
|
|
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
|
|
247
|
-
const created =
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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(
|
|
368
|
+
deleteIdeaFile(slug: string): boolean {
|
|
273
369
|
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
274
|
-
const filePath = this.findIdeaFile(iceboxDir,
|
|
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', {
|
|
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(
|
|
382
|
+
promoteIdea(slug: string, targetStatus = 'planning'): { id: number; filePath: string; title: string; description: string } | null {
|
|
287
383
|
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
288
|
-
const oldPath = this.findIdeaFile(iceboxDir,
|
|
384
|
+
const oldPath = this.findIdeaFile(iceboxDir, slug);
|
|
289
385
|
if (!oldPath) return null;
|
|
290
386
|
|
|
291
387
|
// Read existing file for metadata
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
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
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const planningDir = path.join(this.scopesDir, 'planning');
|
|
313
|
-
if (!fs.existsSync(planningDir)) fs.mkdirSync(planningDir, { recursive: true });
|
|
314
|
-
const newFileName = `${paddedId}-${slug}.md`;
|
|
315
|
-
const newPath = path.join(planningDir, newFileName);
|
|
398
|
+
// Build new path
|
|
399
|
+
const titleSlug = this.slugify(title);
|
|
400
|
+
const targetDir = path.join(this.scopesDir, targetStatus);
|
|
401
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
402
|
+
const newFileName = `${paddedId}-${titleSlug}.md`;
|
|
403
|
+
const newPath = path.join(targetDir, newFileName);
|
|
316
404
|
const now = new Date().toISOString().split('T')[0];
|
|
317
405
|
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
`updated: ${now}`,
|
|
326
|
-
'blocked_by: []',
|
|
327
|
-
'blocks: []',
|
|
328
|
-
'tags: []',
|
|
329
|
-
'---',
|
|
330
|
-
'',
|
|
331
|
-
description || '',
|
|
332
|
-
'',
|
|
333
|
-
].join('\n');
|
|
406
|
+
// Update frontmatter in-place: assign ID and change status (preserve other fields)
|
|
407
|
+
parsed.data.id = newId;
|
|
408
|
+
parsed.data.status = targetStatus;
|
|
409
|
+
parsed.data.updated = now;
|
|
410
|
+
parsed.data.created = created;
|
|
411
|
+
delete parsed.data.ghost;
|
|
412
|
+
this.normalizeFrontmatterDates(parsed.data);
|
|
334
413
|
|
|
335
|
-
|
|
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
|
-
//
|
|
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', {
|
|
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
|
-
/**
|
|
366
|
-
* If status
|
|
367
|
-
*
|
|
368
|
-
|
|
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
|
-
//
|
|
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
|
|
385
|
-
const
|
|
386
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
433
|
-
const targetDir = path.join(this.scopesDir,
|
|
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
|
-
},
|
|
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(
|
|
554
|
+
approveGhostIdea(slug: string): boolean {
|
|
461
555
|
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
462
|
-
const filePath = this.findIdeaFile(iceboxDir,
|
|
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', {
|
|
566
|
+
log.info('Ghost approved', { slug });
|
|
473
567
|
|
|
474
568
|
return true;
|
|
475
569
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type Database from 'better-sqlite3';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Emitter } from '../project-emitter.js';
|
|
3
3
|
import { SprintService } from './sprint-service.js';
|
|
4
4
|
import { ScopeService } from './scope-service.js';
|
|
5
|
-
import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
5
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, shellQuote, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
6
6
|
import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
7
|
+
import { buildClaudeFlags, buildEnvVarPrefix } from '../utils/flag-builder.js';
|
|
7
8
|
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
|
-
import {
|
|
9
|
+
import type { OrbitalConfig } from '../config.js';
|
|
9
10
|
import { createLogger } from '../utils/logger.js';
|
|
10
11
|
|
|
11
12
|
const log = createLogger('sprint');
|
|
@@ -20,10 +21,12 @@ function sleep(ms: number): Promise<void> {
|
|
|
20
21
|
export class SprintOrchestrator {
|
|
21
22
|
constructor(
|
|
22
23
|
private db: Database.Database,
|
|
23
|
-
private io:
|
|
24
|
+
private io: Emitter,
|
|
24
25
|
private sprintService: SprintService,
|
|
25
26
|
private scopeService: ScopeService,
|
|
26
27
|
private engine: WorkflowEngine,
|
|
28
|
+
private projectRoot: string,
|
|
29
|
+
private config: OrbitalConfig,
|
|
27
30
|
) {}
|
|
28
31
|
|
|
29
32
|
/** Build execution layers using Kahn's topological sort */
|
|
@@ -112,14 +115,16 @@ export class SprintOrchestrator {
|
|
|
112
115
|
async onScopeReachedDev(scopeId: number): Promise<void> {
|
|
113
116
|
const match = this.sprintService.findActiveSprintForScope(scopeId);
|
|
114
117
|
if (!match) return;
|
|
115
|
-
log.debug('Scope reached dev', { scopeId, sprintId: match.sprint_id });
|
|
116
118
|
|
|
119
|
+
// Batches are managed by BatchOrchestrator — don't dispatch individual scopes
|
|
117
120
|
const sprintId = match.sprint_id;
|
|
121
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
122
|
+
if (!sprint || sprint.group_type === 'batch') return;
|
|
123
|
+
|
|
124
|
+
log.debug('Scope reached dev', { scopeId, sprintId });
|
|
118
125
|
this.sprintService.updateScopeStatus(sprintId, scopeId, 'completed');
|
|
119
126
|
|
|
120
127
|
// Ensure sprint is in 'in_progress' state
|
|
121
|
-
const sprint = this.sprintService.getById(sprintId);
|
|
122
|
-
if (!sprint) return;
|
|
123
128
|
if (sprint.status === 'dispatched') {
|
|
124
129
|
this.sprintService.updateStatus(sprintId, 'in_progress');
|
|
125
130
|
}
|
|
@@ -196,7 +201,7 @@ export class SprintOrchestrator {
|
|
|
196
201
|
const sprint = this.sprintService.getById(sprintId);
|
|
197
202
|
if (!sprint) return null;
|
|
198
203
|
|
|
199
|
-
const layers = sprint.layers ??
|
|
204
|
+
const layers = sprint.layers ?? this.buildExecutionLayers(sprint.scope_ids).layers;
|
|
200
205
|
const sprintSet = new Set(sprint.scope_ids);
|
|
201
206
|
const edges: Array<{ from: number; to: number }> = [];
|
|
202
207
|
|
|
@@ -225,9 +230,15 @@ export class SprintOrchestrator {
|
|
|
225
230
|
const currentScope = this.scopeService.getById(scopeId);
|
|
226
231
|
const previousStatus = currentScope?.status ?? 'implementing';
|
|
227
232
|
|
|
233
|
+
// Resolve command and target status from workflow engine
|
|
234
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
235
|
+
const targetColumn = sprint?.target_column ?? 'backlog';
|
|
236
|
+
const edgeCommand = this.engine.getBatchCommand(targetColumn);
|
|
237
|
+
const targetStatus = this.engine.getBatchTargetStatus(targetColumn);
|
|
238
|
+
|
|
228
239
|
// Record DISPATCH event
|
|
229
240
|
const eventId = crypto.randomUUID();
|
|
230
|
-
const command = `/scope
|
|
241
|
+
const command = edgeCommand ?? `/scope-implement ${scopeId}`;
|
|
231
242
|
this.db.prepare(
|
|
232
243
|
`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
233
244
|
VALUES (?, 'DISPATCH', ?, NULL, 'sprint-orchestrator', ?, ?)`,
|
|
@@ -241,26 +252,30 @@ export class SprintOrchestrator {
|
|
|
241
252
|
});
|
|
242
253
|
|
|
243
254
|
// Update scope + sprint_scope status
|
|
244
|
-
|
|
255
|
+
if (targetStatus) {
|
|
256
|
+
this.scopeService.updateStatus(scopeId, targetStatus, 'dispatch');
|
|
257
|
+
}
|
|
245
258
|
this.sprintService.updateScopeStatus(sprintId, scopeId, 'dispatched');
|
|
246
259
|
|
|
247
260
|
// Build scope-aware session name and snapshot PIDs
|
|
248
261
|
const scopeRow = this.scopeService.getById(scopeId);
|
|
249
262
|
const sessionName = buildSessionName({ scopeId, title: scopeRow?.title, command });
|
|
250
|
-
const beforePids = snapshotSessionPids(
|
|
263
|
+
const beforePids = snapshotSessionPids(this.projectRoot);
|
|
251
264
|
|
|
252
|
-
// Launch in iTerm — interactive TUI mode
|
|
265
|
+
// Launch in iTerm — interactive TUI mode for full visibility
|
|
253
266
|
const escaped = escapeForAnsiC(command);
|
|
254
|
-
const
|
|
267
|
+
const flagsStr = buildClaudeFlags(this.config.claude.dispatchFlags);
|
|
268
|
+
const envPrefix = buildEnvVarPrefix(this.config.dispatch.envVars);
|
|
269
|
+
const fullCmd = `cd '${shellQuote(this.projectRoot)}' && ${envPrefix}ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude ${flagsStr} $'${escaped}'`;
|
|
255
270
|
try {
|
|
256
271
|
await launchInCategorizedTerminal(command, fullCmd, sessionName);
|
|
257
272
|
|
|
258
273
|
// Fire-and-forget: discover session PID, link to dispatch, and rename
|
|
259
|
-
discoverNewSession(
|
|
274
|
+
discoverNewSession(this.projectRoot, beforePids)
|
|
260
275
|
.then((session) => {
|
|
261
276
|
if (!session) return;
|
|
262
277
|
linkPidToDispatch(this.db, eventId, session.pid);
|
|
263
|
-
if (sessionName) renameSession(
|
|
278
|
+
if (sessionName) renameSession(this.projectRoot, session.sessionId, sessionName);
|
|
264
279
|
})
|
|
265
280
|
.catch(err => log.error('PID discovery failed', { error: err.message }));
|
|
266
281
|
} catch (err) {
|