orbital-command 0.1.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/LICENSE +21 -0
- package/README.md +396 -0
- package/bin/orbital.js +362 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
- package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
- package/dist/assets/charts-D__PA1zp.js +72 -0
- package/dist/assets/index-D1G6i0nS.css +1 -0
- package/dist/assets/index-DpItvKpf.js +419 -0
- package/dist/assets/ui-BvF022GT.js +53 -0
- package/dist/assets/vendor-Dzv9lrRc.js +59 -0
- package/dist/index.html +19 -0
- package/dist/scanner-sweep.png +0 -0
- package/dist/server/server/adapters/index.js +34 -0
- package/dist/server/server/adapters/iterm2-adapter.js +29 -0
- package/dist/server/server/adapters/subprocess-adapter.js +21 -0
- package/dist/server/server/adapters/terminal-adapter.js +1 -0
- package/dist/server/server/config.js +156 -0
- package/dist/server/server/database.js +90 -0
- package/dist/server/server/index.js +372 -0
- package/dist/server/server/init.js +811 -0
- package/dist/server/server/parsers/event-parser.js +64 -0
- package/dist/server/server/parsers/scope-parser.js +188 -0
- package/dist/server/server/routes/config-routes.js +163 -0
- package/dist/server/server/routes/data-routes.js +461 -0
- package/dist/server/server/routes/dispatch-routes.js +215 -0
- package/dist/server/server/routes/git-routes.js +92 -0
- package/dist/server/server/routes/scope-routes.js +215 -0
- package/dist/server/server/routes/sprint-routes.js +116 -0
- package/dist/server/server/routes/version-routes.js +130 -0
- package/dist/server/server/routes/workflow-routes.js +185 -0
- package/dist/server/server/schema.js +90 -0
- package/dist/server/server/services/batch-orchestrator.js +253 -0
- package/dist/server/server/services/claude-session-service.js +352 -0
- package/dist/server/server/services/config-service.js +132 -0
- package/dist/server/server/services/deploy-service.js +51 -0
- package/dist/server/server/services/event-service.js +63 -0
- package/dist/server/server/services/gate-service.js +83 -0
- package/dist/server/server/services/git-service.js +309 -0
- package/dist/server/server/services/github-service.js +145 -0
- package/dist/server/server/services/readiness-service.js +184 -0
- package/dist/server/server/services/scope-cache.js +72 -0
- package/dist/server/server/services/scope-service.js +424 -0
- package/dist/server/server/services/sprint-orchestrator.js +312 -0
- package/dist/server/server/services/sprint-service.js +293 -0
- package/dist/server/server/services/workflow-service.js +397 -0
- package/dist/server/server/utils/cc-hooks-parser.js +49 -0
- package/dist/server/server/utils/dispatch-utils.js +305 -0
- package/dist/server/server/utils/logger.js +86 -0
- package/dist/server/server/utils/terminal-launcher.js +388 -0
- package/dist/server/server/utils/worktree-manager.js +98 -0
- package/dist/server/server/watchers/event-watcher.js +81 -0
- package/dist/server/server/watchers/scope-watcher.js +33 -0
- package/dist/server/shared/api-types.js +5 -0
- package/dist/server/shared/default-workflow.json +616 -0
- package/dist/server/shared/workflow-config.js +44 -0
- package/dist/server/shared/workflow-engine.js +353 -0
- package/index.html +15 -0
- package/package.json +110 -0
- package/postcss.config.js +6 -0
- package/schemas/orbital.config.schema.json +83 -0
- package/scripts/postinstall.js +24 -0
- package/scripts/start.sh +20 -0
- package/server/adapters/index.ts +41 -0
- package/server/adapters/iterm2-adapter.ts +37 -0
- package/server/adapters/subprocess-adapter.ts +25 -0
- package/server/adapters/terminal-adapter.ts +24 -0
- package/server/config.ts +234 -0
- package/server/database.ts +107 -0
- package/server/index.ts +452 -0
- package/server/init.ts +891 -0
- package/server/parsers/event-parser.ts +74 -0
- package/server/parsers/scope-parser.ts +240 -0
- package/server/routes/config-routes.ts +182 -0
- package/server/routes/data-routes.ts +548 -0
- package/server/routes/dispatch-routes.ts +275 -0
- package/server/routes/git-routes.ts +112 -0
- package/server/routes/scope-routes.ts +262 -0
- package/server/routes/sprint-routes.ts +142 -0
- package/server/routes/version-routes.ts +156 -0
- package/server/routes/workflow-routes.ts +198 -0
- package/server/schema.ts +90 -0
- package/server/services/batch-orchestrator.ts +286 -0
- package/server/services/claude-session-service.ts +441 -0
- package/server/services/config-service.ts +151 -0
- package/server/services/deploy-service.ts +98 -0
- package/server/services/event-service.ts +98 -0
- package/server/services/gate-service.ts +126 -0
- package/server/services/git-service.ts +391 -0
- package/server/services/github-service.ts +183 -0
- package/server/services/readiness-service.ts +250 -0
- package/server/services/scope-cache.ts +81 -0
- package/server/services/scope-service.ts +476 -0
- package/server/services/sprint-orchestrator.ts +361 -0
- package/server/services/sprint-service.ts +415 -0
- package/server/services/workflow-service.ts +461 -0
- package/server/utils/cc-hooks-parser.ts +70 -0
- package/server/utils/dispatch-utils.ts +395 -0
- package/server/utils/logger.ts +109 -0
- package/server/utils/terminal-launcher.ts +462 -0
- package/server/utils/worktree-manager.ts +104 -0
- package/server/watchers/event-watcher.ts +100 -0
- package/server/watchers/scope-watcher.ts +38 -0
- package/shared/api-types.ts +20 -0
- package/shared/default-workflow.json +616 -0
- package/shared/workflow-config.ts +170 -0
- package/shared/workflow-engine.ts +427 -0
- package/src/App.tsx +33 -0
- package/src/components/AgentBadge.tsx +40 -0
- package/src/components/BatchPreflightModal.tsx +115 -0
- package/src/components/CardDisplayToggle.tsx +74 -0
- package/src/components/ColumnHeaderActions.tsx +55 -0
- package/src/components/ColumnMenu.tsx +99 -0
- package/src/components/DeployHistory.tsx +141 -0
- package/src/components/DispatchModal.tsx +164 -0
- package/src/components/DispatchPopover.tsx +139 -0
- package/src/components/DragOverlay.tsx +25 -0
- package/src/components/DriftSidebar.tsx +140 -0
- package/src/components/EnvironmentStrip.tsx +88 -0
- package/src/components/ErrorBoundary.tsx +62 -0
- package/src/components/FilterChip.tsx +105 -0
- package/src/components/GateIndicator.tsx +33 -0
- package/src/components/IdeaDetailModal.tsx +190 -0
- package/src/components/IdeaFormDialog.tsx +113 -0
- package/src/components/KanbanColumn.tsx +201 -0
- package/src/components/MarkdownRenderer.tsx +114 -0
- package/src/components/NeonGrid.tsx +128 -0
- package/src/components/PromotionQueue.tsx +89 -0
- package/src/components/ScopeCard.tsx +234 -0
- package/src/components/ScopeDetailModal.tsx +255 -0
- package/src/components/ScopeFilterBar.tsx +152 -0
- package/src/components/SearchInput.tsx +102 -0
- package/src/components/SessionPanel.tsx +335 -0
- package/src/components/SprintContainer.tsx +303 -0
- package/src/components/SprintDependencyDialog.tsx +78 -0
- package/src/components/SprintPreflightModal.tsx +138 -0
- package/src/components/StatusBar.tsx +168 -0
- package/src/components/SwimCell.tsx +67 -0
- package/src/components/SwimLaneRow.tsx +94 -0
- package/src/components/SwimlaneBoardView.tsx +108 -0
- package/src/components/VersionBadge.tsx +139 -0
- package/src/components/ViewModeSelector.tsx +114 -0
- package/src/components/config/AgentChip.tsx +53 -0
- package/src/components/config/AgentCreateDialog.tsx +321 -0
- package/src/components/config/AgentEditor.tsx +175 -0
- package/src/components/config/DirectoryTree.tsx +582 -0
- package/src/components/config/FileEditor.tsx +550 -0
- package/src/components/config/HookChip.tsx +50 -0
- package/src/components/config/StageCard.tsx +198 -0
- package/src/components/config/TransitionZone.tsx +173 -0
- package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
- package/src/components/config/WorkflowPipeline.tsx +161 -0
- package/src/components/source-control/BranchList.tsx +93 -0
- package/src/components/source-control/BranchPanel.tsx +105 -0
- package/src/components/source-control/CommitLog.tsx +100 -0
- package/src/components/source-control/CommitRow.tsx +47 -0
- package/src/components/source-control/GitHubPanel.tsx +110 -0
- package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
- package/src/components/source-control/GitOverviewBar.tsx +101 -0
- package/src/components/source-control/PullRequestList.tsx +69 -0
- package/src/components/source-control/WorktreeList.tsx +80 -0
- package/src/components/ui/badge.tsx +41 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/dialog.tsx +94 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/toggle-switch.tsx +35 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/components/workflow/AddEdgeDialog.tsx +217 -0
- package/src/components/workflow/AddListDialog.tsx +201 -0
- package/src/components/workflow/ChecklistEditor.tsx +239 -0
- package/src/components/workflow/CommandPrefixManager.tsx +118 -0
- package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
- package/src/components/workflow/DirectionSelector.tsx +133 -0
- package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
- package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
- package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
- package/src/components/workflow/EditToolbar.tsx +138 -0
- package/src/components/workflow/HookDetailPanel.tsx +250 -0
- package/src/components/workflow/HookExecutionLog.tsx +24 -0
- package/src/components/workflow/HookSourceModal.tsx +129 -0
- package/src/components/workflow/HooksDashboard.tsx +363 -0
- package/src/components/workflow/ListPropertyEditor.tsx +251 -0
- package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
- package/src/components/workflow/MovementRulesPanel.tsx +188 -0
- package/src/components/workflow/NodeDetailPanel.tsx +245 -0
- package/src/components/workflow/PresetSelector.tsx +414 -0
- package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
- package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
- package/src/components/workflow/WorkflowNode.tsx +147 -0
- package/src/components/workflow/graphLayout.ts +186 -0
- package/src/components/workflow/mergeHooks.ts +85 -0
- package/src/components/workflow/useEditHistory.ts +88 -0
- package/src/components/workflow/useWorkflowEditor.ts +262 -0
- package/src/components/workflow/validateConfig.ts +70 -0
- package/src/hooks/useActiveDispatches.ts +198 -0
- package/src/hooks/useBoardSettings.ts +170 -0
- package/src/hooks/useCardDisplay.ts +57 -0
- package/src/hooks/useCcHooks.ts +24 -0
- package/src/hooks/useConfigTree.ts +51 -0
- package/src/hooks/useEnforcementRules.ts +46 -0
- package/src/hooks/useEvents.ts +59 -0
- package/src/hooks/useFileEditor.ts +165 -0
- package/src/hooks/useGates.ts +57 -0
- package/src/hooks/useIdeaActions.ts +53 -0
- package/src/hooks/useKanbanDnd.ts +410 -0
- package/src/hooks/useOrbitalConfig.ts +54 -0
- package/src/hooks/usePipeline.ts +47 -0
- package/src/hooks/usePipelineData.ts +338 -0
- package/src/hooks/useReconnect.ts +25 -0
- package/src/hooks/useScopeFilters.ts +125 -0
- package/src/hooks/useScopeSessions.ts +44 -0
- package/src/hooks/useScopes.ts +67 -0
- package/src/hooks/useSearch.ts +67 -0
- package/src/hooks/useSettings.tsx +187 -0
- package/src/hooks/useSocket.ts +25 -0
- package/src/hooks/useSourceControl.ts +105 -0
- package/src/hooks/useSprintPreflight.ts +55 -0
- package/src/hooks/useSprints.ts +154 -0
- package/src/hooks/useStatusBarHighlight.ts +18 -0
- package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
- package/src/hooks/useTheme.ts +9 -0
- package/src/hooks/useTransitionReadiness.ts +53 -0
- package/src/hooks/useVersion.ts +155 -0
- package/src/hooks/useViolations.ts +65 -0
- package/src/hooks/useWorkflow.tsx +125 -0
- package/src/hooks/useZoomModifier.ts +19 -0
- package/src/index.css +797 -0
- package/src/layouts/DashboardLayout.tsx +113 -0
- package/src/lib/collisionDetection.ts +20 -0
- package/src/lib/scope-fields.ts +61 -0
- package/src/lib/swimlane.ts +146 -0
- package/src/lib/utils.ts +15 -0
- package/src/main.tsx +19 -0
- package/src/socket.ts +11 -0
- package/src/types/index.ts +497 -0
- package/src/views/AgentFeed.tsx +339 -0
- package/src/views/DeployPipeline.tsx +59 -0
- package/src/views/EnforcementView.tsx +378 -0
- package/src/views/PrimitivesConfig.tsx +500 -0
- package/src/views/QualityGates.tsx +1012 -0
- package/src/views/ScopeBoard.tsx +454 -0
- package/src/views/SessionTimeline.tsx +516 -0
- package/src/views/Settings.tsx +183 -0
- package/src/views/SourceControl.tsx +95 -0
- package/src/views/WorkflowVisualizer.tsx +382 -0
- package/tailwind.config.js +161 -0
- package/templates/agents/AUTO-INVOKE.md +180 -0
- package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
- package/templates/agents/QUICK-REFERENCE.md +122 -0
- package/templates/agents/README.md +188 -0
- package/templates/agents/SKILL-TRIGGERS.md +100 -0
- package/templates/agents/blue-team/frontend-designer.md +424 -0
- package/templates/agents/green-team/architect.md +526 -0
- package/templates/agents/green-team/rules-enforcer.md +131 -0
- package/templates/agents/red-team/attacker-learned.md +24 -0
- package/templates/agents/red-team/attacker.md +486 -0
- package/templates/agents/red-team/chaos.md +548 -0
- package/templates/agents/reference/component-registry.md +82 -0
- package/templates/agents/workflows/full-mode.md +218 -0
- package/templates/agents/workflows/quick-mode.md +118 -0
- package/templates/agents/workflows/security-mode.md +283 -0
- package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
- package/templates/config/agent-triggers.json +92 -0
- package/templates/hooks/agent-team-gate.sh +31 -0
- package/templates/hooks/agent-trigger.sh +97 -0
- package/templates/hooks/block-push.sh +66 -0
- package/templates/hooks/block-workarounds.sh +61 -0
- package/templates/hooks/blocker-check.sh +28 -0
- package/templates/hooks/completion-checklist.sh +28 -0
- package/templates/hooks/decision-capture.sh +15 -0
- package/templates/hooks/dependency-check.sh +27 -0
- package/templates/hooks/end-session.sh +31 -0
- package/templates/hooks/exploration-logger.sh +37 -0
- package/templates/hooks/files-changed-summary.sh +37 -0
- package/templates/hooks/get-session-id.sh +49 -0
- package/templates/hooks/git-commit-guard.sh +34 -0
- package/templates/hooks/init-session.sh +93 -0
- package/templates/hooks/orbital-emit.sh +79 -0
- package/templates/hooks/orbital-report-deploy.sh +78 -0
- package/templates/hooks/orbital-report-gates.sh +40 -0
- package/templates/hooks/orbital-report-violation.sh +36 -0
- package/templates/hooks/orbital-scope-update.sh +53 -0
- package/templates/hooks/phase-verify-reminder.sh +26 -0
- package/templates/hooks/review-gate-check.sh +82 -0
- package/templates/hooks/scope-commit-logger.sh +37 -0
- package/templates/hooks/scope-create-cleanup.sh +36 -0
- package/templates/hooks/scope-create-gate.sh +80 -0
- package/templates/hooks/scope-create-tracker.sh +17 -0
- package/templates/hooks/scope-file-sync.sh +53 -0
- package/templates/hooks/scope-gate.sh +35 -0
- package/templates/hooks/scope-helpers.sh +188 -0
- package/templates/hooks/scope-lifecycle-gate.sh +139 -0
- package/templates/hooks/scope-prepare.sh +244 -0
- package/templates/hooks/scope-transition.sh +172 -0
- package/templates/hooks/session-enforcer.sh +143 -0
- package/templates/hooks/time-tracker.sh +33 -0
- package/templates/lessons-learned.md +15 -0
- package/templates/orbital.config.json +35 -0
- package/templates/presets/development.json +42 -0
- package/templates/presets/gitflow.json +712 -0
- package/templates/presets/minimal.json +23 -0
- package/templates/quick/rules.md +218 -0
- package/templates/scopes/_template.md +255 -0
- package/templates/settings-hooks.json +98 -0
- package/templates/skills/git-commit/SKILL.md +85 -0
- package/templates/skills/git-dev/SKILL.md +99 -0
- package/templates/skills/git-hotfix/SKILL.md +223 -0
- package/templates/skills/git-main/SKILL.md +84 -0
- package/templates/skills/git-production/SKILL.md +165 -0
- package/templates/skills/git-staging/SKILL.md +112 -0
- package/templates/skills/scope-create/SKILL.md +81 -0
- package/templates/skills/scope-fix-review/SKILL.md +168 -0
- package/templates/skills/scope-implement/SKILL.md +110 -0
- package/templates/skills/scope-post-review/SKILL.md +144 -0
- package/templates/skills/scope-pre-review/SKILL.md +211 -0
- package/templates/skills/scope-verify/SKILL.md +201 -0
- package/templates/skills/session-init/SKILL.md +62 -0
- package/templates/skills/session-resume/SKILL.md +201 -0
- package/templates/skills/test-checks/SKILL.md +171 -0
- package/templates/skills/test-code-review/SKILL.md +252 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseCcHooks } from '../utils/cc-hooks-parser.js';
|
|
5
|
+
export function createWorkflowRoutes({ workflowService, projectRoot }) {
|
|
6
|
+
const router = Router();
|
|
7
|
+
// GET /workflow — returns active config
|
|
8
|
+
router.get('/workflow', (_req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
res.json({ success: true, data: workflowService.getActive() });
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
// PUT /workflow — validate and update active config
|
|
17
|
+
router.put('/workflow', (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const config = req.body;
|
|
20
|
+
const result = workflowService.updateActive(config);
|
|
21
|
+
if (!result.valid) {
|
|
22
|
+
res.status(400).json({ success: false, error: 'Validation failed', data: result });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
res.json({ success: true, data: result });
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// GET /workflow/presets — list all presets
|
|
32
|
+
router.get('/workflow/presets', (_req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
res.json({ success: true, data: workflowService.listPresets() });
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// POST /workflow/presets — save current config as named preset
|
|
41
|
+
router.post('/workflow/presets', (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const { name } = req.body;
|
|
44
|
+
if (!name) {
|
|
45
|
+
res.status(400).json({ success: false, error: 'name is required' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
workflowService.savePreset(name);
|
|
49
|
+
res.json({ success: true });
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const msg = errMsg(err);
|
|
53
|
+
const status = msg.includes('Cannot overwrite') || msg.includes('must be') ? 400 : 500;
|
|
54
|
+
res.status(status).json({ success: false, error: msg });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// GET /workflow/presets/:name — get specific preset
|
|
58
|
+
router.get('/workflow/presets/:name', (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const config = workflowService.getPreset(req.params.name);
|
|
61
|
+
res.json({ success: true, data: config });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = errMsg(err);
|
|
65
|
+
const status = msg.includes('not found') ? 404 : 500;
|
|
66
|
+
res.status(status).json({ success: false, error: msg });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// DELETE /workflow/presets/:name — delete preset
|
|
70
|
+
router.delete('/workflow/presets/:name', (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
workflowService.deletePreset(req.params.name);
|
|
73
|
+
res.json({ success: true });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const msg = errMsg(err);
|
|
77
|
+
const status = msg.includes('Cannot delete') ? 400 : msg.includes('not found') ? 404 : 500;
|
|
78
|
+
res.status(status).json({ success: false, error: msg });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// GET /workflow/hooks — returns all hooks with edge mapping
|
|
82
|
+
router.get('/workflow/hooks', (_req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const engine = workflowService.getEngine();
|
|
85
|
+
const hooks = engine.getAllHooks();
|
|
86
|
+
const edgeHookMap = {};
|
|
87
|
+
for (const edge of engine.getAllEdges()) {
|
|
88
|
+
if (edge.hooks && edge.hooks.length > 0) {
|
|
89
|
+
edgeHookMap[`${edge.from}:${edge.to}`] = edge.hooks;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
res.json({ success: true, data: { hooks, edgeHookMap } });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// POST /workflow/preview — dry-run migration preview
|
|
99
|
+
router.post('/workflow/preview', (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const config = req.body;
|
|
102
|
+
const plan = workflowService.previewMigration(config);
|
|
103
|
+
res.json({ success: true, data: plan });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// GET /workflow/hooks/:id/source — read hook source file
|
|
110
|
+
router.get('/workflow/hooks/:id/source', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const hookId = req.params.id;
|
|
113
|
+
const engine = workflowService.getEngine();
|
|
114
|
+
const hook = engine.getAllHooks().find((h) => h.id === hookId);
|
|
115
|
+
if (!hook) {
|
|
116
|
+
res.status(404).json({ success: false, error: `Hook '${hookId}' not found` });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (hook.target.includes('..')) {
|
|
120
|
+
res.status(400).json({ success: false, error: 'Invalid hook target path' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const filePath = path.resolve(projectRoot, hook.target);
|
|
124
|
+
const content = await readFile(filePath, 'utf-8');
|
|
125
|
+
const lineCount = content.split('\n').length;
|
|
126
|
+
res.json({ success: true, data: { hookId, filePath: hook.target, content, lineCount } });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const msg = errMsg(err);
|
|
130
|
+
const status = msg.includes('ENOENT') ? 404 : 500;
|
|
131
|
+
res.status(status).json({ success: false, error: msg });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// GET /workflow/claude-hooks — returns all Claude Code hooks from settings.local.json
|
|
135
|
+
router.get('/workflow/claude-hooks', (_req, res) => {
|
|
136
|
+
try {
|
|
137
|
+
const settingsPath = path.resolve(projectRoot, '.claude/settings.local.json');
|
|
138
|
+
const data = parseCcHooks(settingsPath);
|
|
139
|
+
res.json({ success: true, data });
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// GET /workflow/hooks/source — read any hook source file by path
|
|
146
|
+
router.get('/workflow/hooks/source', async (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const hookPath = req.query.path;
|
|
149
|
+
if (!hookPath) {
|
|
150
|
+
res.status(400).json({ success: false, error: 'path query parameter is required' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (hookPath.includes('..')) {
|
|
154
|
+
res.status(400).json({ success: false, error: 'Invalid path: directory traversal not allowed' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const filePath = path.resolve(projectRoot, hookPath);
|
|
158
|
+
const content = await readFile(filePath, 'utf-8');
|
|
159
|
+
const lineCount = content.split('\n').length;
|
|
160
|
+
res.json({ success: true, data: { filePath: hookPath, content, lineCount } });
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const msg = errMsg(err);
|
|
164
|
+
const status = msg.includes('ENOENT') ? 404 : 500;
|
|
165
|
+
res.status(status).json({ success: false, error: msg });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// POST /workflow/apply — apply new config with orphan mappings
|
|
169
|
+
router.post('/workflow/apply', (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const { config, orphanMappings } = req.body;
|
|
172
|
+
const plan = workflowService.applyMigration(config, orphanMappings ?? {});
|
|
173
|
+
res.json({ success: true, data: plan });
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
const msg = errMsg(err);
|
|
177
|
+
const status = msg.includes('Missing orphan') || msg.includes('Validation failed') || msg.includes('not a valid') ? 400 : 500;
|
|
178
|
+
res.status(status).json({ success: false, error: msg });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return router;
|
|
182
|
+
}
|
|
183
|
+
function errMsg(err) {
|
|
184
|
+
return err instanceof Error ? err.message : String(err);
|
|
185
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const SCHEMA_DDL = `
|
|
2
|
+
-- Events from hooks/watchers
|
|
3
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
type TEXT NOT NULL,
|
|
6
|
+
scope_id INTEGER,
|
|
7
|
+
session_id TEXT,
|
|
8
|
+
agent TEXT,
|
|
9
|
+
data TEXT DEFAULT '{}',
|
|
10
|
+
timestamp TEXT NOT NULL,
|
|
11
|
+
processed INTEGER DEFAULT 0
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
-- Quality gate results per scope
|
|
15
|
+
CREATE TABLE IF NOT EXISTS quality_gates (
|
|
16
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
+
scope_id INTEGER,
|
|
18
|
+
gate_name TEXT NOT NULL,
|
|
19
|
+
status TEXT NOT NULL,
|
|
20
|
+
details TEXT,
|
|
21
|
+
duration_ms INTEGER,
|
|
22
|
+
run_at TEXT NOT NULL,
|
|
23
|
+
commit_sha TEXT
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
-- Deployment tracking
|
|
27
|
+
CREATE TABLE IF NOT EXISTS deployments (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
environment TEXT NOT NULL,
|
|
30
|
+
status TEXT NOT NULL,
|
|
31
|
+
commit_sha TEXT,
|
|
32
|
+
branch TEXT,
|
|
33
|
+
pr_number INTEGER,
|
|
34
|
+
health_check_url TEXT,
|
|
35
|
+
started_at TEXT,
|
|
36
|
+
completed_at TEXT,
|
|
37
|
+
details TEXT DEFAULT '{}'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- Session history
|
|
41
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
scope_id INTEGER,
|
|
44
|
+
claude_session_id TEXT,
|
|
45
|
+
action TEXT,
|
|
46
|
+
started_at TEXT,
|
|
47
|
+
ended_at TEXT,
|
|
48
|
+
handoff_file TEXT,
|
|
49
|
+
summary TEXT,
|
|
50
|
+
discoveries TEXT DEFAULT '[]',
|
|
51
|
+
next_steps TEXT DEFAULT '[]',
|
|
52
|
+
progress_pct INTEGER
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
-- Sprint containers for batching scope dispatch
|
|
56
|
+
CREATE TABLE IF NOT EXISTS sprints (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
name TEXT NOT NULL,
|
|
59
|
+
status TEXT NOT NULL DEFAULT 'assembling',
|
|
60
|
+
concurrency_cap INTEGER NOT NULL DEFAULT 5,
|
|
61
|
+
created_at TEXT NOT NULL,
|
|
62
|
+
updated_at TEXT NOT NULL,
|
|
63
|
+
dispatched_at TEXT,
|
|
64
|
+
completed_at TEXT,
|
|
65
|
+
dispatch_meta TEXT DEFAULT '{}'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS sprint_scopes (
|
|
69
|
+
sprint_id INTEGER NOT NULL,
|
|
70
|
+
scope_id INTEGER NOT NULL,
|
|
71
|
+
layer INTEGER,
|
|
72
|
+
dispatch_status TEXT NOT NULL DEFAULT 'pending',
|
|
73
|
+
dispatched_at TEXT,
|
|
74
|
+
completed_at TEXT,
|
|
75
|
+
error TEXT,
|
|
76
|
+
PRIMARY KEY (sprint_id, scope_id),
|
|
77
|
+
FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE CASCADE
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- Indexes for common queries
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_events_scope_id ON events(scope_id);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_gates_scope_id ON quality_gates(scope_id);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_gates_run_at ON quality_gates(run_at);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_deployments_env ON deployments(environment);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope_id);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_sprints_status ON sprints(status);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_sprint_scopes_sprint ON sprint_scopes(sprint_id);
|
|
90
|
+
`;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, snapshotSessionPids, discoverNewSession, isSessionPidAlive } from '../utils/terminal-launcher.js';
|
|
2
|
+
import { linkPidToDispatch, resolveDispatchEvent } from '../utils/dispatch-utils.js';
|
|
3
|
+
import { getConfig } from '../config.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
const log = createLogger('batch');
|
|
6
|
+
const VALID_MERGE_MODES = ['push', 'pr'];
|
|
7
|
+
// ─── Orchestrator ───────────────────────────────────────────
|
|
8
|
+
export class BatchOrchestrator {
|
|
9
|
+
db;
|
|
10
|
+
io;
|
|
11
|
+
sprintService;
|
|
12
|
+
scopeService;
|
|
13
|
+
engine;
|
|
14
|
+
constructor(db, io, sprintService, scopeService, engine) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.io = io;
|
|
17
|
+
this.sprintService = sprintService;
|
|
18
|
+
this.scopeService = scopeService;
|
|
19
|
+
this.engine = engine;
|
|
20
|
+
}
|
|
21
|
+
/** Dispatch a batch — validates constraints and routes to column-specific handler */
|
|
22
|
+
async dispatch(batchId, mergeMode) {
|
|
23
|
+
const batch = this.sprintService.getById(batchId);
|
|
24
|
+
if (!batch)
|
|
25
|
+
return { ok: false, error: 'Batch not found' };
|
|
26
|
+
if (batch.group_type !== 'batch')
|
|
27
|
+
return { ok: false, error: 'Not a batch group' };
|
|
28
|
+
if (batch.status !== 'assembling')
|
|
29
|
+
return { ok: false, error: `Batch status is '${batch.status}', expected 'assembling'` };
|
|
30
|
+
if (batch.scope_ids.length === 0)
|
|
31
|
+
return { ok: false, error: 'Batch has no scopes' };
|
|
32
|
+
// W-4: One active batch per column
|
|
33
|
+
const existingActive = this.sprintService.findActiveBatchForColumn(batch.target_column);
|
|
34
|
+
if (existingActive && existingActive.id !== batchId) {
|
|
35
|
+
return { ok: false, error: `Column '${batch.target_column}' already has an active batch (ID: ${existingActive.id})` };
|
|
36
|
+
}
|
|
37
|
+
const command = this.engine.getBatchCommand(batch.target_column);
|
|
38
|
+
if (!command)
|
|
39
|
+
return { ok: false, error: `No dispatch command for column '${batch.target_column}'` };
|
|
40
|
+
// Mark batch as dispatched
|
|
41
|
+
this.sprintService.updateStatus(batchId, 'dispatched');
|
|
42
|
+
log.info('Batch dispatched', { id: batchId, target_column: batch.target_column, scope_ids: batch.scope_ids });
|
|
43
|
+
// Build scope IDs env var prefix (W-1: prepend to command, not process.env)
|
|
44
|
+
const scopeIdsStr = batch.scope_ids.join(',');
|
|
45
|
+
const mergeModeStr = VALID_MERGE_MODES.includes(mergeMode ?? '') ? mergeMode : 'push';
|
|
46
|
+
// Record DISPATCH event
|
|
47
|
+
const eventId = crypto.randomUUID();
|
|
48
|
+
const eventData = {
|
|
49
|
+
command,
|
|
50
|
+
batch_id: batchId,
|
|
51
|
+
scope_ids: batch.scope_ids,
|
|
52
|
+
target_column: batch.target_column,
|
|
53
|
+
batch: true,
|
|
54
|
+
resolved: null,
|
|
55
|
+
};
|
|
56
|
+
this.db.prepare(`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
57
|
+
VALUES (?, 'DISPATCH', NULL, NULL, 'batch-orchestrator', ?, ?)`).run(eventId, JSON.stringify(eventData), new Date().toISOString());
|
|
58
|
+
this.io.emit('event:new', {
|
|
59
|
+
id: eventId, type: 'DISPATCH', scope_id: null,
|
|
60
|
+
session_id: null, agent: 'batch-orchestrator',
|
|
61
|
+
data: eventData, timestamp: new Date().toISOString(),
|
|
62
|
+
});
|
|
63
|
+
// Launch single CLI session with BATCH_SCOPE_IDS prepended to command
|
|
64
|
+
const escaped = escapeForAnsiC(command);
|
|
65
|
+
const fullCmd = `cd '${getConfig().projectRoot}' && BATCH_SCOPE_IDS='${scopeIdsStr}' MERGE_MODE='${mergeModeStr}' claude --dangerously-skip-permissions $'${escaped}'`;
|
|
66
|
+
const beforePids = snapshotSessionPids(getConfig().projectRoot);
|
|
67
|
+
try {
|
|
68
|
+
await launchInCategorizedTerminal(command, fullCmd);
|
|
69
|
+
// Store dispatch result timestamp
|
|
70
|
+
this.sprintService.updateDispatchResult(batchId, {
|
|
71
|
+
dispatched_at: new Date().toISOString(),
|
|
72
|
+
});
|
|
73
|
+
// Fire-and-forget: discover session PID and link to dispatch
|
|
74
|
+
discoverNewSession(getConfig().projectRoot, beforePids)
|
|
75
|
+
.then((session) => {
|
|
76
|
+
if (!session)
|
|
77
|
+
return;
|
|
78
|
+
linkPidToDispatch(this.db, eventId, session.pid);
|
|
79
|
+
// Store PID on the batch for two-phase completion
|
|
80
|
+
const currentResult = this.sprintService.getById(batchId)?.dispatch_result ?? {};
|
|
81
|
+
this.sprintService.updateDispatchResult(batchId, {
|
|
82
|
+
...currentResult,
|
|
83
|
+
dispatched_at: currentResult.dispatched_at ?? new Date().toISOString(),
|
|
84
|
+
});
|
|
85
|
+
// Store PID in event data for later liveness checking
|
|
86
|
+
const row = this.db.prepare('SELECT data FROM events WHERE id = ?').get(eventId);
|
|
87
|
+
if (row) {
|
|
88
|
+
const data = JSON.parse(row.data);
|
|
89
|
+
data.pid = session.pid;
|
|
90
|
+
this.db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.catch(err => log.error('PID discovery failed', { error: err.message }));
|
|
94
|
+
return { ok: true };
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
this.sprintService.updateStatus(batchId, 'failed');
|
|
98
|
+
resolveDispatchEvent(this.db, this.io, eventId, 'failed', String(err));
|
|
99
|
+
return { ok: false, error: `Failed to launch terminal: ${err}` };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Called when a scope reaches a new status — check if it satisfies a batch,
|
|
103
|
+
* or remove the scope from the batch if its status diverged from the target. */
|
|
104
|
+
onScopeStatusChanged(scopeId, newStatus) {
|
|
105
|
+
// Find any active batch containing this scope
|
|
106
|
+
const match = this.sprintService.findActiveSprintForScope(scopeId);
|
|
107
|
+
if (!match)
|
|
108
|
+
return;
|
|
109
|
+
const batch = this.sprintService.getById(match.sprint_id);
|
|
110
|
+
if (!batch || batch.group_type !== 'batch')
|
|
111
|
+
return;
|
|
112
|
+
const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
|
|
113
|
+
if (newStatus === expectedStatus || this.engine.isTerminalStatus(newStatus)) {
|
|
114
|
+
this.sprintService.updateScopeStatus(batch.id, scopeId, 'completed');
|
|
115
|
+
// Check if all scopes have transitioned
|
|
116
|
+
if (batch.status === 'dispatched') {
|
|
117
|
+
this.sprintService.updateStatus(batch.id, 'in_progress');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (newStatus !== batch.target_column) {
|
|
121
|
+
// Scope diverged from batch target — remove it from the batch
|
|
122
|
+
this.sprintService.forceRemoveScope(batch.id, scopeId);
|
|
123
|
+
// If batch is now empty, mark it as failed
|
|
124
|
+
const remaining = this.sprintService.getSprintScopes(batch.id);
|
|
125
|
+
if (remaining.length === 0 && batch.status !== 'assembling') {
|
|
126
|
+
this.sprintService.updateStatus(batch.id, 'failed');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Called when a dispatched session PID dies — second phase of two-phase completion.
|
|
131
|
+
* Reverts un-transitioned scopes to their pre-dispatch status. */
|
|
132
|
+
onSessionPidDied(batchId) {
|
|
133
|
+
const batch = this.sprintService.getById(batchId);
|
|
134
|
+
if (!batch || batch.group_type !== 'batch')
|
|
135
|
+
return;
|
|
136
|
+
if (batch.status !== 'dispatched' && batch.status !== 'in_progress')
|
|
137
|
+
return;
|
|
138
|
+
const scopes = this.sprintService.getSprintScopes(batchId);
|
|
139
|
+
const allTransitioned = scopes.every((ss) => ss.dispatch_status === 'completed');
|
|
140
|
+
if (allTransitioned) {
|
|
141
|
+
this.sprintService.updateStatus(batchId, 'completed');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const pending = scopes.filter((ss) => ss.dispatch_status !== 'completed').map((ss) => ss.scope_id);
|
|
145
|
+
this.sprintService.updateStatus(batchId, 'failed');
|
|
146
|
+
// Mark un-transitioned scopes as failed and revert their status
|
|
147
|
+
for (const scopeId of pending) {
|
|
148
|
+
this.sprintService.updateScopeStatus(batchId, scopeId, 'failed', 'Session exited before scope transitioned');
|
|
149
|
+
// Revert scope to pre-dispatch status (the batch's source column)
|
|
150
|
+
this.scopeService.updateStatus(scopeId, batch.target_column, 'rollback');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve stale batches — catches batches stuck due to lost PIDs, Orbital downtime, or
|
|
156
|
+
* missing PID records. Unlike recoverActiveBatches (which focuses on PID polling),
|
|
157
|
+
* this also resolves batches where no PID was ever recorded.
|
|
158
|
+
*/
|
|
159
|
+
resolveStaleBatches() {
|
|
160
|
+
const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
161
|
+
const active = this.db.prepare(`SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`).all();
|
|
162
|
+
if (active.length > 0) {
|
|
163
|
+
log.debug('Checking stale batches', { activeCount: active.length });
|
|
164
|
+
}
|
|
165
|
+
let resolved = 0;
|
|
166
|
+
for (const { id } of active) {
|
|
167
|
+
const batch = this.sprintService.getById(id);
|
|
168
|
+
if (!batch)
|
|
169
|
+
continue;
|
|
170
|
+
const scopes = this.sprintService.getSprintScopes(id);
|
|
171
|
+
const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
|
|
172
|
+
// Phase 1: auto-complete scopes that reached or passed target status
|
|
173
|
+
for (const ss of scopes) {
|
|
174
|
+
if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
|
|
175
|
+
const scope = this.scopeService.getById(ss.scope_id);
|
|
176
|
+
if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
|
|
177
|
+
this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Phase 2: check PID liveness (check both unresolved and resolved events —
|
|
182
|
+
// SESSION_END may have resolved the dispatch event before we get here)
|
|
183
|
+
const dispatchEvent = this.db.prepare(`SELECT data FROM events
|
|
184
|
+
WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
|
|
185
|
+
ORDER BY timestamp DESC LIMIT 1`).get(id);
|
|
186
|
+
let pidDead = false;
|
|
187
|
+
if (dispatchEvent) {
|
|
188
|
+
const data = JSON.parse(dispatchEvent.data);
|
|
189
|
+
// If the dispatch event is already resolved, the session is definitely done
|
|
190
|
+
if (data.resolved != null) {
|
|
191
|
+
pidDead = true;
|
|
192
|
+
}
|
|
193
|
+
else if (typeof data.pid === 'number') {
|
|
194
|
+
pidDead = !isSessionPidAlive(data.pid);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// No PID recorded — check if batch is old enough to consider stale
|
|
198
|
+
const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
|
|
199
|
+
pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// No dispatch event at all — check age
|
|
204
|
+
const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
|
|
205
|
+
pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
|
|
206
|
+
}
|
|
207
|
+
if (pidDead) {
|
|
208
|
+
this.onSessionPidDied(id);
|
|
209
|
+
resolved++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return resolved;
|
|
213
|
+
}
|
|
214
|
+
/** Recover active batches after server restart (W-3) */
|
|
215
|
+
async recoverActiveBatches() {
|
|
216
|
+
const active = this.db.prepare(`SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`).all();
|
|
217
|
+
if (active.length > 0) {
|
|
218
|
+
log.debug('Recovering active batches', { count: active.length });
|
|
219
|
+
}
|
|
220
|
+
for (const { id } of active) {
|
|
221
|
+
const batch = this.sprintService.getById(id);
|
|
222
|
+
if (!batch)
|
|
223
|
+
continue;
|
|
224
|
+
const scopes = this.sprintService.getSprintScopes(id);
|
|
225
|
+
const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
|
|
226
|
+
// Check if scopes reached or passed target status while server was down
|
|
227
|
+
for (const ss of scopes) {
|
|
228
|
+
if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
|
|
229
|
+
const scope = this.scopeService.getById(ss.scope_id);
|
|
230
|
+
if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
|
|
231
|
+
this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Check if dispatch PID is still alive (include resolved events —
|
|
236
|
+
// SESSION_END may have resolved the dispatch before server restart)
|
|
237
|
+
const dispatchEvent = this.db.prepare(`SELECT data FROM events
|
|
238
|
+
WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
|
|
239
|
+
ORDER BY timestamp DESC LIMIT 1`).get(id);
|
|
240
|
+
if (dispatchEvent) {
|
|
241
|
+
const data = JSON.parse(dispatchEvent.data);
|
|
242
|
+
if (data.resolved != null) {
|
|
243
|
+
// Dispatch already resolved — session is done
|
|
244
|
+
this.onSessionPidDied(id);
|
|
245
|
+
}
|
|
246
|
+
else if (typeof data.pid === 'number' && !isSessionPidAlive(data.pid)) {
|
|
247
|
+
// PID is dead — trigger two-phase completion check
|
|
248
|
+
this.onSessionPidDied(id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|