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,6 +1,6 @@
|
|
|
1
1
|
import { isSessionPidAlive } from './terminal-launcher.js';
|
|
2
2
|
import { createLogger } from './logger.js';
|
|
3
|
-
const log = createLogger('dispatch');
|
|
3
|
+
const log = createLogger('dispatch-utils');
|
|
4
4
|
/** Mark a DISPATCH event as resolved and emit socket notification. */
|
|
5
5
|
export function resolveDispatchEvent(db, io, eventId, outcome, error) {
|
|
6
6
|
const row = db.prepare('SELECT data, scope_id FROM events WHERE id = ?')
|
|
@@ -24,6 +24,58 @@ export function resolveDispatchEvent(db, io, eventId, outcome, error) {
|
|
|
24
24
|
outcome,
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
+
/** Auto-revert scope status when a dispatch is abandoned, if the forward edge
|
|
28
|
+
* has autoRevert=true and the scope is still at the dispatch target.
|
|
29
|
+
* Safe: only reverts if the scope hasn't been moved since the dispatch.
|
|
30
|
+
* Returns true if revert was successful. */
|
|
31
|
+
function autoRevertAbandonedScope(scopeService, engine, scopeId, data) {
|
|
32
|
+
try {
|
|
33
|
+
const transition = data.transition;
|
|
34
|
+
if (!transition?.from || !transition?.to)
|
|
35
|
+
return false;
|
|
36
|
+
const scope = scopeService.getById(scopeId);
|
|
37
|
+
// Only revert if scope is still at the dispatch target (hasn't been moved)
|
|
38
|
+
if (!scope || scope.status !== transition.to)
|
|
39
|
+
return false;
|
|
40
|
+
const edge = engine.findEdge(transition.from, transition.to);
|
|
41
|
+
if (!edge?.autoRevert)
|
|
42
|
+
return false;
|
|
43
|
+
const result = scopeService.updateStatus(scopeId, transition.from, 'rollback');
|
|
44
|
+
if (!result.ok)
|
|
45
|
+
return false;
|
|
46
|
+
log.info('Auto-reverted abandoned dispatch', {
|
|
47
|
+
scopeId, from: transition.to, to: transition.from,
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
log.error('Auto-revert failed', { scopeId, error: String(err) });
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Attempt auto-revert for an abandoned dispatch and clear the abandoned state if successful.
|
|
57
|
+
* Loads the dispatch event data, tries auto-revert, and re-resolves as 'completed' if the
|
|
58
|
+
* scope was successfully reverted. Returns true if auto-revert + clear succeeded. */
|
|
59
|
+
export function tryAutoRevertAndClear(db, io, scopeService, engine, eventId) {
|
|
60
|
+
const row = db.prepare('SELECT data, scope_id FROM events WHERE id = ?')
|
|
61
|
+
.get(eventId);
|
|
62
|
+
if (!row || row.scope_id == null)
|
|
63
|
+
return false;
|
|
64
|
+
let data;
|
|
65
|
+
try {
|
|
66
|
+
data = JSON.parse(row.data);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const reverted = autoRevertAbandonedScope(scopeService, engine, row.scope_id, data);
|
|
72
|
+
if (reverted) {
|
|
73
|
+
// Clear the abandoned state so getAbandonedScopeIds won't return this scope
|
|
74
|
+
resolveDispatchEvent(db, io, eventId, 'completed');
|
|
75
|
+
log.info('Cleared abandoned dispatch after auto-revert', { eventId, scope_id: row.scope_id });
|
|
76
|
+
}
|
|
77
|
+
return reverted;
|
|
78
|
+
}
|
|
27
79
|
/** Resolve all unresolved DISPATCH events for a given scope */
|
|
28
80
|
export function resolveActiveDispatchesForScope(db, io, scopeId, outcome) {
|
|
29
81
|
const rows = db.prepare(`SELECT id FROM events
|
|
@@ -65,43 +117,41 @@ export function linkPidToDispatch(db, eventId, pid) {
|
|
|
65
117
|
/** Resolve all unresolved DISPATCH events linked to a specific PID.
|
|
66
118
|
* Called when a SESSION_END event is received, indicating the Claude session
|
|
67
119
|
* process has exited and its dispatches should be cleared.
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
* keep scopes at the transition target (e.g. "implementing") after completion.
|
|
71
|
-
* Reverting on session end was destroying completed work and deleting scope files. */
|
|
72
|
-
export function resolveDispatchesByPid(db, io, pid) {
|
|
120
|
+
* Returns the resolved event IDs so callers can attempt auto-revert. */
|
|
121
|
+
export function resolveDispatchesByPid(db, io, pid, outcome = 'abandoned') {
|
|
73
122
|
const rows = db.prepare(`SELECT id FROM events
|
|
74
123
|
WHERE type = 'DISPATCH'
|
|
75
124
|
AND JSON_EXTRACT(data, '$.resolved') IS NULL
|
|
76
125
|
AND JSON_EXTRACT(data, '$.pid') = ?`).all(pid);
|
|
77
126
|
for (const row of rows) {
|
|
78
|
-
resolveDispatchEvent(db, io, row.id,
|
|
127
|
+
resolveDispatchEvent(db, io, row.id, outcome);
|
|
79
128
|
}
|
|
80
|
-
return rows.
|
|
129
|
+
return rows.map(r => r.id);
|
|
81
130
|
}
|
|
82
131
|
/** Resolve all unresolved DISPATCH events linked to a specific dispatch ID.
|
|
83
132
|
* Called when a SESSION_END event includes dispatch_id from ORBITAL_DISPATCH_ID env var.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
export function resolveDispatchesByDispatchId(db, io, dispatchId) {
|
|
133
|
+
* Outcome depends on how the session ended: normal_exit → completed, otherwise → abandoned.
|
|
134
|
+
* Returns the resolved event IDs so callers can attempt auto-revert. */
|
|
135
|
+
export function resolveDispatchesByDispatchId(db, io, dispatchId, outcome = 'abandoned') {
|
|
87
136
|
const row = db.prepare(`SELECT id FROM events
|
|
88
137
|
WHERE id = ? AND type = 'DISPATCH' AND JSON_EXTRACT(data, '$.resolved') IS NULL`).get(dispatchId);
|
|
89
138
|
if (!row)
|
|
90
|
-
return
|
|
91
|
-
resolveDispatchEvent(db, io, row.id,
|
|
92
|
-
return
|
|
139
|
+
return [];
|
|
140
|
+
resolveDispatchEvent(db, io, row.id, outcome);
|
|
141
|
+
return [row.id];
|
|
93
142
|
}
|
|
94
|
-
/**
|
|
95
|
-
const
|
|
143
|
+
/** Default fallback age threshold for dispatches without a linked PID (10 minutes). */
|
|
144
|
+
const DEFAULT_STALE_AGE_MS = 10 * 60 * 1000;
|
|
96
145
|
/** Get all scope IDs that have actively running DISPATCH events.
|
|
97
146
|
* Uses PID liveness (process.kill(pid, 0)) when available, falls back to
|
|
98
147
|
* age-based heuristic for legacy dispatches without a linked PID. */
|
|
99
|
-
export function getActiveScopeIds(db, scopeService, engine) {
|
|
148
|
+
export function getActiveScopeIds(db, scopeService, engine, staleTimeoutMinutes) {
|
|
100
149
|
const rows = db.prepare(`SELECT scope_id, data FROM events
|
|
101
150
|
WHERE type = 'DISPATCH'
|
|
102
151
|
AND scope_id IS NOT NULL
|
|
103
152
|
AND JSON_EXTRACT(data, '$.resolved') IS NULL`).all();
|
|
104
|
-
const
|
|
153
|
+
const staleMs = staleTimeoutMinutes != null ? staleTimeoutMinutes * 60 * 1000 : DEFAULT_STALE_AGE_MS;
|
|
154
|
+
const cutoff = new Date(Date.now() - staleMs).toISOString();
|
|
105
155
|
const active = new Set();
|
|
106
156
|
for (const row of rows) {
|
|
107
157
|
if (active.has(row.scope_id))
|
|
@@ -146,6 +196,7 @@ export function getActiveScopeIds(db, scopeService, engine) {
|
|
|
146
196
|
batchData = JSON.parse(batchRow.data);
|
|
147
197
|
}
|
|
148
198
|
catch {
|
|
199
|
+
log.warn('Skipping unparseable batch dispatch event data', { data: batchRow.data });
|
|
149
200
|
continue;
|
|
150
201
|
}
|
|
151
202
|
const scopeIds = batchData.scope_ids;
|
|
@@ -176,12 +227,14 @@ export function getActiveScopeIds(db, scopeService, engine) {
|
|
|
176
227
|
* 3. No linked PID and dispatch older than STALE_AGE_MS (fallback)
|
|
177
228
|
* Called once at startup and periodically to clean up unresolved dispatches.
|
|
178
229
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
230
|
+
* When a dispatch is abandoned, auto-reverts scope status if the forward edge
|
|
231
|
+
* has autoRevert=true AND the scope is still at the dispatch target. This allows
|
|
232
|
+
* safe recovery for edges like backlog→implementing where the session crashed
|
|
233
|
+
* before doing meaningful work. Edges without autoRevert leave the scope in place
|
|
234
|
+
* for manual recovery from the dashboard. */
|
|
235
|
+
export function resolveStaleDispatches(db, io, scopeService, engine, staleTimeoutMinutes) {
|
|
236
|
+
const staleMs = staleTimeoutMinutes != null ? staleTimeoutMinutes * 60 * 1000 : DEFAULT_STALE_AGE_MS;
|
|
237
|
+
const cutoff = new Date(Date.now() - staleMs).toISOString();
|
|
185
238
|
// Single query on events only — split by cache status
|
|
186
239
|
const rows = db.prepare(`SELECT id, scope_id, data, timestamp FROM events
|
|
187
240
|
WHERE type = 'DISPATCH'
|
|
@@ -215,6 +268,8 @@ export function resolveStaleDispatches(db, io, scopeService, engine) {
|
|
|
215
268
|
}
|
|
216
269
|
if (isStale) {
|
|
217
270
|
resolveDispatchEvent(db, io, row.id, 'abandoned');
|
|
271
|
+
// Try auto-revert; if successful, clear the abandoned state
|
|
272
|
+
tryAutoRevertAndClear(db, io, scopeService, engine, row.id);
|
|
218
273
|
resolved++;
|
|
219
274
|
}
|
|
220
275
|
}
|
|
@@ -230,6 +285,7 @@ export function resolveStaleDispatches(db, io, scopeService, engine) {
|
|
|
230
285
|
batchData = JSON.parse(batchRow.data);
|
|
231
286
|
}
|
|
232
287
|
catch {
|
|
288
|
+
log.warn('Skipping unparseable batch dispatch event data', { eventId: batchRow.id });
|
|
233
289
|
continue;
|
|
234
290
|
}
|
|
235
291
|
const scopeIds = batchData.scope_ids;
|
|
@@ -299,6 +355,9 @@ export function getAbandonedScopeIds(db, scopeService, engine, activeScopeIds) {
|
|
|
299
355
|
const resolved = data.resolved;
|
|
300
356
|
const fromStatus = transition?.from ?? null;
|
|
301
357
|
const abandonedAt = resolved?.at ?? row.timestamp;
|
|
358
|
+
// Defense-in-depth: skip scopes already at their pre-dispatch status (already reverted)
|
|
359
|
+
if (fromStatus && scope.status === fromStatus)
|
|
360
|
+
continue;
|
|
302
361
|
result.push({ scope_id: row.scope_id, from_status: fromStatus, abandoned_at: abandonedAt });
|
|
303
362
|
}
|
|
304
363
|
return result;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { resolveDispatchEvent, resolveActiveDispatchesForScope, resolveAbandonedDispatchesForScope, linkPidToDispatch, resolveDispatchesByPid, resolveDispatchesByDispatchId, getActiveScopeIds, getAbandonedScopeIds, } from './dispatch-utils.js';
|
|
3
|
+
import { createTestDb } from '../__tests__/helpers/db.js';
|
|
4
|
+
import { createMockEmitter } from '../__tests__/helpers/mock-emitter.js';
|
|
5
|
+
import { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
6
|
+
import { CONFIG_WITH_HOOKS } from '../../shared/__fixtures__/workflow-configs.js';
|
|
7
|
+
// Mock isSessionPidAlive since it checks real OS processes
|
|
8
|
+
vi.mock('./terminal-launcher.js', () => ({
|
|
9
|
+
isSessionPidAlive: vi.fn().mockReturnValue(false),
|
|
10
|
+
launchInTerminal: vi.fn(),
|
|
11
|
+
buildSessionName: vi.fn(),
|
|
12
|
+
snapshotSessionPids: vi.fn().mockReturnValue([]),
|
|
13
|
+
discoverNewSession: vi.fn(),
|
|
14
|
+
renameSession: vi.fn(),
|
|
15
|
+
launchInCategorizedTerminal: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
function insertDispatchEvent(db, overrides = {}) {
|
|
18
|
+
const id = overrides.id ?? `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
19
|
+
const data = JSON.stringify({
|
|
20
|
+
command: '/scope-implement 1',
|
|
21
|
+
transition: { from: 'backlog', to: 'active' },
|
|
22
|
+
...(overrides.data ?? {}),
|
|
23
|
+
});
|
|
24
|
+
db.prepare(`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
25
|
+
VALUES (?, 'DISPATCH', ?, ?, NULL, ?, ?)`).run(id, overrides.scope_id ?? 1, overrides.session_id ?? 'sess-1', data, overrides.timestamp ?? new Date().toISOString());
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
describe('dispatch-utils', () => {
|
|
29
|
+
let db;
|
|
30
|
+
let cleanup;
|
|
31
|
+
let emitter;
|
|
32
|
+
let engine;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
({ db, cleanup } = createTestDb());
|
|
35
|
+
emitter = createMockEmitter();
|
|
36
|
+
engine = new WorkflowEngine(CONFIG_WITH_HOOKS);
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
cleanup?.();
|
|
40
|
+
});
|
|
41
|
+
// ─── resolveDispatchEvent() ───────────────────────────────
|
|
42
|
+
describe('resolveDispatchEvent()', () => {
|
|
43
|
+
it('marks dispatch as resolved with outcome', () => {
|
|
44
|
+
const id = insertDispatchEvent(db);
|
|
45
|
+
resolveDispatchEvent(db, emitter, id, 'completed');
|
|
46
|
+
const row = db.prepare('SELECT data FROM events WHERE id = ?').get(id);
|
|
47
|
+
const data = JSON.parse(row.data);
|
|
48
|
+
expect(data.resolved).toBeDefined();
|
|
49
|
+
expect(data.resolved.outcome).toBe('completed');
|
|
50
|
+
expect(data.resolved.at).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
it('emits dispatch:resolved event', () => {
|
|
53
|
+
const id = insertDispatchEvent(db);
|
|
54
|
+
resolveDispatchEvent(db, emitter, id, 'completed');
|
|
55
|
+
expect(emitter.emit).toHaveBeenCalledWith('dispatch:resolved', expect.objectContaining({ event_id: id }));
|
|
56
|
+
});
|
|
57
|
+
it('stores error message for failed dispatches', () => {
|
|
58
|
+
const id = insertDispatchEvent(db);
|
|
59
|
+
resolveDispatchEvent(db, emitter, id, 'failed', 'Timeout');
|
|
60
|
+
const row = db.prepare('SELECT data FROM events WHERE id = ?').get(id);
|
|
61
|
+
const data = JSON.parse(row.data);
|
|
62
|
+
expect(data.resolved.outcome).toBe('failed');
|
|
63
|
+
expect(data.resolved.error).toBe('Timeout');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
// ─── resolveActiveDispatchesForScope() ────────────────────
|
|
67
|
+
describe('resolveActiveDispatchesForScope()', () => {
|
|
68
|
+
it('resolves all unresolved dispatches for a scope', () => {
|
|
69
|
+
insertDispatchEvent(db, { id: 'd1', scope_id: 42 });
|
|
70
|
+
insertDispatchEvent(db, { id: 'd2', scope_id: 42 });
|
|
71
|
+
insertDispatchEvent(db, { id: 'd3', scope_id: 99 }); // different scope
|
|
72
|
+
resolveActiveDispatchesForScope(db, emitter, 42, 'completed');
|
|
73
|
+
const rows = db.prepare("SELECT data FROM events WHERE scope_id = 42").all();
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const data = JSON.parse(row.data);
|
|
76
|
+
expect(data.resolved).toBeDefined();
|
|
77
|
+
expect(data.resolved.outcome).toBe('completed');
|
|
78
|
+
}
|
|
79
|
+
// Scope 99 should NOT be resolved
|
|
80
|
+
const other = db.prepare("SELECT data FROM events WHERE scope_id = 99").get();
|
|
81
|
+
expect(JSON.parse(other.data).resolved).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
it('handles no active dispatches gracefully', () => {
|
|
84
|
+
expect(() => resolveActiveDispatchesForScope(db, emitter, 999, 'completed')).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
// ─── resolveAbandonedDispatchesForScope() ─────────────────
|
|
88
|
+
describe('resolveAbandonedDispatchesForScope()', () => {
|
|
89
|
+
it('resolves abandoned dispatches as completed', () => {
|
|
90
|
+
insertDispatchEvent(db, { scope_id: 42, data: { resolved: true, outcome: 'abandoned' } });
|
|
91
|
+
const count = resolveAbandonedDispatchesForScope(db, emitter, 42);
|
|
92
|
+
expect(count).toBeGreaterThanOrEqual(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
// ─── linkPidToDispatch() ──────────────────────────────────
|
|
96
|
+
describe('linkPidToDispatch()', () => {
|
|
97
|
+
it('stores PID in event data', () => {
|
|
98
|
+
const id = insertDispatchEvent(db);
|
|
99
|
+
linkPidToDispatch(db, id, 12345);
|
|
100
|
+
const row = db.prepare('SELECT data FROM events WHERE id = ?').get(id);
|
|
101
|
+
const data = JSON.parse(row.data);
|
|
102
|
+
expect(data.pid).toBe(12345);
|
|
103
|
+
});
|
|
104
|
+
it('handles non-existent event gracefully', () => {
|
|
105
|
+
expect(() => linkPidToDispatch(db, 'nonexistent', 12345)).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// ─── resolveDispatchesByPid() ─────────────────────────────
|
|
109
|
+
describe('resolveDispatchesByPid()', () => {
|
|
110
|
+
it('resolves dispatches matching PID', () => {
|
|
111
|
+
const id = insertDispatchEvent(db, { scope_id: 1 });
|
|
112
|
+
linkPidToDispatch(db, id, 54321);
|
|
113
|
+
const ids = resolveDispatchesByPid(db, emitter, 54321);
|
|
114
|
+
expect(ids).toHaveLength(1);
|
|
115
|
+
expect(ids[0]).toBe(id);
|
|
116
|
+
const row = db.prepare('SELECT data FROM events WHERE id = ?').get(id);
|
|
117
|
+
expect(JSON.parse(row.data).resolved).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
it('returns empty array when no dispatches match PID', () => {
|
|
120
|
+
expect(resolveDispatchesByPid(db, emitter, 99999)).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// ─── resolveDispatchesByDispatchId() ──────────────────────
|
|
124
|
+
describe('resolveDispatchesByDispatchId()', () => {
|
|
125
|
+
it('resolves single dispatch by event ID', () => {
|
|
126
|
+
const id = insertDispatchEvent(db);
|
|
127
|
+
const ids = resolveDispatchesByDispatchId(db, emitter, id);
|
|
128
|
+
expect(ids).toHaveLength(1);
|
|
129
|
+
expect(ids[0]).toBe(id);
|
|
130
|
+
});
|
|
131
|
+
it('returns empty array for non-existent dispatch', () => {
|
|
132
|
+
expect(resolveDispatchesByDispatchId(db, emitter, 'nonexistent')).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// ─── getActiveScopeIds() ──────────────────────────────────
|
|
136
|
+
describe('getActiveScopeIds()', () => {
|
|
137
|
+
it('returns scope IDs with unresolved dispatches', () => {
|
|
138
|
+
insertDispatchEvent(db, { scope_id: 10 });
|
|
139
|
+
insertDispatchEvent(db, { scope_id: 20 });
|
|
140
|
+
const mockScopeService = {
|
|
141
|
+
getById: (id) => ({ id, status: 'active' }),
|
|
142
|
+
};
|
|
143
|
+
const ids = getActiveScopeIds(db, mockScopeService, engine);
|
|
144
|
+
// May return empty if dispatches are stale (PID check fails), but shouldn't throw
|
|
145
|
+
expect(Array.isArray(ids)).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
it('excludes scopes in terminal status', () => {
|
|
148
|
+
insertDispatchEvent(db, { scope_id: 30 });
|
|
149
|
+
const mockScopeService = {
|
|
150
|
+
getById: (id) => ({ id, status: 'shipped' }), // terminal
|
|
151
|
+
};
|
|
152
|
+
const ids = getActiveScopeIds(db, mockScopeService, engine);
|
|
153
|
+
expect(ids).not.toContain(30);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
// ─── getAbandonedScopeIds() ───────────────────────────────
|
|
157
|
+
describe('getAbandonedScopeIds()', () => {
|
|
158
|
+
it('returns recently abandoned scopes', () => {
|
|
159
|
+
insertDispatchEvent(db, {
|
|
160
|
+
scope_id: 50,
|
|
161
|
+
data: { resolved: true, outcome: 'abandoned', resolved_at: new Date().toISOString(), transition: { from: 'backlog', to: 'active' } },
|
|
162
|
+
});
|
|
163
|
+
const mockScopeService = {
|
|
164
|
+
getById: (id) => ({ id, status: 'active' }),
|
|
165
|
+
};
|
|
166
|
+
const abandoned = getAbandonedScopeIds(db, mockScopeService, engine);
|
|
167
|
+
expect(Array.isArray(abandoned)).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
it('excludes terminal scopes', () => {
|
|
170
|
+
insertDispatchEvent(db, {
|
|
171
|
+
scope_id: 60,
|
|
172
|
+
data: { resolved: true, outcome: 'abandoned', resolved_at: new Date().toISOString(), transition: { from: 'review', to: 'shipped' } },
|
|
173
|
+
});
|
|
174
|
+
const mockScopeService = {
|
|
175
|
+
getById: (id) => ({ id, status: 'shipped' }), // terminal
|
|
176
|
+
};
|
|
177
|
+
const abandoned = getAbandonedScopeIds(db, mockScopeService, engine);
|
|
178
|
+
const ids = abandoned.map(a => a.scope_id);
|
|
179
|
+
expect(ids).not.toContain(60);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { VALID_OUTPUT_FORMATS, validateToolName, validateEnvKey } from '../../shared/api-types.js';
|
|
2
|
+
import { shellQuote } from './terminal-launcher.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compile a structured DispatchFlags object into a CLI flags string
|
|
5
|
+
* for the `claude` command. All parameterized values are validated
|
|
6
|
+
* and shell-quoted to prevent injection.
|
|
7
|
+
*/
|
|
8
|
+
export function buildClaudeFlags(flags) {
|
|
9
|
+
const parts = [];
|
|
10
|
+
// Permission mode — 'default' means no flag (use Claude's built-in default)
|
|
11
|
+
if (flags.permissionMode === 'bypass') {
|
|
12
|
+
parts.push('--dangerously-skip-permissions');
|
|
13
|
+
}
|
|
14
|
+
else if (flags.permissionMode && flags.permissionMode !== 'default') {
|
|
15
|
+
parts.push('--permission-mode', flags.permissionMode);
|
|
16
|
+
}
|
|
17
|
+
if (flags.verbose)
|
|
18
|
+
parts.push('--verbose');
|
|
19
|
+
if (flags.noMarkdown)
|
|
20
|
+
parts.push('--no-markdown');
|
|
21
|
+
if (flags.printMode)
|
|
22
|
+
parts.push('-p');
|
|
23
|
+
if (flags.outputFormat && VALID_OUTPUT_FORMATS.includes(flags.outputFormat)) {
|
|
24
|
+
parts.push('--output-format', flags.outputFormat);
|
|
25
|
+
}
|
|
26
|
+
if (flags.allowedTools.length > 0) {
|
|
27
|
+
const safe = flags.allowedTools.filter(validateToolName);
|
|
28
|
+
if (safe.length > 0)
|
|
29
|
+
parts.push('--allowedTools', safe.join(','));
|
|
30
|
+
}
|
|
31
|
+
if (flags.disallowedTools.length > 0) {
|
|
32
|
+
const safe = flags.disallowedTools.filter(validateToolName);
|
|
33
|
+
if (safe.length > 0)
|
|
34
|
+
parts.push('--disallowedTools', safe.join(','));
|
|
35
|
+
}
|
|
36
|
+
if (flags.appendSystemPrompt) {
|
|
37
|
+
const sanitized = flags.appendSystemPrompt.replace(/\n/g, '\\n');
|
|
38
|
+
parts.push('--append-system-prompt', `'${shellQuote(sanitized)}'`);
|
|
39
|
+
}
|
|
40
|
+
return parts.join(' ');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build env var prefix string for dispatch commands.
|
|
44
|
+
* Keys are validated against POSIX naming rules.
|
|
45
|
+
* Returns empty string if no vars configured.
|
|
46
|
+
*/
|
|
47
|
+
export function buildEnvVarPrefix(envVars) {
|
|
48
|
+
const entries = Object.entries(envVars).filter(([k]) => validateEnvKey(k));
|
|
49
|
+
if (entries.length === 0)
|
|
50
|
+
return '';
|
|
51
|
+
return entries
|
|
52
|
+
.map(([k, v]) => `${k}='${v.replace(/'/g, "'\\''")}'`)
|
|
53
|
+
.join(' ') + ' ';
|
|
54
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const JSON_FIELDS = ['tags', 'blocked_by', 'blocks', 'data', 'discoveries', 'next_steps', 'details'];
|
|
2
|
+
/** Parse stringified JSON fields in a database row back to objects. */
|
|
3
|
+
export function parseJsonFields(row) {
|
|
4
|
+
const parsed = { ...row };
|
|
5
|
+
for (const field of JSON_FIELDS) {
|
|
6
|
+
if (typeof parsed[field] === 'string') {
|
|
7
|
+
try {
|
|
8
|
+
parsed[field] = JSON.parse(parsed[field]);
|
|
9
|
+
}
|
|
10
|
+
catch { /* keep string */ }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseJsonFields } from './json-fields.js';
|
|
3
|
+
describe('parseJsonFields', () => {
|
|
4
|
+
it('parses stringified JSON arrays in known fields', () => {
|
|
5
|
+
const row = { tags: '["a","b"]', blocked_by: '[1,2]', title: 'scope-1' };
|
|
6
|
+
const result = parseJsonFields(row);
|
|
7
|
+
expect(result.tags).toEqual(['a', 'b']);
|
|
8
|
+
expect(result.blocked_by).toEqual([1, 2]);
|
|
9
|
+
expect(result.title).toBe('scope-1');
|
|
10
|
+
});
|
|
11
|
+
it('parses stringified JSON objects in data field', () => {
|
|
12
|
+
const row = { data: '{"key":"val","nested":{"a":1}}' };
|
|
13
|
+
const result = parseJsonFields(row);
|
|
14
|
+
expect(result.data).toEqual({ key: 'val', nested: { a: 1 } });
|
|
15
|
+
});
|
|
16
|
+
it('handles all 7 known JSON fields', () => {
|
|
17
|
+
const row = {
|
|
18
|
+
tags: '[]', blocked_by: '[]', blocks: '[]',
|
|
19
|
+
data: '{}', discoveries: '[]', next_steps: '[]', details: '{}',
|
|
20
|
+
};
|
|
21
|
+
const result = parseJsonFields(row);
|
|
22
|
+
expect(result.tags).toEqual([]);
|
|
23
|
+
expect(result.blocked_by).toEqual([]);
|
|
24
|
+
expect(result.blocks).toEqual([]);
|
|
25
|
+
expect(result.data).toEqual({});
|
|
26
|
+
expect(result.discoveries).toEqual([]);
|
|
27
|
+
expect(result.next_steps).toEqual([]);
|
|
28
|
+
expect(result.details).toEqual({});
|
|
29
|
+
});
|
|
30
|
+
it('passes through already-parsed objects untouched', () => {
|
|
31
|
+
const tags = ['a', 'b'];
|
|
32
|
+
const row = { tags, data: { foo: 1 } };
|
|
33
|
+
const result = parseJsonFields(row);
|
|
34
|
+
expect(result.tags).toBe(tags); // same reference — not re-parsed
|
|
35
|
+
expect(result.data).toEqual({ foo: 1 });
|
|
36
|
+
});
|
|
37
|
+
it('keeps malformed JSON strings as-is without throwing', () => {
|
|
38
|
+
const row = { tags: '{broken json', data: 'not json at all', blocks: '["valid"]' };
|
|
39
|
+
const result = parseJsonFields(row);
|
|
40
|
+
expect(result.tags).toBe('{broken json');
|
|
41
|
+
expect(result.data).toBe('not json at all');
|
|
42
|
+
expect(result.blocks).toEqual(['valid']);
|
|
43
|
+
});
|
|
44
|
+
it('returns row unchanged when no JSON fields are present', () => {
|
|
45
|
+
const row = { id: 1, title: 'hello', status: 'active' };
|
|
46
|
+
const result = parseJsonFields(row);
|
|
47
|
+
expect(result).toEqual(row);
|
|
48
|
+
});
|
|
49
|
+
it('does not mutate the original row', () => {
|
|
50
|
+
const row = { tags: '["x"]', title: 'scope' };
|
|
51
|
+
const result = parseJsonFields(row);
|
|
52
|
+
expect(row.tags).toBe('["x"]'); // original unchanged
|
|
53
|
+
expect(result.tags).toEqual(['x']); // copy was parsed
|
|
54
|
+
expect(result).not.toBe(row);
|
|
55
|
+
});
|
|
56
|
+
it('handles null and undefined field values', () => {
|
|
57
|
+
const row = { tags: null, data: undefined, blocks: '["a"]' };
|
|
58
|
+
const result = parseJsonFields(row);
|
|
59
|
+
expect(result.tags).toBeNull();
|
|
60
|
+
expect(result.data).toBeUndefined();
|
|
61
|
+
expect(result.blocks).toEqual(['a']);
|
|
62
|
+
});
|
|
63
|
+
it('handles empty row', () => {
|
|
64
|
+
const result = parseJsonFields({});
|
|
65
|
+
expect(result).toEqual({});
|
|
66
|
+
});
|
|
67
|
+
it('ignores non-JSON-field strings', () => {
|
|
68
|
+
const row = { title: '["not a json field"]', tags: '["real"]' };
|
|
69
|
+
const result = parseJsonFields(row);
|
|
70
|
+
expect(result.title).toBe('["not a json field"]'); // title is not in JSON_FIELDS
|
|
71
|
+
expect(result.tags).toEqual(['real']);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -24,8 +24,14 @@ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
|
24
24
|
const c = {
|
|
25
25
|
reset: useColor ? '\x1b[0m' : '',
|
|
26
26
|
dim: useColor ? '\x1b[2m' : '',
|
|
27
|
-
|
|
27
|
+
green: useColor ? '\x1b[32m' : '',
|
|
28
|
+
blue: useColor ? '\x1b[34m' : '',
|
|
29
|
+
magenta: useColor ? '\x1b[35m' : '',
|
|
28
30
|
cyan: useColor ? '\x1b[36m' : '',
|
|
31
|
+
white: useColor ? '\x1b[37m' : '',
|
|
32
|
+
gray: useColor ? '\x1b[90m' : '',
|
|
33
|
+
brightMagenta: useColor ? '\x1b[95m' : '',
|
|
34
|
+
brightCyan: useColor ? '\x1b[96m' : '',
|
|
29
35
|
yellow: useColor ? '\x1b[33m' : '',
|
|
30
36
|
red: useColor ? '\x1b[31m' : '',
|
|
31
37
|
};
|
|
@@ -41,6 +47,31 @@ const LEVEL_LABEL = {
|
|
|
41
47
|
warn: 'WARN ',
|
|
42
48
|
error: 'ERROR',
|
|
43
49
|
};
|
|
50
|
+
// ─── Component Colors ───────────────────────────────────────
|
|
51
|
+
const COMPONENT_COLOR = {
|
|
52
|
+
// Scope (green)
|
|
53
|
+
'scope': c.green, 'scope-watcher': c.green,
|
|
54
|
+
// Dispatch (magenta)
|
|
55
|
+
'dispatch': c.magenta, 'dispatch-utils': c.magenta, 'batch': c.magenta,
|
|
56
|
+
// Git (blue)
|
|
57
|
+
'git': c.blue, 'worktree': c.blue,
|
|
58
|
+
// Workflow (cyan)
|
|
59
|
+
'workflow': c.cyan, 'sprint': c.cyan,
|
|
60
|
+
// Sync / Config (yellow)
|
|
61
|
+
'sync': c.yellow, 'config': c.yellow, 'manifest': c.yellow, 'global-config': c.yellow,
|
|
62
|
+
// Events (white)
|
|
63
|
+
'event': c.white, 'event-watcher': c.white, 'global-watcher': c.white,
|
|
64
|
+
// Infrastructure (white)
|
|
65
|
+
'server': c.white, 'central': c.white, 'database': c.white,
|
|
66
|
+
'project-context': c.white, 'project-manager': c.white, 'launch': c.white,
|
|
67
|
+
// Terminal (bright magenta)
|
|
68
|
+
'terminal': c.brightMagenta,
|
|
69
|
+
// Services (bright cyan)
|
|
70
|
+
'gate': c.brightCyan, 'deploy': c.brightCyan, 'telemetry': c.brightCyan, 'version': c.brightCyan,
|
|
71
|
+
};
|
|
72
|
+
function componentColor(name) {
|
|
73
|
+
return COMPONENT_COLOR[name] ?? c.dim;
|
|
74
|
+
}
|
|
44
75
|
// ─── Formatting ─────────────────────────────────────────────
|
|
45
76
|
function timestamp() {
|
|
46
77
|
const d = new Date();
|
|
@@ -65,10 +96,13 @@ function formatData(data) {
|
|
|
65
96
|
function write(level, component, msg, data) {
|
|
66
97
|
if (LEVEL_VALUE[level] < LEVEL_VALUE[currentLevel])
|
|
67
98
|
return;
|
|
68
|
-
const
|
|
99
|
+
const lvl = LEVEL_COLOR[level];
|
|
69
100
|
const label = LEVEL_LABEL[level];
|
|
70
101
|
const kv = formatData(data);
|
|
71
|
-
const
|
|
102
|
+
const cc = componentColor(component);
|
|
103
|
+
// Message uses level color for warn/error (urgency), component color otherwise
|
|
104
|
+
const mc = (level === 'warn' || level === 'error') ? lvl : cc;
|
|
105
|
+
const line = `${c.dim}${timestamp()}${c.reset} ${lvl}${label}${c.reset} ${cc}[${component}]${c.reset} ${mc}${msg}${c.reset}${c.dim}${kv}${c.reset}\n`;
|
|
72
106
|
if (level === 'warn' || level === 'error') {
|
|
73
107
|
process.stderr.write(line);
|
|
74
108
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __selfDir = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the root directory of the orbital-command package itself.
|
|
7
|
+
* Walks up from the current file until it finds package.json.
|
|
8
|
+
*/
|
|
9
|
+
export function getOrbitalRoot() {
|
|
10
|
+
let dir = __selfDir;
|
|
11
|
+
for (let i = 0; i < 6; i++) {
|
|
12
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
dir = path.dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
// Fallback: assume dev layout (server/utils/ → 2 levels up)
|
|
18
|
+
return path.resolve(__selfDir, '../..');
|
|
19
|
+
}
|
|
20
|
+
/** Read package version from package.json in the given root, or the orbital root. */
|
|
21
|
+
export function getPackageVersion(rootDir) {
|
|
22
|
+
try {
|
|
23
|
+
const dir = rootDir ?? getOrbitalRoot();
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
25
|
+
return pkg.version || '0.0.0';
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return '0.0.0';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
/** Extract a human-readable message from an unknown error value. */
|
|
3
|
+
export function errMsg(err) {
|
|
4
|
+
return err instanceof Error ? err.message : String(err);
|
|
5
|
+
}
|
|
6
|
+
/** Validate that a relative path stays within bounds (no traversal). */
|
|
7
|
+
export function isValidRelativePath(p) {
|
|
8
|
+
const normalized = path.normalize(p);
|
|
9
|
+
return !normalized.startsWith('..') && !path.isAbsolute(normalized) && !normalized.includes('\0');
|
|
10
|
+
}
|
|
11
|
+
/** Infer an HTTP status code from an error message. */
|
|
12
|
+
export function inferErrorStatus(msg) {
|
|
13
|
+
if (msg.includes('traversal'))
|
|
14
|
+
return 403;
|
|
15
|
+
if (msg.includes('ENOENT') || msg.includes('not found'))
|
|
16
|
+
return 404;
|
|
17
|
+
if (msg.includes('already exists'))
|
|
18
|
+
return 409;
|
|
19
|
+
if (msg.includes('directory'))
|
|
20
|
+
return 400;
|
|
21
|
+
return 500;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Wrap an Express route handler to catch thrown errors and send a JSON error response.
|
|
25
|
+
* Works with both sync and async handlers.
|
|
26
|
+
*
|
|
27
|
+
* @param fn — route handler that may throw
|
|
28
|
+
* @param statusFn — optional function to infer status from error message (defaults to 500)
|
|
29
|
+
*/
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
export function catchRoute(fn, statusFn) {
|
|
32
|
+
return ((req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const result = fn(req, res, next);
|
|
35
|
+
if (result instanceof Promise) {
|
|
36
|
+
result.catch((err) => {
|
|
37
|
+
const msg = errMsg(err);
|
|
38
|
+
res.status(statusFn ? statusFn(msg) : 500).json({ success: false, error: msg });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const msg = errMsg(err);
|
|
44
|
+
res.status(statusFn ? statusFn(msg) : 500).json({ success: false, error: msg });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|