orbital-command 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/orbital.js +676 -53
- package/dist/assets/PrimitivesConfig-CrmQXYh4.js +32 -0
- package/dist/assets/QualityGates-BbasOsF3.js +21 -0
- package/dist/assets/SessionTimeline-CGeJsVvy.js +1 -0
- package/dist/assets/Settings-oiM496mc.js +12 -0
- package/dist/assets/SourceControl-B1fP2nJL.js +41 -0
- package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +74 -0
- package/dist/assets/arrow-down-CPy85_J6.js +6 -0
- package/dist/assets/charts-DbDg0Psc.js +68 -0
- package/dist/assets/circle-x-Cwz6ZQDV.js +6 -0
- package/dist/assets/file-text-C46Xr65c.js +6 -0
- package/dist/assets/formatDistanceToNow-BMqsSP44.js +1 -0
- package/dist/assets/globe-Cn2yNZUD.js +6 -0
- package/dist/assets/index-Aj4sV8Al.css +1 -0
- package/dist/assets/index-Bc9dK3MW.js +354 -0
- package/dist/assets/key-OPaNTWJ5.js +6 -0
- package/dist/assets/minus-GMsbpKym.js +6 -0
- package/dist/assets/shield-DwAFkDYI.js +6 -0
- package/dist/assets/ui-BmsSg9jU.js +53 -0
- package/dist/assets/useWorkflowEditor-BJkTX_NR.js +16 -0
- package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
- package/dist/assets/zap-DfbUoOty.js +11 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +6 -5
- package/dist/server/server/__tests__/data-routes.test.js +124 -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 +137 -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 +138 -0
- package/dist/server/server/config.js +17 -2
- package/dist/server/server/database.js +27 -12
- package/dist/server/server/global-config.js +143 -0
- package/dist/server/server/index.js +882 -252
- package/dist/server/server/init.js +579 -194
- 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.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 +255 -0
- package/dist/server/server/project-emitter.js +41 -0
- package/dist/server/server/project-manager.js +297 -0
- package/dist/server/server/routes/config-routes.js +1 -3
- package/dist/server/server/routes/data-routes.js +22 -110
- package/dist/server/server/routes/dispatch-routes.js +15 -9
- 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 +37 -23
- 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 +2 -0
- package/dist/server/server/services/batch-orchestrator.js +26 -16
- package/dist/server/server/services/claude-session-service.js +17 -14
- 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 +217 -126
- package/dist/server/server/services/scope-service.test.js +137 -0
- package/dist/server/server/services/sprint-orchestrator.js +7 -6
- package/dist/server/server/services/sprint-service.js +21 -1
- 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/telemetry-service.js +143 -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/update-planner.js +279 -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 +77 -20
- package/dist/server/server/utils/dispatch-utils.test.js +182 -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 +10 -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/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 +155 -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 +35 -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 +74 -0
- package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
- 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.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 +20 -15
- package/schemas/orbital.config.schema.json +16 -1
- package/scripts/postinstall.js +55 -7
- package/server/__tests__/data-routes.test.ts +149 -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 +157 -0
- package/server/__tests__/sprint-routes.test.ts +118 -0
- package/server/__tests__/workflow-routes.test.ts +120 -0
- package/server/config-migrator.ts +163 -0
- package/server/config.ts +26 -2
- package/server/database.ts +35 -18
- package/server/global-config.ts +200 -0
- package/server/index.ts +975 -287
- package/server/init.ts +625 -182
- 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/scope-parser.test.ts +270 -0
- package/server/parsers/scope-parser.ts +79 -31
- package/server/project-context.ts +309 -0
- package/server/project-emitter.ts +50 -0
- package/server/project-manager.ts +369 -0
- package/server/routes/config-routes.ts +3 -5
- package/server/routes/data-routes.ts +28 -141
- package/server/routes/dispatch-routes.ts +19 -11
- package/server/routes/git-routes.ts +77 -0
- package/server/routes/manifest-routes.ts +388 -0
- package/server/routes/scope-routes.ts +29 -25
- 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 +2 -0
- package/server/services/batch-orchestrator.ts +24 -16
- package/server/services/claude-session-service.ts +16 -14
- 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 +220 -126
- package/server/services/sprint-orchestrator.ts +7 -7
- package/server/services/sprint-service.test.ts +271 -0
- package/server/services/sprint-service.ts +27 -3
- package/server/services/sync-service.ts +482 -0
- package/server/services/sync-types.ts +77 -0
- package/server/services/telemetry-service.ts +195 -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/update-planner.ts +346 -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 +97 -27
- package/server/utils/logger.ts +40 -3
- package/server/utils/package-info.ts +32 -0
- package/server/utils/route-helpers.ts +12 -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/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 +187 -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 +43 -0
- package/server/wizard/phases/workflow-setup.ts +28 -0
- package/server/wizard/types.ts +56 -0
- package/server/wizard/ui.ts +93 -0
- package/shared/__fixtures__/workflow-configs.ts +80 -0
- package/shared/default-workflow.json +65 -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-normalizer.test.ts +119 -0
- package/shared/workflow-normalizer.ts +118 -0
- package/templates/hooks/end-session.sh +3 -1
- 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-create-cleanup.sh +2 -2
- package/templates/hooks/scope-create-gate.sh +0 -1
- package/templates/hooks/scope-helpers.sh +18 -0
- package/templates/hooks/scope-prepare.sh +66 -11
- package/templates/migrations/renames.json +1 -0
- package/templates/orbital.config.json +7 -2
- package/templates/settings-hooks.json +1 -1
- package/templates/skills/git-commit/SKILL.md +9 -4
- package/templates/skills/git-dev/SKILL.md +8 -3
- package/templates/skills/git-main/SKILL.md +8 -2
- package/templates/skills/git-production/SKILL.md +6 -2
- package/templates/skills/git-staging/SKILL.md +8 -3
- package/templates/skills/scope-create/SKILL.md +17 -3
- package/templates/skills/scope-fix-review/SKILL.md +6 -3
- package/templates/skills/scope-implement/SKILL.md +4 -1
- package/templates/skills/scope-post-review/SKILL.md +63 -5
- package/templates/skills/scope-pre-review/SKILL.md +5 -2
- 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 -49
|
@@ -4,72 +4,43 @@ import { Server } from 'socket.io';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { GateService } from './services/gate-service.js';
|
|
13
|
-
import { DeployService } from './services/deploy-service.js';
|
|
14
|
-
import { SprintService } from './services/sprint-service.js';
|
|
15
|
-
import { SprintOrchestrator } from './services/sprint-orchestrator.js';
|
|
16
|
-
import { BatchOrchestrator } from './services/batch-orchestrator.js';
|
|
17
|
-
import { ReadinessService } from './services/readiness-service.js';
|
|
18
|
-
import { startScopeWatcher } from './watchers/scope-watcher.js';
|
|
19
|
-
import { startEventWatcher } from './watchers/event-watcher.js';
|
|
20
|
-
import { ensureDynamicProfiles } from './utils/terminal-launcher.js';
|
|
21
|
-
import { syncClaudeSessionsToDB } from './services/claude-session-service.js';
|
|
22
|
-
import { resolveStaleDispatches, resolveActiveDispatchesForScope, resolveDispatchesByPid, resolveDispatchesByDispatchId, linkPidToDispatch } from './utils/dispatch-utils.js';
|
|
23
|
-
import { createScopeRoutes } from './routes/scope-routes.js';
|
|
24
|
-
import { createDataRoutes } from './routes/data-routes.js';
|
|
25
|
-
import { createDispatchRoutes } from './routes/dispatch-routes.js';
|
|
26
|
-
import { createSprintRoutes } from './routes/sprint-routes.js';
|
|
27
|
-
import { createWorkflowRoutes } from './routes/workflow-routes.js';
|
|
28
|
-
import { createConfigRoutes } from './routes/config-routes.js';
|
|
29
|
-
import { createGitRoutes } from './routes/git-routes.js';
|
|
7
|
+
import { launchInTerminal } from './utils/terminal-launcher.js';
|
|
8
|
+
import { getClaudeSessions, getSessionStats } from './services/claude-session-service.js';
|
|
9
|
+
import { getActiveScopeIds, getAbandonedScopeIds } from './utils/dispatch-utils.js';
|
|
10
|
+
import { ConfigService, isValidPrimitiveType } from './services/config-service.js';
|
|
11
|
+
import { GLOBAL_PRIMITIVES_DIR } from './global-config.js';
|
|
30
12
|
import { createVersionRoutes } from './routes/version-routes.js';
|
|
31
|
-
import {
|
|
32
|
-
import { GitService } from './services/git-service.js';
|
|
33
|
-
import { GitHubService } from './services/github-service.js';
|
|
34
|
-
import { WorkflowEngine } from '../shared/workflow-engine.js';
|
|
35
|
-
import defaultWorkflow from '../shared/default-workflow.json' with { type: 'json' };
|
|
13
|
+
import { getHookEnforcement } from '../shared/workflow-config.js';
|
|
36
14
|
import { createLogger, setLogLevel } from './utils/logger.js';
|
|
37
|
-
// ─── Server
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
15
|
+
// ─── Central Server ─────────────────────────────────────────
|
|
16
|
+
import { ProjectManager } from './project-manager.js';
|
|
17
|
+
import { SyncService } from './services/sync-service.js';
|
|
18
|
+
import { startGlobalWatcher } from './watchers/global-watcher.js';
|
|
19
|
+
import { createSyncRoutes } from './routes/sync-routes.js';
|
|
20
|
+
import { seedGlobalPrimitives, runUpdate } from './init.js';
|
|
21
|
+
import { loadManifest, refreshFileStatuses, summarizeManifest } from './manifest.js';
|
|
22
|
+
import { getPackageVersion } from './utils/package-info.js';
|
|
23
|
+
import { ensureOrbitalHome, loadGlobalConfig, registerProject as registerProjectGlobal, ORBITAL_HOME, } from './global-config.js';
|
|
24
|
+
export async function startCentralServer(overrides) {
|
|
25
|
+
ensureOrbitalHome();
|
|
45
26
|
const envLevel = process.env.ORBITAL_LOG_LEVEL;
|
|
46
27
|
if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {
|
|
47
28
|
setLogLevel(envLevel);
|
|
48
29
|
}
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
const log = createLogger('central');
|
|
31
|
+
const port = overrides?.port ?? (Number(process.env.ORBITAL_SERVER_PORT) || 4444);
|
|
32
|
+
const clientPort = overrides?.clientPort ?? (Number(process.env.ORBITAL_CLIENT_PORT) || 4445);
|
|
33
|
+
// Auto-register current project if registry is empty
|
|
34
|
+
const globalConfig = loadGlobalConfig();
|
|
35
|
+
if (globalConfig.projects.length === 0 && overrides?.autoRegisterPath) {
|
|
36
|
+
registerProjectGlobal(overrides.autoRegisterPath);
|
|
37
|
+
log.info('Auto-registered current project', { path: overrides.autoRegisterPath });
|
|
51
38
|
}
|
|
52
|
-
const log = createLogger('server');
|
|
53
|
-
const port = overrides?.port ?? config.serverPort;
|
|
54
|
-
const workflowEngine = new WorkflowEngine(defaultWorkflow);
|
|
55
|
-
// Generate shell manifest for bash hooks (config-driven lifecycle)
|
|
56
|
-
const MANIFEST_PATH = path.join(config.configDir, 'workflow-manifest.sh');
|
|
57
|
-
if (!fs.existsSync(config.configDir))
|
|
58
|
-
fs.mkdirSync(config.configDir, { recursive: true });
|
|
59
|
-
fs.writeFileSync(MANIFEST_PATH, workflowEngine.generateShellManifest(), 'utf-8');
|
|
60
|
-
const ICEBOX_DIR = path.join(config.scopesDir, 'icebox');
|
|
61
|
-
// Resolve path to the bundled default workflow config.
|
|
62
|
-
const __selfDir2 = path.dirname(fileURLToPath(import.meta.url));
|
|
63
|
-
const DEFAULT_CONFIG_PATH = path.resolve(__selfDir2, '../shared/default-workflow.json');
|
|
64
|
-
// Ensure icebox directory exists for idea files
|
|
65
|
-
if (!fs.existsSync(ICEBOX_DIR))
|
|
66
|
-
fs.mkdirSync(ICEBOX_DIR, { recursive: true });
|
|
67
39
|
const app = express();
|
|
68
40
|
const httpServer = createServer(app);
|
|
69
41
|
const io = new Server(httpServer, {
|
|
70
42
|
cors: {
|
|
71
43
|
origin: (origin, callback) => {
|
|
72
|
-
// Allow all localhost origins (dev tool, not production)
|
|
73
44
|
if (!origin || origin.startsWith('http://localhost:')) {
|
|
74
45
|
callback(null, true);
|
|
75
46
|
}
|
|
@@ -80,122 +51,833 @@ export async function startServer(overrides) {
|
|
|
80
51
|
methods: ['GET', 'POST'],
|
|
81
52
|
},
|
|
82
53
|
});
|
|
83
|
-
// Middleware
|
|
84
54
|
app.use(express.json());
|
|
85
|
-
// Initialize
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
55
|
+
// Initialize ProjectManager and boot all registered projects
|
|
56
|
+
const projectManager = new ProjectManager(io);
|
|
57
|
+
await projectManager.initializeAll();
|
|
58
|
+
// Seed global primitives if empty (lazy fallback for first launch)
|
|
59
|
+
const globalPrimitivesEmpty = ['agents', 'skills', 'hooks'].every(t => {
|
|
60
|
+
const dir = path.join(GLOBAL_PRIMITIVES_DIR, t);
|
|
61
|
+
return !fs.existsSync(dir) || fs.readdirSync(dir).filter(f => !f.startsWith('.')).length === 0;
|
|
62
|
+
});
|
|
63
|
+
if (globalPrimitivesEmpty) {
|
|
64
|
+
seedGlobalPrimitives();
|
|
65
|
+
log.info('Seeded global primitives from package templates');
|
|
66
|
+
}
|
|
67
|
+
// Initialize SyncService and global watcher
|
|
68
|
+
const syncService = new SyncService();
|
|
69
|
+
const globalWatcher = startGlobalWatcher(syncService, io);
|
|
70
|
+
// ─── Routes ──────────────────────────────────────────────
|
|
71
|
+
// Health check
|
|
72
|
+
app.get('/api/orbital/health', (_req, res) => {
|
|
73
|
+
res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
|
|
74
|
+
});
|
|
75
|
+
// Project management + sync routes (top-level)
|
|
76
|
+
app.use('/api/orbital', createSyncRoutes({ syncService, projectManager }));
|
|
77
|
+
app.use('/api/orbital', createVersionRoutes({ io }));
|
|
78
|
+
// Per-project routes — dynamic middleware that resolves :projectId
|
|
79
|
+
app.use('/api/orbital/projects/:projectId', (req, res, next) => {
|
|
80
|
+
const projectId = req.params.projectId;
|
|
81
|
+
const router = projectManager.getRouter(projectId);
|
|
82
|
+
if (!router) {
|
|
83
|
+
const ctx = projectManager.getContext(projectId);
|
|
84
|
+
if (!ctx)
|
|
85
|
+
return res.status(404).json({ error: `Project '${projectId}' not found` });
|
|
86
|
+
return res.status(503).json({ error: `Project '${projectId}' is offline` });
|
|
87
|
+
}
|
|
88
|
+
router(req, res, next);
|
|
89
|
+
});
|
|
90
|
+
// Aggregate endpoints
|
|
91
|
+
app.get('/api/orbital/aggregate/scopes', (_req, res) => {
|
|
92
|
+
const allScopes = [];
|
|
93
|
+
for (const [projectId, ctx] of projectManager.getAllContexts()) {
|
|
94
|
+
for (const scope of ctx.scopeService.getAll()) {
|
|
95
|
+
allScopes.push({ ...scope, project_id: projectId });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
res.json(allScopes);
|
|
99
|
+
});
|
|
100
|
+
app.get('/api/orbital/aggregate/events', (req, res) => {
|
|
101
|
+
const limit = Number(req.query.limit) || 50;
|
|
102
|
+
const allEvents = [];
|
|
103
|
+
for (const [projectId, ctx] of projectManager.getAllContexts()) {
|
|
104
|
+
const events = ctx.db.prepare(`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`).all(limit);
|
|
105
|
+
for (const event of events) {
|
|
106
|
+
allEvents.push({ ...event, project_id: projectId });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Sort by timestamp descending across all projects
|
|
110
|
+
allEvents.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)));
|
|
111
|
+
res.json(allEvents.slice(0, limit));
|
|
112
|
+
});
|
|
113
|
+
// Aggregate sessions across all projects
|
|
114
|
+
const JSON_FIELDS = ['tags', 'blocked_by', 'blocks', 'data', 'discoveries', 'next_steps', 'details'];
|
|
115
|
+
function parseJsonFields(row) {
|
|
116
|
+
const parsed = { ...row };
|
|
117
|
+
for (const field of JSON_FIELDS) {
|
|
118
|
+
if (typeof parsed[field] === 'string') {
|
|
119
|
+
try {
|
|
120
|
+
parsed[field] = JSON.parse(parsed[field]);
|
|
121
|
+
}
|
|
122
|
+
catch { /* keep string */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
app.get('/api/orbital/aggregate/sessions', (_req, res) => {
|
|
128
|
+
const allRows = [];
|
|
129
|
+
for (const [projectId, ctx] of projectManager.getAllContexts()) {
|
|
130
|
+
const rows = ctx.db.prepare('SELECT * FROM sessions ORDER BY started_at DESC').all();
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
allRows.push({ ...parseJsonFields(row), project_id: projectId });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Deduplicate by claude_session_id, aggregate scope_ids and actions
|
|
136
|
+
const seen = new Map();
|
|
137
|
+
const scopeMap = new Map();
|
|
138
|
+
const actionMap = new Map();
|
|
139
|
+
for (const row of allRows) {
|
|
140
|
+
const key = row.claude_session_id ?? row.id;
|
|
141
|
+
if (!seen.has(key)) {
|
|
142
|
+
seen.set(key, row);
|
|
143
|
+
scopeMap.set(key, []);
|
|
144
|
+
actionMap.set(key, []);
|
|
145
|
+
}
|
|
146
|
+
const sid = row.scope_id;
|
|
147
|
+
if (sid != null) {
|
|
148
|
+
const arr = scopeMap.get(key);
|
|
149
|
+
if (!arr.includes(sid))
|
|
150
|
+
arr.push(sid);
|
|
151
|
+
}
|
|
152
|
+
const action = row.action;
|
|
153
|
+
if (action) {
|
|
154
|
+
const actions = actionMap.get(key);
|
|
155
|
+
if (!actions.includes(action))
|
|
156
|
+
actions.push(action);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const results = [...seen.values()].map((row) => {
|
|
160
|
+
const key = row.claude_session_id ?? row.id;
|
|
161
|
+
return { ...row, scope_ids: scopeMap.get(key) ?? [], actions: actionMap.get(key) ?? [] };
|
|
162
|
+
});
|
|
163
|
+
// Sort by started_at descending across all projects
|
|
164
|
+
results.sort((a, b) => String(b.started_at ?? '').localeCompare(String(a.started_at ?? '')));
|
|
165
|
+
res.json(results.slice(0, 50));
|
|
166
|
+
});
|
|
167
|
+
app.get('/api/orbital/aggregate/sessions/:id/content', async (req, res) => {
|
|
168
|
+
const sessionId = req.params.id;
|
|
169
|
+
// Find the session across all project databases
|
|
170
|
+
let session;
|
|
171
|
+
let matchedProjectRoot;
|
|
172
|
+
for (const [, ctx] of projectManager.getAllContexts()) {
|
|
173
|
+
const row = ctx.db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
|
|
174
|
+
if (row) {
|
|
175
|
+
session = parseJsonFields(row);
|
|
176
|
+
matchedProjectRoot = ctx.config.projectRoot;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!session || !matchedProjectRoot) {
|
|
181
|
+
res.status(404).json({ error: 'Session not found' });
|
|
120
182
|
return;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
183
|
+
}
|
|
184
|
+
let content = '';
|
|
185
|
+
let meta = null;
|
|
186
|
+
let stats = null;
|
|
187
|
+
if (session.claude_session_id && typeof session.claude_session_id === 'string') {
|
|
188
|
+
const claudeSessions = await getClaudeSessions(undefined, matchedProjectRoot);
|
|
189
|
+
const match = claudeSessions.find(s => s.id === session.claude_session_id);
|
|
190
|
+
if (match) {
|
|
191
|
+
meta = {
|
|
192
|
+
slug: match.slug,
|
|
193
|
+
branch: match.branch,
|
|
194
|
+
fileSize: match.fileSize,
|
|
195
|
+
summary: match.summary,
|
|
196
|
+
startedAt: match.startedAt,
|
|
197
|
+
lastActiveAt: match.lastActiveAt,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
stats = getSessionStats(session.claude_session_id, matchedProjectRoot);
|
|
201
|
+
}
|
|
202
|
+
if (!content) {
|
|
203
|
+
const parts = [];
|
|
204
|
+
if (session.summary)
|
|
205
|
+
parts.push(`# ${session.summary}\n`);
|
|
206
|
+
const discoveries = Array.isArray(session.discoveries) ? session.discoveries : [];
|
|
207
|
+
if (discoveries.length > 0) {
|
|
208
|
+
parts.push('## Completed\n');
|
|
209
|
+
for (const d of discoveries)
|
|
210
|
+
parts.push(`- ${d}`);
|
|
211
|
+
parts.push('');
|
|
212
|
+
}
|
|
213
|
+
const nextSteps = Array.isArray(session.next_steps) ? session.next_steps : [];
|
|
214
|
+
if (nextSteps.length > 0) {
|
|
215
|
+
parts.push('## Next Steps\n');
|
|
216
|
+
for (const n of nextSteps)
|
|
217
|
+
parts.push(`- ${n}`);
|
|
218
|
+
}
|
|
219
|
+
content = parts.join('\n');
|
|
220
|
+
}
|
|
221
|
+
res.json({
|
|
222
|
+
id: session.id,
|
|
223
|
+
content,
|
|
224
|
+
claude_session_id: session.claude_session_id ?? null,
|
|
225
|
+
meta,
|
|
226
|
+
stats,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
app.post('/api/orbital/aggregate/sessions/:id/resume', async (req, res) => {
|
|
230
|
+
const sessionId = req.params.id;
|
|
231
|
+
const { claude_session_id } = req.body;
|
|
232
|
+
if (!claude_session_id || !/^[0-9a-f-]{36}$/i.test(claude_session_id)) {
|
|
233
|
+
res.status(400).json({ error: 'Valid claude_session_id (UUID) required' });
|
|
124
234
|
return;
|
|
125
235
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
236
|
+
// Find the session's project root
|
|
237
|
+
let matchedProjectRoot;
|
|
238
|
+
for (const [, ctx] of projectManager.getAllContexts()) {
|
|
239
|
+
const row = ctx.db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
|
|
240
|
+
if (row) {
|
|
241
|
+
matchedProjectRoot = ctx.config.projectRoot;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!matchedProjectRoot) {
|
|
246
|
+
res.status(404).json({ error: 'Session not found' });
|
|
133
247
|
return;
|
|
134
248
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
249
|
+
const resumeCmd = `cd '${matchedProjectRoot}' && claude --dangerously-skip-permissions --resume '${claude_session_id}'`;
|
|
250
|
+
try {
|
|
251
|
+
await launchInTerminal(resumeCmd);
|
|
252
|
+
res.json({ ok: true, session_id: claude_session_id });
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
log.error('Terminal launch failed', { error: String(err) });
|
|
256
|
+
res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
// ─── Aggregate: Enforcement & Gates ──────────────────────
|
|
260
|
+
app.get('/api/orbital/aggregate/events/violations/summary', (_req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const mergedByRule = new Map();
|
|
263
|
+
const mergedByFile = new Map();
|
|
264
|
+
let allOverrides = [];
|
|
265
|
+
let totalViolations = 0;
|
|
266
|
+
let totalOverrides = 0;
|
|
267
|
+
for (const [, ctx] of projectManager.getAllContexts()) {
|
|
268
|
+
const byRule = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
|
|
269
|
+
FROM events WHERE type = 'VIOLATION' GROUP BY rule ORDER BY count DESC`).all();
|
|
270
|
+
for (const r of byRule) {
|
|
271
|
+
const existing = mergedByRule.get(r.rule);
|
|
272
|
+
if (existing) {
|
|
273
|
+
existing.count += r.count;
|
|
274
|
+
if (r.last_seen > existing.last_seen)
|
|
275
|
+
existing.last_seen = r.last_seen;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
mergedByRule.set(r.rule, { ...r });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const byFile = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.file') as file, COUNT(*) as count FROM events
|
|
282
|
+
WHERE type = 'VIOLATION' AND JSON_EXTRACT(data, '$.file') IS NOT NULL AND JSON_EXTRACT(data, '$.file') != ''
|
|
283
|
+
GROUP BY file ORDER BY count DESC LIMIT 20`).all();
|
|
284
|
+
for (const f of byFile) {
|
|
285
|
+
const existing = mergedByFile.get(f.file);
|
|
286
|
+
if (existing) {
|
|
287
|
+
existing.count += f.count;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
mergedByFile.set(f.file, { ...f });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const overrides = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, JSON_EXTRACT(data, '$.reason') as reason, timestamp as date
|
|
294
|
+
FROM events WHERE type = 'OVERRIDE' ORDER BY timestamp DESC LIMIT 50`).all();
|
|
295
|
+
allOverrides = allOverrides.concat(overrides);
|
|
296
|
+
const tv = ctx.db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'VIOLATION'`).get();
|
|
297
|
+
const to = ctx.db.prepare(`SELECT COUNT(*) as count FROM events WHERE type = 'OVERRIDE'`).get();
|
|
298
|
+
totalViolations += tv.count;
|
|
299
|
+
totalOverrides += to.count;
|
|
300
|
+
}
|
|
301
|
+
const byRule = [...mergedByRule.values()].sort((a, b) => b.count - a.count);
|
|
302
|
+
const byFile = [...mergedByFile.values()].sort((a, b) => b.count - a.count).slice(0, 20);
|
|
303
|
+
allOverrides.sort((a, b) => b.date.localeCompare(a.date));
|
|
304
|
+
res.json({ byRule, byFile, overrides: allOverrides.slice(0, 50), totalViolations, totalOverrides });
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
log.error('Violations summary failed', { error: String(err) });
|
|
308
|
+
res.status(500).json({ error: 'Failed to aggregate violations summary' });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
app.get('/api/orbital/aggregate/enforcement/rules', (_req, res) => {
|
|
312
|
+
try {
|
|
313
|
+
const hookMap = new Map();
|
|
314
|
+
const summary = { guards: 0, gates: 0, lifecycle: 0, observers: 0 };
|
|
315
|
+
const edgeIdSet = new Set();
|
|
316
|
+
let totalEdges = 0;
|
|
317
|
+
for (const [, ctx] of projectManager.getAllContexts()) {
|
|
318
|
+
const allHooks = ctx.workflowEngine.getAllHooks();
|
|
319
|
+
const allEdges = ctx.workflowEngine.getAllEdges();
|
|
320
|
+
// Build edge map for this project
|
|
321
|
+
const hookEdgeMap = new Map();
|
|
322
|
+
for (const edge of allEdges) {
|
|
323
|
+
const edgeKey = `${edge.from}->${edge.to}`;
|
|
324
|
+
if (!edgeIdSet.has(edgeKey)) {
|
|
325
|
+
edgeIdSet.add(edgeKey);
|
|
326
|
+
totalEdges++;
|
|
327
|
+
}
|
|
328
|
+
for (const hookId of edge.hooks ?? []) {
|
|
329
|
+
if (!hookEdgeMap.has(hookId))
|
|
330
|
+
hookEdgeMap.set(hookId, []);
|
|
331
|
+
hookEdgeMap.get(hookId).push({ from: edge.from, to: edge.to, label: edge.label });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Query stats from this project's DB
|
|
335
|
+
const violationStats = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count, MAX(timestamp) as last_seen
|
|
336
|
+
FROM events WHERE type = 'VIOLATION' GROUP BY rule`).all();
|
|
337
|
+
const overrideStats = ctx.db.prepare(`SELECT JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
|
|
338
|
+
FROM events WHERE type = 'OVERRIDE' GROUP BY rule`).all();
|
|
339
|
+
const violationMap = new Map(violationStats.map((v) => [v.rule, v]));
|
|
340
|
+
const overrideMap = new Map(overrideStats.map((o) => [o.rule, o]));
|
|
341
|
+
for (const hook of allHooks) {
|
|
342
|
+
const existing = hookMap.get(hook.id);
|
|
343
|
+
const projViolations = violationMap.get(hook.id)?.count ?? 0;
|
|
344
|
+
const projOverrides = overrideMap.get(hook.id)?.count ?? 0;
|
|
345
|
+
const projLastTriggered = violationMap.get(hook.id)?.last_seen ?? null;
|
|
346
|
+
if (existing) {
|
|
347
|
+
// Sum stats across projects
|
|
348
|
+
existing.stats.violations += projViolations;
|
|
349
|
+
existing.stats.overrides += projOverrides;
|
|
350
|
+
if (projLastTriggered && (!existing.stats.last_triggered || projLastTriggered > existing.stats.last_triggered)) {
|
|
351
|
+
existing.stats.last_triggered = projLastTriggered;
|
|
352
|
+
}
|
|
353
|
+
// Union edges
|
|
354
|
+
const existingEdgeKeys = new Set(existing.edges.map((e) => `${e.from}->${e.to}`));
|
|
355
|
+
for (const edge of hookEdgeMap.get(hook.id) ?? []) {
|
|
356
|
+
if (!existingEdgeKeys.has(`${edge.from}->${edge.to}`)) {
|
|
357
|
+
existing.edges.push(edge);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// First time seeing this hook — count it in summary
|
|
363
|
+
if (hook.category === 'guard')
|
|
364
|
+
summary.guards++;
|
|
365
|
+
else if (hook.category === 'gate')
|
|
366
|
+
summary.gates++;
|
|
367
|
+
else if (hook.category === 'lifecycle')
|
|
368
|
+
summary.lifecycle++;
|
|
369
|
+
else if (hook.category === 'observer')
|
|
370
|
+
summary.observers++;
|
|
371
|
+
hookMap.set(hook.id, {
|
|
372
|
+
hook,
|
|
373
|
+
enforcement: getHookEnforcement(hook),
|
|
374
|
+
edges: hookEdgeMap.get(hook.id) ?? [],
|
|
375
|
+
stats: {
|
|
376
|
+
violations: projViolations,
|
|
377
|
+
overrides: projOverrides,
|
|
378
|
+
last_triggered: projLastTriggered,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
res.json({ summary, rules: [...hookMap.values()], totalEdges });
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
log.error('Enforcement rules failed', { error: String(err) });
|
|
388
|
+
res.status(500).json({ error: 'Failed to aggregate enforcement rules' });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
app.get('/api/orbital/aggregate/events/violations/trend', (req, res) => {
|
|
392
|
+
try {
|
|
393
|
+
const days = Number(req.query.days) || 30;
|
|
394
|
+
const merged = new Map();
|
|
395
|
+
for (const [, ctx] of projectManager.getAllContexts()) {
|
|
396
|
+
const trend = ctx.db.prepare(`SELECT date(timestamp) as day, JSON_EXTRACT(data, '$.rule') as rule, COUNT(*) as count
|
|
397
|
+
FROM events WHERE type = 'VIOLATION' AND timestamp >= datetime('now', ? || ' days')
|
|
398
|
+
GROUP BY day, rule ORDER BY day ASC`).all(`-${days}`);
|
|
399
|
+
for (const t of trend) {
|
|
400
|
+
const key = `${t.day}:${t.rule}`;
|
|
401
|
+
const existing = merged.get(key);
|
|
402
|
+
if (existing) {
|
|
403
|
+
existing.count += t.count;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
merged.set(key, { ...t });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const result = [...merged.values()].sort((a, b) => a.day.localeCompare(b.day));
|
|
411
|
+
res.json(result);
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
log.error('Violation trends failed', { error: String(err) });
|
|
415
|
+
res.status(500).json({ error: 'Failed to aggregate violation trends' });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
app.get('/api/orbital/aggregate/gates', (req, res) => {
|
|
419
|
+
try {
|
|
420
|
+
const scopeId = req.query.scope_id;
|
|
421
|
+
const filterProjectId = req.query.project_id;
|
|
422
|
+
const mergedGates = new Map();
|
|
423
|
+
for (const [projectId, ctx] of projectManager.getAllContexts()) {
|
|
424
|
+
if (filterProjectId && projectId !== filterProjectId)
|
|
425
|
+
continue;
|
|
426
|
+
const gates = scopeId
|
|
427
|
+
? ctx.gateService.getLatestForScope(Number(scopeId))
|
|
428
|
+
: ctx.gateService.getLatestRun();
|
|
429
|
+
for (const gate of gates) {
|
|
430
|
+
const existing = mergedGates.get(gate.gate_name);
|
|
431
|
+
if (!existing || gate.run_at > existing.run_at) {
|
|
432
|
+
mergedGates.set(gate.gate_name, { ...gate, project_id: projectId });
|
|
433
|
+
}
|
|
142
434
|
}
|
|
143
435
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
436
|
+
res.json([...mergedGates.values()]);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
log.error('Gates aggregation failed', { error: String(err) });
|
|
440
|
+
res.status(500).json({ error: 'Failed to aggregate gates' });
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
app.get('/api/orbital/aggregate/gates/stats', (_req, res) => {
|
|
444
|
+
try {
|
|
445
|
+
const merged = new Map();
|
|
446
|
+
for (const [, ctx] of projectManager.getAllContexts()) {
|
|
447
|
+
const stats = ctx.gateService.getStats();
|
|
448
|
+
for (const s of stats) {
|
|
449
|
+
const existing = merged.get(s.gate_name);
|
|
450
|
+
if (existing) {
|
|
451
|
+
existing.total += s.total;
|
|
452
|
+
existing.passed += s.passed;
|
|
453
|
+
existing.failed += s.failed;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
merged.set(s.gate_name, { ...s });
|
|
457
|
+
}
|
|
149
458
|
}
|
|
150
459
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
460
|
+
res.json([...merged.values()]);
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
log.error('Gate stats failed', { error: String(err) });
|
|
464
|
+
res.status(500).json({ error: 'Failed to aggregate gate stats' });
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
// ─── Aggregate: Git & GitHub ───────────────────────────────
|
|
468
|
+
app.get('/api/orbital/aggregate/git/overview', async (_req, res) => {
|
|
469
|
+
try {
|
|
470
|
+
const projects = projectManager.getProjectList();
|
|
471
|
+
const results = await Promise.allSettled(projects.filter(p => p.enabled && p.status === 'active').map(async (proj) => {
|
|
472
|
+
const ctx = projectManager.getContext(proj.id);
|
|
473
|
+
if (!ctx)
|
|
474
|
+
throw new Error('Project offline');
|
|
475
|
+
const config = ctx.workflowEngine.getConfig();
|
|
476
|
+
const overview = await ctx.gitService.getOverview(config.branchingMode ?? 'trunk');
|
|
477
|
+
return {
|
|
478
|
+
projectId: proj.id,
|
|
479
|
+
projectName: proj.name,
|
|
480
|
+
projectColor: proj.color,
|
|
481
|
+
status: 'ok',
|
|
482
|
+
overview,
|
|
483
|
+
};
|
|
484
|
+
}));
|
|
485
|
+
const overviews = results.map((r, i) => {
|
|
486
|
+
if (r.status === 'fulfilled')
|
|
487
|
+
return r.value;
|
|
488
|
+
const proj = projects.filter(p => p.enabled && p.status === 'active')[i];
|
|
489
|
+
return {
|
|
490
|
+
projectId: proj.id,
|
|
491
|
+
projectName: proj.name,
|
|
492
|
+
projectColor: proj.color,
|
|
493
|
+
status: 'error',
|
|
494
|
+
error: String(r.reason),
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
res.json(overviews);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
log.error('Git overviews failed', { error: String(err) });
|
|
501
|
+
res.status(500).json({ error: 'Failed to aggregate git overviews' });
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
app.get('/api/orbital/aggregate/git/commits', async (req, res) => {
|
|
505
|
+
try {
|
|
506
|
+
const limit = Number(req.query.limit) || 50;
|
|
507
|
+
const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
|
|
508
|
+
const results = await Promise.allSettled(projects.map(async (proj) => {
|
|
509
|
+
const ctx = projectManager.getContext(proj.id);
|
|
510
|
+
if (!ctx)
|
|
511
|
+
return [];
|
|
512
|
+
const commits = await ctx.gitService.getCommits({ limit });
|
|
513
|
+
return commits.map(c => ({
|
|
514
|
+
...c,
|
|
515
|
+
project_id: proj.id,
|
|
516
|
+
projectName: proj.name,
|
|
517
|
+
projectColor: proj.color,
|
|
518
|
+
}));
|
|
519
|
+
}));
|
|
520
|
+
const allCommits = [];
|
|
521
|
+
for (const r of results) {
|
|
522
|
+
if (r.status === 'fulfilled')
|
|
523
|
+
allCommits.push(...r.value);
|
|
155
524
|
}
|
|
525
|
+
allCommits.sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
|
526
|
+
res.json(allCommits.slice(0, limit));
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
log.error('Commits aggregation failed', { error: String(err) });
|
|
530
|
+
res.status(500).json({ error: 'Failed to aggregate commits' });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
app.get('/api/orbital/aggregate/github/prs', async (_req, res) => {
|
|
534
|
+
try {
|
|
535
|
+
const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
|
|
536
|
+
const results = await Promise.allSettled(projects.map(async (proj) => {
|
|
537
|
+
const ctx = projectManager.getContext(proj.id);
|
|
538
|
+
if (!ctx)
|
|
539
|
+
return [];
|
|
540
|
+
const prs = await ctx.githubService.getOpenPRs();
|
|
541
|
+
return prs.map(pr => ({
|
|
542
|
+
...pr,
|
|
543
|
+
project_id: proj.id,
|
|
544
|
+
projectName: proj.name,
|
|
545
|
+
projectColor: proj.color,
|
|
546
|
+
}));
|
|
547
|
+
}));
|
|
548
|
+
const allPrs = [];
|
|
549
|
+
for (const r of results) {
|
|
550
|
+
if (r.status === 'fulfilled')
|
|
551
|
+
allPrs.push(...r.value);
|
|
552
|
+
}
|
|
553
|
+
allPrs.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
|
|
554
|
+
res.json(allPrs);
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
log.error('PRs aggregation failed', { error: String(err) });
|
|
558
|
+
res.status(500).json({ error: 'Failed to aggregate PRs' });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
app.get('/api/orbital/aggregate/git/health', async (_req, res) => {
|
|
562
|
+
try {
|
|
563
|
+
const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
|
|
564
|
+
const results = await Promise.allSettled(projects.map(async (proj) => {
|
|
565
|
+
const ctx = projectManager.getContext(proj.id);
|
|
566
|
+
if (!ctx)
|
|
567
|
+
throw new Error('offline');
|
|
568
|
+
const branches = await ctx.gitService.getBranches();
|
|
569
|
+
const config = ctx.workflowEngine.getConfig();
|
|
570
|
+
const listsWithBranch = config.lists.filter(l => l.gitBranch).sort((a, b) => a.order - b.order);
|
|
571
|
+
const driftPairs = [];
|
|
572
|
+
for (let i = 0; i < listsWithBranch.length - 1; i++) {
|
|
573
|
+
driftPairs.push({ from: listsWithBranch[i].gitBranch, to: listsWithBranch[i + 1].gitBranch });
|
|
574
|
+
}
|
|
575
|
+
const drift = driftPairs.length > 0 ? await ctx.gitService.getDrift(driftPairs) : [];
|
|
576
|
+
const maxDrift = Math.max(0, ...drift.map(d => d.count));
|
|
577
|
+
const staleBranches = branches.filter(b => b.isStale && !b.isRemote);
|
|
578
|
+
return {
|
|
579
|
+
projectId: proj.id,
|
|
580
|
+
projectName: proj.name,
|
|
581
|
+
projectColor: proj.color,
|
|
582
|
+
branchCount: branches.filter(b => !b.isRemote).length,
|
|
583
|
+
staleBranchCount: staleBranches.length,
|
|
584
|
+
featureBranchCount: branches.filter(b => !b.isRemote && /(?:feat|fix|scope)[/-]/.test(b.name)).length,
|
|
585
|
+
maxDriftSeverity: maxDrift === 0 ? 'clean' : maxDrift <= 5 ? 'low' : maxDrift <= 20 ? 'moderate' : 'high',
|
|
586
|
+
};
|
|
587
|
+
}));
|
|
588
|
+
const health = [];
|
|
589
|
+
for (const r of results) {
|
|
590
|
+
if (r.status === 'fulfilled')
|
|
591
|
+
health.push(r.value);
|
|
592
|
+
}
|
|
593
|
+
res.json(health);
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
log.error('Branch health failed', { error: String(err) });
|
|
597
|
+
res.status(500).json({ error: 'Failed to aggregate branch health' });
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
app.get('/api/orbital/aggregate/git/activity', async (req, res) => {
|
|
601
|
+
try {
|
|
602
|
+
const days = Number(req.query.days) || 30;
|
|
603
|
+
const projects = projectManager.getProjectList().filter(p => p.enabled && p.status === 'active');
|
|
604
|
+
const results = await Promise.allSettled(projects.map(async (proj) => {
|
|
605
|
+
const ctx = projectManager.getContext(proj.id);
|
|
606
|
+
if (!ctx)
|
|
607
|
+
return { projectId: proj.id, series: [] };
|
|
608
|
+
const series = await ctx.gitService.getActivitySeries(days);
|
|
609
|
+
return { projectId: proj.id, projectName: proj.name, projectColor: proj.color, series };
|
|
610
|
+
}));
|
|
611
|
+
const activity = [];
|
|
612
|
+
for (const r of results) {
|
|
613
|
+
if (r.status === 'fulfilled')
|
|
614
|
+
activity.push(r.value);
|
|
615
|
+
}
|
|
616
|
+
res.json(activity);
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
log.error('Activity aggregation failed', { error: String(err) });
|
|
620
|
+
res.status(500).json({ error: 'Failed to aggregate activity' });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
app.get('/api/orbital/aggregate/scopes/:id/readiness', (req, res) => {
|
|
624
|
+
const scopeId = Number(req.params.id);
|
|
625
|
+
const projectId = req.query.project_id;
|
|
626
|
+
for (const [pid, ctx] of projectManager.getAllContexts()) {
|
|
627
|
+
if (projectId && pid !== projectId)
|
|
628
|
+
continue;
|
|
629
|
+
const scope = ctx.scopeService.getById(scopeId);
|
|
630
|
+
if (scope) {
|
|
631
|
+
const readiness = ctx.readinessService.getReadiness(scopeId);
|
|
632
|
+
if (readiness) {
|
|
633
|
+
res.json(readiness);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
res.status(404).json({ error: 'Scope not found in any project' });
|
|
639
|
+
});
|
|
640
|
+
app.get('/api/orbital/aggregate/dispatch/active-scopes', (_req, res) => {
|
|
641
|
+
const allActive = [];
|
|
642
|
+
const seenActive = new Set();
|
|
643
|
+
const allAbandoned = [];
|
|
644
|
+
const seenAbandoned = new Set();
|
|
645
|
+
for (const [projectId, ctx] of projectManager.getAllContexts()) {
|
|
646
|
+
const activeIds = getActiveScopeIds(ctx.db, ctx.scopeService, ctx.workflowEngine);
|
|
647
|
+
for (const id of activeIds) {
|
|
648
|
+
const key = `${projectId}::${id}`;
|
|
649
|
+
if (!seenActive.has(key)) {
|
|
650
|
+
seenActive.add(key);
|
|
651
|
+
allActive.push({ scope_id: id, project_id: projectId });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const abandoned = getAbandonedScopeIds(ctx.db, ctx.scopeService, ctx.workflowEngine, activeIds);
|
|
655
|
+
for (const entry of abandoned) {
|
|
656
|
+
const key = `${projectId}::${entry.scope_id}`;
|
|
657
|
+
if (!seenAbandoned.has(key)) {
|
|
658
|
+
seenAbandoned.add(key);
|
|
659
|
+
allAbandoned.push({ ...entry, project_id: projectId });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
res.json({ scope_ids: allActive, abandoned_scopes: allAbandoned });
|
|
664
|
+
});
|
|
665
|
+
app.get('/api/orbital/aggregate/dispatch/active', (req, res) => {
|
|
666
|
+
const scopeId = Number(req.query.scope_id);
|
|
667
|
+
if (isNaN(scopeId) || scopeId <= 0) {
|
|
668
|
+
res.status(400).json({ error: 'Valid scope_id query param required' });
|
|
156
669
|
return;
|
|
157
670
|
}
|
|
158
|
-
|
|
671
|
+
for (const [, ctx] of projectManager.getAllContexts()) {
|
|
672
|
+
const scope = ctx.scopeService.getById(scopeId);
|
|
673
|
+
if (!scope)
|
|
674
|
+
continue;
|
|
675
|
+
const active = ctx.db.prepare(`SELECT id, timestamp, JSON_EXTRACT(data, '$.command') as command
|
|
676
|
+
FROM events
|
|
677
|
+
WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
|
|
678
|
+
ORDER BY timestamp DESC LIMIT 1`).get(scopeId);
|
|
679
|
+
res.json({ active: active ?? null });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
res.json({ active: null });
|
|
159
683
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
684
|
+
// ─── Aggregate: Manifest Health ────────────────────────────
|
|
685
|
+
app.get('/api/orbital/aggregate/manifest/status', (_req, res) => {
|
|
686
|
+
try {
|
|
687
|
+
const projects = projectManager.getProjectList().filter(p => p.enabled);
|
|
688
|
+
const pkgVersion = getPackageVersion();
|
|
689
|
+
const projectOverviews = projects.map((proj) => {
|
|
690
|
+
const ctx = projectManager.getContext(proj.id);
|
|
691
|
+
if (!ctx) {
|
|
692
|
+
return {
|
|
693
|
+
projectId: proj.id,
|
|
694
|
+
projectName: proj.name,
|
|
695
|
+
projectColor: proj.color,
|
|
696
|
+
status: 'error',
|
|
697
|
+
manifest: null,
|
|
698
|
+
error: 'Project offline',
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const manifest = loadManifest(ctx.config.projectRoot);
|
|
703
|
+
if (!manifest) {
|
|
704
|
+
return {
|
|
705
|
+
projectId: proj.id,
|
|
706
|
+
projectName: proj.name,
|
|
707
|
+
projectColor: proj.color,
|
|
708
|
+
status: 'no-manifest',
|
|
709
|
+
manifest: null,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const claudeDir = path.join(ctx.config.projectRoot, '.claude');
|
|
713
|
+
refreshFileStatuses(manifest, claudeDir);
|
|
714
|
+
const summary = summarizeManifest(manifest);
|
|
715
|
+
return {
|
|
716
|
+
projectId: proj.id,
|
|
717
|
+
projectName: proj.name,
|
|
718
|
+
projectColor: proj.color,
|
|
719
|
+
status: 'ok',
|
|
720
|
+
manifest: {
|
|
721
|
+
exists: true,
|
|
722
|
+
packageVersion: pkgVersion,
|
|
723
|
+
installedVersion: manifest.packageVersion,
|
|
724
|
+
needsUpdate: manifest.packageVersion !== pkgVersion,
|
|
725
|
+
preset: manifest.preset,
|
|
726
|
+
files: summary,
|
|
727
|
+
lastUpdated: manifest.updatedAt,
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
return {
|
|
733
|
+
projectId: proj.id,
|
|
734
|
+
projectName: proj.name,
|
|
735
|
+
projectColor: proj.color,
|
|
736
|
+
status: 'error',
|
|
737
|
+
manifest: null,
|
|
738
|
+
error: String(err),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
const projectsUpToDate = projectOverviews.filter(p => p.status === 'ok' && !p.manifest?.needsUpdate).length;
|
|
743
|
+
const projectsOutdated = projectOverviews.filter(p => p.status === 'ok' && p.manifest?.needsUpdate).length;
|
|
744
|
+
const noManifest = projectOverviews.filter(p => p.status === 'no-manifest').length;
|
|
745
|
+
const totalOutdated = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.outdated ?? 0), 0);
|
|
746
|
+
const totalModified = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.modified ?? 0), 0);
|
|
747
|
+
const totalPinned = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.pinned ?? 0), 0);
|
|
748
|
+
const totalMissing = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.missing ?? 0), 0);
|
|
749
|
+
const totalSynced = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.synced ?? 0), 0);
|
|
750
|
+
const totalUserOwned = projectOverviews.reduce((sum, p) => sum + (p.manifest?.files.userOwned ?? 0), 0);
|
|
751
|
+
res.json({
|
|
752
|
+
total: projects.length,
|
|
753
|
+
projectsUpToDate,
|
|
754
|
+
projectsOutdated,
|
|
755
|
+
noManifest,
|
|
756
|
+
totalOutdated,
|
|
757
|
+
totalModified,
|
|
758
|
+
totalPinned,
|
|
759
|
+
totalMissing,
|
|
760
|
+
totalSynced,
|
|
761
|
+
totalUserOwned,
|
|
762
|
+
projects: projectOverviews,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
log.error('Manifest status failed', { error: String(err) });
|
|
767
|
+
res.status(500).json({ error: 'Failed to aggregate manifest status' });
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
app.post('/api/orbital/aggregate/manifest/update-all', (_req, res) => {
|
|
771
|
+
try {
|
|
772
|
+
const projects = projectManager.getProjectList().filter(p => p.enabled);
|
|
773
|
+
const pkgVersion = getPackageVersion();
|
|
774
|
+
const results = [];
|
|
775
|
+
for (const proj of projects) {
|
|
776
|
+
const ctx = projectManager.getContext(proj.id);
|
|
777
|
+
if (!ctx) {
|
|
778
|
+
results.push({ projectId: proj.id, success: false, error: 'Project offline' });
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const manifest = loadManifest(ctx.config.projectRoot);
|
|
782
|
+
if (!manifest)
|
|
783
|
+
continue; // uninitialized — skip
|
|
784
|
+
// Refresh statuses and check if there's anything to update
|
|
785
|
+
const claudeDir = path.join(ctx.config.projectRoot, '.claude');
|
|
786
|
+
refreshFileStatuses(manifest, claudeDir);
|
|
787
|
+
const manifestSummary = summarizeManifest(manifest);
|
|
788
|
+
if (manifest.packageVersion === pkgVersion && manifestSummary.outdated === 0 && manifestSummary.missing === 0) {
|
|
789
|
+
continue; // fully up to date
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
runUpdate(ctx.config.projectRoot, { dryRun: false });
|
|
793
|
+
ctx.emitter.emit('manifest:changed', { action: 'updated' });
|
|
794
|
+
results.push({ projectId: proj.id, success: true });
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
results.push({ projectId: proj.id, success: false, error: String(err) });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
res.json({ success: true, results });
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
log.error('Update all projects failed', { error: String(err) });
|
|
804
|
+
res.status(500).json({ error: 'Failed to update all projects' });
|
|
163
805
|
}
|
|
164
|
-
// Batch orchestrator tracks all status transitions (dev, staging, production)
|
|
165
|
-
batchOrchestrator.onScopeStatusChanged(scopeId, newStatus);
|
|
166
806
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
807
|
+
// ─── Aggregate: Config Primitives (Global) ────────────────
|
|
808
|
+
// In aggregate mode, config reads/writes target ~/.orbital/primitives/
|
|
809
|
+
// Writes propagate to all synced (non-overridden) projects via SyncService.
|
|
810
|
+
const globalConfigService = new ConfigService(GLOBAL_PRIMITIVES_DIR);
|
|
811
|
+
app.get('/api/orbital/aggregate/config/:type/tree', (req, res) => {
|
|
812
|
+
const type = req.params.type;
|
|
813
|
+
if (!isValidPrimitiveType(type)) {
|
|
814
|
+
res.status(400).json({ success: false, error: `Invalid type "${type}". Must be one of: agents, skills, hooks` });
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const basePath = path.join(GLOBAL_PRIMITIVES_DIR, type);
|
|
819
|
+
const tree = globalConfigService.scanDirectory(basePath);
|
|
820
|
+
res.json({ success: true, data: tree });
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
log.error('Config tree read failed', { type, error: String(err) });
|
|
824
|
+
res.status(500).json({ success: false, error: 'Failed to read global config tree' });
|
|
170
825
|
}
|
|
171
826
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
827
|
+
app.get('/api/orbital/aggregate/config/:type/file', (req, res) => {
|
|
828
|
+
const type = req.params.type;
|
|
829
|
+
if (!isValidPrimitiveType(type)) {
|
|
830
|
+
res.status(400).json({ success: false, error: `Invalid type "${type}". Must be one of: agents, skills, hooks` });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const filePath = req.query.path;
|
|
834
|
+
if (!filePath) {
|
|
835
|
+
res.status(400).json({ success: false, error: 'path query parameter is required' });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const basePath = path.join(GLOBAL_PRIMITIVES_DIR, type);
|
|
840
|
+
const content = globalConfigService.readFile(basePath, filePath);
|
|
841
|
+
res.json({ success: true, data: { path: filePath, content } });
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
845
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('ENOENT') || msg.includes('not found') ? 404 : 500;
|
|
846
|
+
res.status(status).json({ success: false, error: msg });
|
|
847
|
+
}
|
|
175
848
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
849
|
+
app.put('/api/orbital/aggregate/config/:type/file', (req, res) => {
|
|
850
|
+
const type = req.params.type;
|
|
851
|
+
if (!isValidPrimitiveType(type)) {
|
|
852
|
+
res.status(400).json({ success: false, error: `Invalid type "${type}". Must be one of: agents, skills, hooks` });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const { path: filePath, content } = req.body;
|
|
856
|
+
if (!filePath || content === undefined) {
|
|
857
|
+
res.status(400).json({ success: false, error: 'path and content are required' });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
const basePath = path.join(GLOBAL_PRIMITIVES_DIR, type);
|
|
862
|
+
globalConfigService.writeFile(basePath, filePath, content);
|
|
863
|
+
// Propagate to all synced projects
|
|
864
|
+
const relativePath = path.join(type, filePath);
|
|
865
|
+
const result = syncService.propagateGlobalChange(relativePath);
|
|
866
|
+
io.emit(`config:${type}:changed`, { action: 'updated', path: filePath, global: true });
|
|
867
|
+
res.json({ success: true, propagation: result });
|
|
868
|
+
}
|
|
869
|
+
catch (err) {
|
|
870
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
871
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : 500;
|
|
872
|
+
res.status(status).json({ success: false, error: msg });
|
|
873
|
+
}
|
|
185
874
|
});
|
|
186
|
-
|
|
187
|
-
app.use('/api/orbital', createDataRoutes({ db, io, gateService, deployService, engine: workflowEngine, projectRoot: config.projectRoot, inferScopeStatus }));
|
|
188
|
-
app.use('/api/orbital', createDispatchRoutes({ db, io, scopeService, projectRoot: config.projectRoot, engine: workflowEngine }));
|
|
189
|
-
app.use('/api/orbital', createSprintRoutes({ sprintService, sprintOrchestrator, batchOrchestrator }));
|
|
190
|
-
app.use('/api/orbital', createWorkflowRoutes({ workflowService, projectRoot: config.projectRoot }));
|
|
191
|
-
app.use('/api/orbital', createConfigRoutes({ projectRoot: config.projectRoot, workflowService, io }));
|
|
192
|
-
app.use('/api/orbital', createGitRoutes({ gitService, githubService, engine: workflowEngine }));
|
|
193
|
-
app.use('/api/orbital', createVersionRoutes({ io }));
|
|
194
|
-
// ─── Static File Serving (production) ───────────────────────
|
|
195
|
-
// Resolve the Vite-built frontend dist directory (server/ → ../dist).
|
|
875
|
+
// ─── Static File Serving ─────────────────────────────────
|
|
196
876
|
const __selfDir = path.dirname(fileURLToPath(import.meta.url));
|
|
197
877
|
const distDir = path.resolve(__selfDir, '../dist');
|
|
198
|
-
|
|
878
|
+
const devMode = clientPort !== port;
|
|
879
|
+
const hasBuiltFrontend = !devMode && fs.existsSync(path.join(distDir, 'index.html'));
|
|
880
|
+
if (hasBuiltFrontend) {
|
|
199
881
|
app.use(express.static(distDir));
|
|
200
882
|
app.get('*', (req, res, next) => {
|
|
201
883
|
if (req.path.startsWith('/api/') || req.path.startsWith('/socket.io'))
|
|
@@ -204,33 +886,48 @@ export async function startServer(overrides) {
|
|
|
204
886
|
});
|
|
205
887
|
}
|
|
206
888
|
else {
|
|
207
|
-
|
|
208
|
-
app.get('/', (_req, res) => res.redirect(`http://localhost:${config.clientPort}`));
|
|
889
|
+
app.get('/', (_req, res) => res.redirect(`http://localhost:${clientPort}`));
|
|
209
890
|
}
|
|
210
|
-
// ─── Socket.io
|
|
891
|
+
// ─── Socket.io ───────────────────────────────────────────
|
|
211
892
|
io.on('connection', (socket) => {
|
|
212
893
|
log.debug('Client connected', { socketId: socket.id });
|
|
894
|
+
socket.on('subscribe', (payload) => {
|
|
895
|
+
if (payload.scope === 'all') {
|
|
896
|
+
socket.join('all-projects');
|
|
897
|
+
}
|
|
898
|
+
else if (payload.projectId) {
|
|
899
|
+
socket.join(`project:${payload.projectId}`);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
socket.on('unsubscribe', (payload) => {
|
|
903
|
+
if (payload.scope === 'all') {
|
|
904
|
+
socket.leave('all-projects');
|
|
905
|
+
}
|
|
906
|
+
else if (payload.projectId) {
|
|
907
|
+
socket.leave(`project:${payload.projectId}`);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
213
910
|
socket.on('disconnect', () => {
|
|
214
911
|
log.debug('Client disconnected', { socketId: socket.id });
|
|
215
912
|
});
|
|
216
913
|
});
|
|
217
|
-
// ───
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
914
|
+
// ─── Error Handling Middleware ─────────────────────────────
|
|
915
|
+
// Catches unhandled errors thrown from route handlers.
|
|
916
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
917
|
+
app.use((err, _req, res, _next) => {
|
|
918
|
+
log.error('Unhandled route error', { error: err.message, stack: err.stack });
|
|
919
|
+
if (!res.headersSent) {
|
|
920
|
+
res.status(500).json({ ok: false, error: 'Internal server error' });
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
// ─── Start Listening ─────────────────────────────────────
|
|
225
924
|
const actualPort = await new Promise((resolve, reject) => {
|
|
226
925
|
let attempt = 0;
|
|
227
926
|
const maxAttempts = 10;
|
|
228
927
|
httpServer.on('error', (err) => {
|
|
229
928
|
if (err.code === 'EADDRINUSE' && attempt < maxAttempts) {
|
|
230
929
|
attempt++;
|
|
231
|
-
|
|
232
|
-
log.warn('Port in use, trying next', { tried: port + attempt - 1, next: nextPort });
|
|
233
|
-
httpServer.listen(nextPort);
|
|
930
|
+
httpServer.listen(port + attempt);
|
|
234
931
|
}
|
|
235
932
|
else {
|
|
236
933
|
reject(new Error(`Failed to start server: ${err.message}`));
|
|
@@ -238,125 +935,58 @@ export async function startServer(overrides) {
|
|
|
238
935
|
});
|
|
239
936
|
httpServer.on('listening', () => {
|
|
240
937
|
const addr = httpServer.address();
|
|
241
|
-
|
|
242
|
-
resolve(listenPort);
|
|
938
|
+
resolve(typeof addr === 'object' && addr ? addr.port : port);
|
|
243
939
|
});
|
|
244
940
|
httpServer.listen(port);
|
|
245
941
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
// Resolve stale dispatch events (terminal scopes + age-based)
|
|
250
|
-
const staleResolved = resolveStaleDispatches(db, io, scopeService, workflowEngine);
|
|
251
|
-
if (staleResolved > 0) {
|
|
252
|
-
log.info('Resolved stale dispatch events', { count: staleResolved });
|
|
253
|
-
}
|
|
254
|
-
// Write iTerm2 dispatch profiles (idempotent, fire-and-forget)
|
|
255
|
-
ensureDynamicProfiles(workflowEngine);
|
|
256
|
-
// Start file watchers
|
|
257
|
-
scopeWatcher = startScopeWatcher(config.scopesDir, scopeService);
|
|
258
|
-
eventWatcher = startEventWatcher(config.eventsDir, eventService);
|
|
259
|
-
// Recover any active sprints/batches from before server restart
|
|
260
|
-
sprintOrchestrator.recoverActiveSprints().catch(err => log.error('Sprint recovery failed', { error: err.message }));
|
|
261
|
-
batchOrchestrator.recoverActiveBatches().catch(err => log.error('Batch recovery failed', { error: err.message }));
|
|
262
|
-
// Resolve stale batches on startup (catches stuck dispatches from previous runs)
|
|
263
|
-
const staleBatchesResolved = batchOrchestrator.resolveStaleBatches();
|
|
264
|
-
if (staleBatchesResolved > 0) {
|
|
265
|
-
log.info('Resolved stale batches', { count: staleBatchesResolved });
|
|
266
|
-
}
|
|
267
|
-
// Poll active batch PIDs every 30s for two-phase completion (B-1)
|
|
268
|
-
batchRecoveryInterval = setInterval(() => {
|
|
269
|
-
batchOrchestrator.recoverActiveBatches().catch(err => log.error('Batch recovery failed', { error: err.message }));
|
|
270
|
-
}, 30_000);
|
|
271
|
-
// Periodic stale dispatch + batch cleanup (crash recovery — catches SIGKILL'd sessions)
|
|
272
|
-
staleCleanupInterval = setInterval(() => {
|
|
273
|
-
const count = resolveStaleDispatches(db, io, scopeService, workflowEngine);
|
|
274
|
-
if (count > 0) {
|
|
275
|
-
log.info('Periodic cleanup: resolved stale dispatches', { count });
|
|
276
|
-
}
|
|
277
|
-
const batchCount = batchOrchestrator.resolveStaleBatches();
|
|
278
|
-
if (batchCount > 0) {
|
|
279
|
-
log.info('Periodic cleanup: resolved stale batches', { count: batchCount });
|
|
280
|
-
}
|
|
281
|
-
}, 30_000);
|
|
282
|
-
// Sync frontmatter-derived sessions into DB (non-blocking)
|
|
283
|
-
syncClaudeSessionsToDB(db, scopeService).then((count) => {
|
|
284
|
-
log.info('Synced frontmatter sessions', { count });
|
|
285
|
-
// Purge legacy pattern-matched rows (no action = old regex system)
|
|
286
|
-
const purged = db.prepare("DELETE FROM sessions WHERE action IS NULL AND id LIKE 'claude-%'").run();
|
|
287
|
-
if (purged.changes > 0) {
|
|
288
|
-
log.info('Purged legacy pattern-matched session rows', { count: purged.changes });
|
|
289
|
-
}
|
|
290
|
-
}).catch(err => log.error('Session sync failed', { error: err.message }));
|
|
291
|
-
// Re-sync every 5 minutes so new sessions appear without restart
|
|
292
|
-
sessionSyncInterval = setInterval(() => {
|
|
293
|
-
syncClaudeSessionsToDB(db, scopeService)
|
|
294
|
-
.then((count) => {
|
|
295
|
-
if (count > 0)
|
|
296
|
-
io.emit('session:updated', { type: 'resync', count });
|
|
297
|
-
})
|
|
298
|
-
.catch(err => log.error('Session resync failed', { error: err.message }));
|
|
299
|
-
}, 5 * 60 * 1000);
|
|
300
|
-
// Poll git status every 10s — emit socket event on change
|
|
301
|
-
let lastGitHash = '';
|
|
302
|
-
gitPollInterval = setInterval(async () => {
|
|
303
|
-
try {
|
|
304
|
-
const hash = await gitService.getStatusHash();
|
|
305
|
-
if (lastGitHash && hash !== lastGitHash) {
|
|
306
|
-
gitService.clearCache();
|
|
307
|
-
io.emit('git:status:changed');
|
|
308
|
-
}
|
|
309
|
-
lastGitHash = hash;
|
|
310
|
-
}
|
|
311
|
-
catch { /* ok */ }
|
|
312
|
-
}, 10_000);
|
|
942
|
+
const projectList = projectManager.getProjectList();
|
|
943
|
+
const projectLines = projectList.map(p => `║ ${p.status === 'active' ? '●' : '○'} ${p.name.padEnd(20)} ${String(p.scopeCount).padStart(3)} scopes ${p.status.padEnd(8)} ║`).join('\n');
|
|
944
|
+
const dashboardPort = devMode ? clientPort : actualPort;
|
|
313
945
|
// eslint-disable-next-line no-console
|
|
314
946
|
console.log(`
|
|
315
947
|
╔══════════════════════════════════════════════════════╗
|
|
316
|
-
║ Orbital Command
|
|
317
|
-
║ ${config.projectName.padEnd(42)} ║
|
|
948
|
+
║ Orbital Command — Central Server ║
|
|
318
949
|
║ ║
|
|
319
|
-
║ >>> Open: http://localhost:${
|
|
950
|
+
║ >>> Open: http://localhost:${String(dashboardPort).padEnd(25)} <<<║
|
|
320
951
|
║ ║
|
|
321
952
|
╠══════════════════════════════════════════════════════╣
|
|
322
|
-
|
|
953
|
+
${projectLines}
|
|
954
|
+
╠══════════════════════════════════════════════════════╣
|
|
323
955
|
║ API: http://localhost:${actualPort}/api/orbital/* ║
|
|
324
956
|
║ Socket.io: ws://localhost:${actualPort} ║
|
|
957
|
+
║ Home: ${ORBITAL_HOME.padEnd(39)} ║
|
|
325
958
|
╚══════════════════════════════════════════════════════╝
|
|
326
959
|
`);
|
|
327
|
-
// ─── Graceful Shutdown
|
|
960
|
+
// ─── Graceful Shutdown ───────────────────────────────────
|
|
328
961
|
let shuttingDown = false;
|
|
329
|
-
function shutdown() {
|
|
962
|
+
async function shutdown() {
|
|
330
963
|
if (shuttingDown)
|
|
331
|
-
return
|
|
964
|
+
return;
|
|
332
965
|
shuttingDown = true;
|
|
333
|
-
log.info('Shutting down');
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
clearInterval(staleCleanupInterval);
|
|
338
|
-
clearInterval(sessionSyncInterval);
|
|
339
|
-
clearInterval(gitPollInterval);
|
|
966
|
+
log.info('Shutting down central server');
|
|
967
|
+
if (globalWatcher)
|
|
968
|
+
await globalWatcher.close();
|
|
969
|
+
await projectManager.shutdownAll();
|
|
340
970
|
return new Promise((resolve) => {
|
|
341
|
-
const forceTimeout = setTimeout(
|
|
342
|
-
closeDatabase();
|
|
343
|
-
resolve();
|
|
344
|
-
}, 2000);
|
|
971
|
+
const forceTimeout = setTimeout(resolve, 2000);
|
|
345
972
|
io.close(() => {
|
|
346
973
|
clearTimeout(forceTimeout);
|
|
347
|
-
closeDatabase();
|
|
348
974
|
resolve();
|
|
349
975
|
});
|
|
350
976
|
});
|
|
351
977
|
}
|
|
352
|
-
return { app, io,
|
|
978
|
+
return { app, io, projectManager, syncService, httpServer, shutdown };
|
|
353
979
|
}
|
|
354
980
|
// ─── Direct Execution (backward compat: tsx watch server/index.ts) ───
|
|
355
981
|
const isDirectRun = process.argv[1] && (process.argv[1].endsWith('server/index.ts') ||
|
|
356
982
|
process.argv[1].endsWith('server/index.js') ||
|
|
357
983
|
process.argv[1].endsWith('server'));
|
|
358
984
|
if (isDirectRun) {
|
|
359
|
-
|
|
985
|
+
const projectRoot = process.env.ORBITAL_PROJECT_ROOT || process.cwd();
|
|
986
|
+
startCentralServer({
|
|
987
|
+
port: Number(process.env.ORBITAL_SERVER_PORT) || 4444,
|
|
988
|
+
autoRegisterPath: projectRoot,
|
|
989
|
+
}).then(({ shutdown }) => {
|
|
360
990
|
process.on('SIGINT', async () => {
|
|
361
991
|
await shutdown();
|
|
362
992
|
process.exit(0);
|