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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update planner — computes a structured diff between the current manifest
|
|
3
|
+
* and the new template set. Produces an UpdatePlan that can be executed
|
|
4
|
+
* or printed as a dry-run.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { buildTemplateInventory } from './manifest.js';
|
|
9
|
+
// ─── Version Comparison ─────────────────────────────────────
|
|
10
|
+
/** Parse a semver string into [major, minor, patch]. Returns null if invalid. */
|
|
11
|
+
function parseSemver(version) {
|
|
12
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
|
|
16
|
+
}
|
|
17
|
+
/** Compare two semver strings. Returns -1, 0, or 1. */
|
|
18
|
+
function compareSemver(a, b) {
|
|
19
|
+
const pa = parseSemver(a);
|
|
20
|
+
const pb = parseSemver(b);
|
|
21
|
+
if (!pa || !pb)
|
|
22
|
+
return 0;
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
if (pa[i] < pb[i])
|
|
25
|
+
return -1;
|
|
26
|
+
if (pa[i] > pb[i])
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load and chain rename maps between two versions.
|
|
33
|
+
* Returns Map<oldPath, newPath> with all renames chained.
|
|
34
|
+
*/
|
|
35
|
+
export function loadRenameMap(templatesDir, fromVersion, toVersion) {
|
|
36
|
+
const renamesPath = path.join(templatesDir, 'migrations', 'renames.json');
|
|
37
|
+
if (!fs.existsSync(renamesPath))
|
|
38
|
+
return new Map();
|
|
39
|
+
let rawMap;
|
|
40
|
+
try {
|
|
41
|
+
rawMap = JSON.parse(fs.readFileSync(renamesPath, 'utf-8'));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return new Map();
|
|
45
|
+
}
|
|
46
|
+
// Collect all applicable rename entries, ordered by target version
|
|
47
|
+
const applicable = [];
|
|
48
|
+
for (const [range, renames] of Object.entries(rawMap)) {
|
|
49
|
+
const parts = range.split('->');
|
|
50
|
+
if (parts.length !== 2)
|
|
51
|
+
continue;
|
|
52
|
+
const [from, to] = parts.map(s => s.trim());
|
|
53
|
+
if (!parseSemver(from) || !parseSemver(to))
|
|
54
|
+
continue;
|
|
55
|
+
// Include if: from > fromVersion (hasn't been applied) AND to <= toVersion
|
|
56
|
+
if (compareSemver(from, fromVersion) <= 0 || compareSemver(to, toVersion) > 0)
|
|
57
|
+
continue;
|
|
58
|
+
applicable.push({ to, renames });
|
|
59
|
+
}
|
|
60
|
+
// Sort by target version ascending
|
|
61
|
+
applicable.sort((a, b) => compareSemver(a.to, b.to));
|
|
62
|
+
// Chain renames: if A→B in v1 and B→C in v2, result is A→C
|
|
63
|
+
const chained = new Map();
|
|
64
|
+
for (const { renames } of applicable) {
|
|
65
|
+
for (const [oldPath, newPath] of Object.entries(renames)) {
|
|
66
|
+
// Check if oldPath is itself a rename target from a previous version
|
|
67
|
+
let originalPath = oldPath;
|
|
68
|
+
for (const [prevOld, prevNew] of chained) {
|
|
69
|
+
if (prevNew === oldPath) {
|
|
70
|
+
originalPath = prevOld;
|
|
71
|
+
chained.delete(prevOld);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
chained.set(originalPath, newPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return chained;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Compute the update plan by diffing manifest against current templates.
|
|
82
|
+
* Does NOT modify any files — pure computation.
|
|
83
|
+
*/
|
|
84
|
+
export function computeUpdatePlan(options) {
|
|
85
|
+
const { templatesDir, claudeDir, manifest, newVersion } = options;
|
|
86
|
+
const renameMap = options.renameMap ??
|
|
87
|
+
loadRenameMap(templatesDir, manifest.packageVersion, newVersion);
|
|
88
|
+
const templateInventory = buildTemplateInventory(templatesDir);
|
|
89
|
+
const toAdd = [];
|
|
90
|
+
const toUpdate = [];
|
|
91
|
+
const toRemove = [];
|
|
92
|
+
const toRename = [];
|
|
93
|
+
const toSkip = [];
|
|
94
|
+
// Build reverse rename map: newPath → oldPath
|
|
95
|
+
const reverseRenames = new Map();
|
|
96
|
+
for (const [oldPath, newPath] of renameMap) {
|
|
97
|
+
reverseRenames.set(newPath, oldPath);
|
|
98
|
+
}
|
|
99
|
+
// 1. Check each template file against the manifest
|
|
100
|
+
for (const [templatePath, templateHash] of templateInventory) {
|
|
101
|
+
// Is this file the target of a rename?
|
|
102
|
+
const oldPath = reverseRenames.get(templatePath);
|
|
103
|
+
if (oldPath && manifest.files[oldPath]) {
|
|
104
|
+
toRename.push({ from: oldPath, to: templatePath });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const record = manifest.files[templatePath];
|
|
108
|
+
if (!record) {
|
|
109
|
+
// New file — not in manifest at all
|
|
110
|
+
toAdd.push(templatePath);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (record.origin === 'user') {
|
|
114
|
+
// A user file occupies a path that now conflicts with a template.
|
|
115
|
+
// Skip it and warn. This is an unusual edge case.
|
|
116
|
+
toSkip.push({ file: templatePath, reason: 'modified', newTemplateHash: templateHash });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (record.status === 'pinned') {
|
|
120
|
+
// Update the template hash in manifest but don't touch the file
|
|
121
|
+
toSkip.push({ file: templatePath, reason: 'pinned', newTemplateHash: templateHash });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (record.status === 'modified') {
|
|
125
|
+
// User modified a template file — skip it
|
|
126
|
+
toSkip.push({ file: templatePath, reason: 'modified', newTemplateHash: templateHash });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (record.status === 'outdated') {
|
|
130
|
+
// File content doesn't match template but user hasn't edited it — safe to update
|
|
131
|
+
toUpdate.push(templatePath);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// synced file — update only if template changed since last sync
|
|
135
|
+
if (record.templateHash !== templateHash) {
|
|
136
|
+
toUpdate.push(templatePath);
|
|
137
|
+
}
|
|
138
|
+
// else: already up to date
|
|
139
|
+
}
|
|
140
|
+
// 2. Check for files in manifest that are no longer in templates
|
|
141
|
+
for (const [filePath, record] of Object.entries(manifest.files)) {
|
|
142
|
+
if (record.origin !== 'template')
|
|
143
|
+
continue;
|
|
144
|
+
// Skip if it's in the template inventory (already handled above)
|
|
145
|
+
if (templateInventory.has(filePath))
|
|
146
|
+
continue;
|
|
147
|
+
// Skip if it's the old side of a rename (will be handled in rename step)
|
|
148
|
+
if (renameMap.has(filePath))
|
|
149
|
+
continue;
|
|
150
|
+
toRemove.push(filePath);
|
|
151
|
+
}
|
|
152
|
+
// 3. Compute settings hook changes
|
|
153
|
+
const settingsChanges = computeSettingsChanges(claudeDir, templatesDir);
|
|
154
|
+
// 4. Check for pending config migrations (placeholder — computed by config-migrator)
|
|
155
|
+
const pendingMigrations = [];
|
|
156
|
+
const isEmpty = toAdd.length === 0 &&
|
|
157
|
+
toUpdate.length === 0 &&
|
|
158
|
+
toRemove.length === 0 &&
|
|
159
|
+
toRename.length === 0 &&
|
|
160
|
+
settingsChanges.hooksToAdd.length === 0 &&
|
|
161
|
+
settingsChanges.hooksToRemove.length === 0 &&
|
|
162
|
+
pendingMigrations.length === 0;
|
|
163
|
+
return {
|
|
164
|
+
toAdd,
|
|
165
|
+
toUpdate,
|
|
166
|
+
toRemove,
|
|
167
|
+
toRename,
|
|
168
|
+
toSkip,
|
|
169
|
+
settingsChanges,
|
|
170
|
+
pendingMigrations,
|
|
171
|
+
isEmpty,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// ─── Settings Changes ───────────────────────────────────────
|
|
175
|
+
/** Compute which _orbital hooks need to be added or removed from settings.local.json. */
|
|
176
|
+
function computeSettingsChanges(claudeDir, templatesDir) {
|
|
177
|
+
const settingsPath = path.join(claudeDir, 'settings.local.json');
|
|
178
|
+
const templatePath = path.join(templatesDir, 'settings-hooks.json');
|
|
179
|
+
if (!fs.existsSync(templatePath)) {
|
|
180
|
+
return { hooksToAdd: [], hooksToRemove: [] };
|
|
181
|
+
}
|
|
182
|
+
// Extract all hook commands from the template
|
|
183
|
+
const newCommands = extractHookCommands(templatePath);
|
|
184
|
+
// Extract all _orbital hook commands from current settings
|
|
185
|
+
const currentCommands = new Set();
|
|
186
|
+
if (fs.existsSync(settingsPath)) {
|
|
187
|
+
try {
|
|
188
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
189
|
+
if (settings.hooks) {
|
|
190
|
+
for (const groups of Object.values(settings.hooks)) {
|
|
191
|
+
for (const group of groups) {
|
|
192
|
+
for (const hook of group.hooks || []) {
|
|
193
|
+
if (hook._orbital)
|
|
194
|
+
currentCommands.add(hook.command);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch { /* malformed settings */ }
|
|
201
|
+
}
|
|
202
|
+
const hooksToAdd = [...newCommands].filter(c => !currentCommands.has(c));
|
|
203
|
+
const hooksToRemove = [...currentCommands].filter(c => !newCommands.has(c));
|
|
204
|
+
return { hooksToAdd, hooksToRemove };
|
|
205
|
+
}
|
|
206
|
+
/** Extract all hook command strings from a settings-hooks.json template. */
|
|
207
|
+
function extractHookCommands(templatePath) {
|
|
208
|
+
const commands = new Set();
|
|
209
|
+
try {
|
|
210
|
+
const template = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
|
|
211
|
+
if (template.hooks) {
|
|
212
|
+
for (const groups of Object.values(template.hooks)) {
|
|
213
|
+
for (const group of groups) {
|
|
214
|
+
for (const hook of group.hooks || []) {
|
|
215
|
+
if (hook.command)
|
|
216
|
+
commands.add(hook.command);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { /* malformed template */ }
|
|
223
|
+
return commands;
|
|
224
|
+
}
|
|
225
|
+
// ─── Plan Formatting ────────────────────────────────────────
|
|
226
|
+
/** Format an update plan as a human-readable string for dry-run output. */
|
|
227
|
+
export function formatPlan(plan, oldVersion, newVersion) {
|
|
228
|
+
const lines = [];
|
|
229
|
+
lines.push(`Orbital Command — update plan (dry run)\n`);
|
|
230
|
+
lines.push(`Package version: ${oldVersion} → ${newVersion}\n`);
|
|
231
|
+
if (plan.isEmpty) {
|
|
232
|
+
lines.push(' Everything up to date. No changes needed.\n');
|
|
233
|
+
return lines.join('\n');
|
|
234
|
+
}
|
|
235
|
+
for (const file of plan.toAdd) {
|
|
236
|
+
lines.push(` ADD ${file}`);
|
|
237
|
+
}
|
|
238
|
+
for (const file of plan.toUpdate) {
|
|
239
|
+
lines.push(` UPDATE ${file}`);
|
|
240
|
+
}
|
|
241
|
+
for (const { from, to } of plan.toRename) {
|
|
242
|
+
lines.push(` RENAME ${from} → ${to}`);
|
|
243
|
+
}
|
|
244
|
+
for (const file of plan.toRemove) {
|
|
245
|
+
lines.push(` REMOVE ${file}`);
|
|
246
|
+
}
|
|
247
|
+
for (const { file, reason } of plan.toSkip) {
|
|
248
|
+
lines.push(` SKIP ${file} (${reason})`);
|
|
249
|
+
}
|
|
250
|
+
if (plan.settingsChanges.hooksToAdd.length > 0 || plan.settingsChanges.hooksToRemove.length > 0) {
|
|
251
|
+
lines.push('');
|
|
252
|
+
lines.push(` SETTINGS hooks to add: ${plan.settingsChanges.hooksToAdd.length}, to remove: ${plan.settingsChanges.hooksToRemove.length}`);
|
|
253
|
+
}
|
|
254
|
+
if (plan.pendingMigrations.length > 0) {
|
|
255
|
+
lines.push(` CONFIG migrations: ${plan.pendingMigrations.length}`);
|
|
256
|
+
}
|
|
257
|
+
lines.push('');
|
|
258
|
+
lines.push(' REGEN INDEX.md, workflow-manifest.sh');
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push('No changes made. Run without --dry-run to apply.');
|
|
261
|
+
return lines.join('\n');
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the list of files that need to be backed up before executing a plan.
|
|
265
|
+
* These are files that will be overwritten or deleted.
|
|
266
|
+
*/
|
|
267
|
+
export function getFilesToBackup(plan) {
|
|
268
|
+
const files = [];
|
|
269
|
+
for (const file of plan.toUpdate) {
|
|
270
|
+
files.push(file);
|
|
271
|
+
}
|
|
272
|
+
for (const file of plan.toRemove) {
|
|
273
|
+
files.push(file);
|
|
274
|
+
}
|
|
275
|
+
for (const { from } of plan.toRename) {
|
|
276
|
+
files.push(from);
|
|
277
|
+
}
|
|
278
|
+
return files;
|
|
279
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createLogger } from './logger.js';
|
|
3
|
+
const log = createLogger('config');
|
|
2
4
|
const CC_HOOK_EVENTS = ['SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse'];
|
|
3
5
|
function extractScriptPath(command) {
|
|
4
6
|
// Strip "$CLAUDE_PROJECT_DIR"/ prefix and quotes
|
|
@@ -17,6 +19,7 @@ export function parseCcHooks(settingsPath) {
|
|
|
17
19
|
raw = readFileSync(settingsPath, 'utf-8');
|
|
18
20
|
}
|
|
19
21
|
catch {
|
|
22
|
+
log.debug('Settings file not found', { path: settingsPath });
|
|
20
23
|
return [];
|
|
21
24
|
}
|
|
22
25
|
const settings = JSON.parse(raw);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { parseCcHooks } from './cc-hooks-parser.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
describe('parseCcHooks', () => {
|
|
7
|
+
let tmpFile;
|
|
8
|
+
function writeSettings(data) {
|
|
9
|
+
tmpFile = path.join(os.tmpdir(), `test-settings-${Date.now()}.json`);
|
|
10
|
+
fs.writeFileSync(tmpFile, JSON.stringify(data));
|
|
11
|
+
return tmpFile;
|
|
12
|
+
}
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (tmpFile && fs.existsSync(tmpFile))
|
|
15
|
+
fs.unlinkSync(tmpFile);
|
|
16
|
+
});
|
|
17
|
+
it('parses all 4 event types', () => {
|
|
18
|
+
const file = writeSettings({
|
|
19
|
+
hooks: {
|
|
20
|
+
SessionStart: [{ hooks: [{ type: 'command', command: '$CLAUDE_PROJECT_DIR/.claude/hooks/init.sh', statusMessage: 'Initializing' }] }],
|
|
21
|
+
SessionEnd: [{ hooks: [{ type: 'command', command: '$CLAUDE_PROJECT_DIR/.claude/hooks/end.sh' }] }],
|
|
22
|
+
PreToolUse: [{ matcher: 'Edit', hooks: [{ type: 'command', command: '$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh', statusMessage: 'Checking' }] }],
|
|
23
|
+
PostToolUse: [{ hooks: [{ type: 'command', command: '$CLAUDE_PROJECT_DIR/.claude/hooks/log.sh' }] }],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const hooks = parseCcHooks(file);
|
|
27
|
+
expect(hooks).toHaveLength(4);
|
|
28
|
+
expect(hooks.map(h => h.event)).toEqual(['SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse']);
|
|
29
|
+
});
|
|
30
|
+
it('extracts script path stripping $CLAUDE_PROJECT_DIR', () => {
|
|
31
|
+
const file = writeSettings({
|
|
32
|
+
hooks: {
|
|
33
|
+
SessionStart: [{ hooks: [{ type: 'command', command: '$CLAUDE_PROJECT_DIR/.claude/hooks/init-session.sh' }] }],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const hooks = parseCcHooks(file);
|
|
37
|
+
expect(hooks[0].scriptPath).toBe('.claude/hooks/init-session.sh');
|
|
38
|
+
expect(hooks[0].scriptName).toBe('init-session.sh');
|
|
39
|
+
});
|
|
40
|
+
it('derives ID from script filename', () => {
|
|
41
|
+
const file = writeSettings({
|
|
42
|
+
hooks: {
|
|
43
|
+
SessionStart: [{ hooks: [{ type: 'command', command: '$CLAUDE_PROJECT_DIR/.claude/hooks/init-session.sh' }] }],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const hooks = parseCcHooks(file);
|
|
47
|
+
expect(hooks[0].id).toBe('init-session');
|
|
48
|
+
});
|
|
49
|
+
it('extracts matcher from group', () => {
|
|
50
|
+
const file = writeSettings({
|
|
51
|
+
hooks: {
|
|
52
|
+
PreToolUse: [{ matcher: 'Edit', hooks: [{ type: 'command', command: 'test.sh' }] }],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const hooks = parseCcHooks(file);
|
|
56
|
+
expect(hooks[0].matcher).toBe('Edit');
|
|
57
|
+
});
|
|
58
|
+
it('sets matcher to null when absent', () => {
|
|
59
|
+
const file = writeSettings({
|
|
60
|
+
hooks: {
|
|
61
|
+
SessionStart: [{ hooks: [{ type: 'command', command: 'test.sh' }] }],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const hooks = parseCcHooks(file);
|
|
65
|
+
expect(hooks[0].matcher).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
it('skips non-command entries', () => {
|
|
68
|
+
const file = writeSettings({
|
|
69
|
+
hooks: {
|
|
70
|
+
SessionStart: [{ hooks: [
|
|
71
|
+
{ type: 'command', command: 'test.sh' },
|
|
72
|
+
{ type: 'url', command: 'https://example.com' },
|
|
73
|
+
] }],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const hooks = parseCcHooks(file);
|
|
77
|
+
expect(hooks).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
it('returns empty array for file not found', () => {
|
|
80
|
+
expect(parseCcHooks('/tmp/nonexistent-settings.json')).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
it('returns empty array when no hooks key in settings', () => {
|
|
83
|
+
const file = writeSettings({ other: 'config' });
|
|
84
|
+
expect(parseCcHooks(file)).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -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,34 +117,31 @@ 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
|
-
/** Fallback age threshold for dispatches without a linked PID (
|
|
95
|
-
const STALE_AGE_MS =
|
|
143
|
+
/** Fallback age threshold for dispatches without a linked PID (10 minutes). */
|
|
144
|
+
const 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. */
|
|
@@ -146,6 +195,7 @@ export function getActiveScopeIds(db, scopeService, engine) {
|
|
|
146
195
|
batchData = JSON.parse(batchRow.data);
|
|
147
196
|
}
|
|
148
197
|
catch {
|
|
198
|
+
log.warn('Skipping unparseable batch dispatch event data', { data: batchRow.data });
|
|
149
199
|
continue;
|
|
150
200
|
}
|
|
151
201
|
const scopeIds = batchData.scope_ids;
|
|
@@ -176,10 +226,11 @@ export function getActiveScopeIds(db, scopeService, engine) {
|
|
|
176
226
|
* 3. No linked PID and dispatch older than STALE_AGE_MS (fallback)
|
|
177
227
|
* Called once at startup and periodically to clean up unresolved dispatches.
|
|
178
228
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
229
|
+
* When a dispatch is abandoned, auto-reverts scope status if the forward edge
|
|
230
|
+
* has autoRevert=true AND the scope is still at the dispatch target. This allows
|
|
231
|
+
* safe recovery for edges like backlog→implementing where the session crashed
|
|
232
|
+
* before doing meaningful work. Edges without autoRevert leave the scope in place
|
|
233
|
+
* for manual recovery from the dashboard. */
|
|
183
234
|
export function resolveStaleDispatches(db, io, scopeService, engine) {
|
|
184
235
|
const cutoff = new Date(Date.now() - STALE_AGE_MS).toISOString();
|
|
185
236
|
// Single query on events only — split by cache status
|
|
@@ -215,6 +266,8 @@ export function resolveStaleDispatches(db, io, scopeService, engine) {
|
|
|
215
266
|
}
|
|
216
267
|
if (isStale) {
|
|
217
268
|
resolveDispatchEvent(db, io, row.id, 'abandoned');
|
|
269
|
+
// Try auto-revert; if successful, clear the abandoned state
|
|
270
|
+
tryAutoRevertAndClear(db, io, scopeService, engine, row.id);
|
|
218
271
|
resolved++;
|
|
219
272
|
}
|
|
220
273
|
}
|
|
@@ -230,6 +283,7 @@ export function resolveStaleDispatches(db, io, scopeService, engine) {
|
|
|
230
283
|
batchData = JSON.parse(batchRow.data);
|
|
231
284
|
}
|
|
232
285
|
catch {
|
|
286
|
+
log.warn('Skipping unparseable batch dispatch event data', { eventId: batchRow.id });
|
|
233
287
|
continue;
|
|
234
288
|
}
|
|
235
289
|
const scopeIds = batchData.scope_ids;
|
|
@@ -299,6 +353,9 @@ export function getAbandonedScopeIds(db, scopeService, engine, activeScopeIds) {
|
|
|
299
353
|
const resolved = data.resolved;
|
|
300
354
|
const fromStatus = transition?.from ?? null;
|
|
301
355
|
const abandonedAt = resolved?.at ?? row.timestamp;
|
|
356
|
+
// Defense-in-depth: skip scopes already at their pre-dispatch status (already reverted)
|
|
357
|
+
if (fromStatus && scope.status === fromStatus)
|
|
358
|
+
continue;
|
|
302
359
|
result.push({ scope_id: row.scope_id, from_status: fromStatus, abandoned_at: abandonedAt });
|
|
303
360
|
}
|
|
304
361
|
return result;
|