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,361 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { Server } from 'socket.io';
|
|
3
|
+
import { SprintService } from './sprint-service.js';
|
|
4
|
+
import { ScopeService } from './scope-service.js';
|
|
5
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
6
|
+
import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
7
|
+
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
|
+
import { getConfig } from '../config.js';
|
|
9
|
+
import { createLogger } from '../utils/logger.js';
|
|
10
|
+
|
|
11
|
+
const log = createLogger('sprint');
|
|
12
|
+
const LAUNCH_STAGGER_MS = 2000;
|
|
13
|
+
|
|
14
|
+
function sleep(ms: number): Promise<void> {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Orchestrator ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export class SprintOrchestrator {
|
|
21
|
+
constructor(
|
|
22
|
+
private db: Database.Database,
|
|
23
|
+
private io: Server,
|
|
24
|
+
private sprintService: SprintService,
|
|
25
|
+
private scopeService: ScopeService,
|
|
26
|
+
private engine: WorkflowEngine,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
/** Build execution layers using Kahn's topological sort */
|
|
30
|
+
buildExecutionLayers(sprintScopeIds: number[]): { layers: number[][]; cycle: number[] } {
|
|
31
|
+
const sprintSet = new Set(sprintScopeIds);
|
|
32
|
+
|
|
33
|
+
// Load dependency info for each scope in the sprint
|
|
34
|
+
const scopeDeps = new Map<number, number[]>();
|
|
35
|
+
for (const id of sprintScopeIds) {
|
|
36
|
+
const scope = this.scopeService.getById(id);
|
|
37
|
+
if (!scope) continue;
|
|
38
|
+
// Only keep deps that are WITHIN the sprint
|
|
39
|
+
scopeDeps.set(id, scope.blocked_by.filter((d) => sprintSet.has(d)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Build in-degree map — in-degree = count of internal deps for each scope
|
|
43
|
+
const inDegree = new Map<number, number>();
|
|
44
|
+
for (const [id, deps] of scopeDeps) {
|
|
45
|
+
inDegree.set(id, deps.length);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const layers: number[][] = [];
|
|
49
|
+
const remaining = new Set(sprintScopeIds);
|
|
50
|
+
|
|
51
|
+
while (remaining.size > 0) {
|
|
52
|
+
// Find all nodes with in-degree 0
|
|
53
|
+
const layer: number[] = [];
|
|
54
|
+
for (const id of remaining) {
|
|
55
|
+
if ((inDegree.get(id) ?? 0) === 0) {
|
|
56
|
+
layer.push(id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (layer.length === 0) {
|
|
61
|
+
// Cycle detected — return remaining as cycle
|
|
62
|
+
return { layers, cycle: [...remaining] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Remove this layer and decrement dependents
|
|
66
|
+
for (const id of layer) {
|
|
67
|
+
remaining.delete(id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Decrement in-degree for scopes that depended on this layer's scopes
|
|
71
|
+
for (const id of remaining) {
|
|
72
|
+
const deps = scopeDeps.get(id) ?? [];
|
|
73
|
+
let newDeg = 0;
|
|
74
|
+
for (const dep of deps) {
|
|
75
|
+
if (remaining.has(dep)) newDeg++;
|
|
76
|
+
}
|
|
77
|
+
inDegree.set(id, newDeg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
layers.push(layer.sort((a, b) => a - b));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { layers, cycle: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Start sprint dispatch: build layers, persist, launch Layer 0 */
|
|
87
|
+
async startSprint(sprintId: number): Promise<{ ok: boolean; error?: string; layers?: number[][] }> {
|
|
88
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
89
|
+
if (!sprint) return { ok: false, error: 'Sprint not found' };
|
|
90
|
+
if (sprint.status !== 'assembling') return { ok: false, error: `Sprint status is '${sprint.status}', expected 'assembling'` };
|
|
91
|
+
if (sprint.scope_ids.length === 0) return { ok: false, error: 'Sprint has no scopes' };
|
|
92
|
+
|
|
93
|
+
// Build dependency graph
|
|
94
|
+
const { layers, cycle } = this.buildExecutionLayers(sprint.scope_ids);
|
|
95
|
+
if (cycle.length > 0) {
|
|
96
|
+
return { ok: false, error: `Dependency cycle detected among scopes: ${cycle.join(', ')}` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Persist layer assignments
|
|
100
|
+
this.sprintService.setLayers(sprintId, layers);
|
|
101
|
+
this.sprintService.updateStatus(sprintId, 'dispatched');
|
|
102
|
+
|
|
103
|
+
log.info('Sprint started', { id: sprintId, layers: layers.length, scopes: sprint.scope_ids.length });
|
|
104
|
+
|
|
105
|
+
// Dispatch Layer 0
|
|
106
|
+
await this.dispatchLayer(sprintId, layers[0], sprint.concurrency_cap);
|
|
107
|
+
|
|
108
|
+
return { ok: true, layers };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Called when a scope reaches 'dev' status — advance the sprint */
|
|
112
|
+
async onScopeReachedDev(scopeId: number): Promise<void> {
|
|
113
|
+
const match = this.sprintService.findActiveSprintForScope(scopeId);
|
|
114
|
+
if (!match) return;
|
|
115
|
+
log.debug('Scope reached dev', { scopeId, sprintId: match.sprint_id });
|
|
116
|
+
|
|
117
|
+
const sprintId = match.sprint_id;
|
|
118
|
+
this.sprintService.updateScopeStatus(sprintId, scopeId, 'completed');
|
|
119
|
+
|
|
120
|
+
// Ensure sprint is in 'in_progress' state
|
|
121
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
122
|
+
if (!sprint) return;
|
|
123
|
+
if (sprint.status === 'dispatched') {
|
|
124
|
+
this.sprintService.updateStatus(sprintId, 'in_progress');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for newly unblocked scopes and dispatch them
|
|
128
|
+
await this.dispatchUnblockedScopes(sprintId);
|
|
129
|
+
this.checkSprintCompletion(sprintId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Called when a scope fails during sprint execution */
|
|
133
|
+
async onScopeFailed(scopeId: number, error?: string): Promise<void> {
|
|
134
|
+
const match = this.sprintService.findActiveSprintForScope(scopeId);
|
|
135
|
+
if (!match) return;
|
|
136
|
+
|
|
137
|
+
const sprintId = match.sprint_id;
|
|
138
|
+
this.sprintService.updateScopeStatus(sprintId, scopeId, 'failed', error);
|
|
139
|
+
|
|
140
|
+
// Skip downstream dependents transitively
|
|
141
|
+
this.skipDownstream(sprintId, scopeId);
|
|
142
|
+
|
|
143
|
+
// Try dispatching other unblocked parallel paths
|
|
144
|
+
await this.dispatchUnblockedScopes(sprintId);
|
|
145
|
+
this.checkSprintCompletion(sprintId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Cancel an active sprint */
|
|
149
|
+
cancelSprint(sprintId: number): boolean {
|
|
150
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
151
|
+
if (!sprint) return false;
|
|
152
|
+
if (!['assembling', 'dispatched', 'in_progress'].includes(sprint.status)) return false;
|
|
153
|
+
|
|
154
|
+
// Mark pending/queued scopes as skipped
|
|
155
|
+
const scopes = this.sprintService.getSprintScopes(sprintId);
|
|
156
|
+
for (const ss of scopes) {
|
|
157
|
+
if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'queued') {
|
|
158
|
+
this.sprintService.updateScopeStatus(sprintId, ss.scope_id, 'skipped');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.sprintService.updateStatus(sprintId, 'cancelled');
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Recover active sprints after server restart */
|
|
167
|
+
async recoverActiveSprints(): Promise<void> {
|
|
168
|
+
const active = this.db.prepare(
|
|
169
|
+
`SELECT id FROM sprints WHERE group_type = 'sprint' AND status IN ('dispatched', 'in_progress')`,
|
|
170
|
+
).all() as Array<{ id: number }>;
|
|
171
|
+
|
|
172
|
+
if (active.length > 0) {
|
|
173
|
+
log.info('Recovering active sprints', { count: active.length });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const { id } of active) {
|
|
177
|
+
// Check if any scopes completed while server was down
|
|
178
|
+
const scopes = this.sprintService.getSprintScopes(id);
|
|
179
|
+
for (const ss of scopes) {
|
|
180
|
+
if (ss.dispatch_status === 'dispatched' || ss.dispatch_status === 'in_progress') {
|
|
181
|
+
// Check actual scope status
|
|
182
|
+
const scope = this.scopeService.getById(ss.scope_id);
|
|
183
|
+
if (scope && this.engine.getStatusOrder(scope.status) >= this.engine.getStatusOrder('dev')) {
|
|
184
|
+
this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await this.dispatchUnblockedScopes(id);
|
|
190
|
+
this.checkSprintCompletion(id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Get execution graph data for visualization */
|
|
195
|
+
getExecutionGraph(sprintId: number): { layers: number[][]; edges: Array<{ from: number; to: number }> } | null {
|
|
196
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
197
|
+
if (!sprint) return null;
|
|
198
|
+
|
|
199
|
+
const layers = sprint.layers ?? [];
|
|
200
|
+
const sprintSet = new Set(sprint.scope_ids);
|
|
201
|
+
const edges: Array<{ from: number; to: number }> = [];
|
|
202
|
+
|
|
203
|
+
for (const scopeId of sprint.scope_ids) {
|
|
204
|
+
const scope = this.scopeService.getById(scopeId);
|
|
205
|
+
if (!scope) continue;
|
|
206
|
+
for (const dep of scope.blocked_by) {
|
|
207
|
+
if (sprintSet.has(dep)) {
|
|
208
|
+
edges.push({ from: dep, to: scopeId });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { layers, edges };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Private Helpers ────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
private async dispatchLayer(sprintId: number, scopeIds: number[], concurrencyCap: number): Promise<void> {
|
|
219
|
+
const toDispatch = scopeIds.slice(0, concurrencyCap);
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < toDispatch.length; i++) {
|
|
222
|
+
const scopeId = toDispatch[i];
|
|
223
|
+
|
|
224
|
+
// Capture current status before optimistic update (for rollback)
|
|
225
|
+
const currentScope = this.scopeService.getById(scopeId);
|
|
226
|
+
const previousStatus = currentScope?.status ?? 'implementing';
|
|
227
|
+
|
|
228
|
+
// Record DISPATCH event
|
|
229
|
+
const eventId = crypto.randomUUID();
|
|
230
|
+
const command = `/scope implement ${scopeId}`;
|
|
231
|
+
this.db.prepare(
|
|
232
|
+
`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
233
|
+
VALUES (?, 'DISPATCH', ?, NULL, 'sprint-orchestrator', ?, ?)`,
|
|
234
|
+
).run(eventId, scopeId, JSON.stringify({ command, sprint_id: sprintId, resolved: null }), new Date().toISOString());
|
|
235
|
+
|
|
236
|
+
this.io.emit('event:new', {
|
|
237
|
+
id: eventId, type: 'DISPATCH', scope_id: scopeId,
|
|
238
|
+
session_id: null, agent: 'sprint-orchestrator',
|
|
239
|
+
data: { command, sprint_id: sprintId, resolved: null },
|
|
240
|
+
timestamp: new Date().toISOString(),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Update scope + sprint_scope status
|
|
244
|
+
this.scopeService.updateStatus(scopeId, 'implementing', 'dispatch');
|
|
245
|
+
this.sprintService.updateScopeStatus(sprintId, scopeId, 'dispatched');
|
|
246
|
+
|
|
247
|
+
// Build scope-aware session name and snapshot PIDs
|
|
248
|
+
const scopeRow = this.scopeService.getById(scopeId);
|
|
249
|
+
const sessionName = buildSessionName({ scopeId, title: scopeRow?.title, command });
|
|
250
|
+
const beforePids = snapshotSessionPids(getConfig().projectRoot);
|
|
251
|
+
|
|
252
|
+
// Launch in iTerm — interactive TUI mode (no -p) for full visibility
|
|
253
|
+
const escaped = escapeForAnsiC(command);
|
|
254
|
+
const fullCmd = `cd '${getConfig().projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
|
|
255
|
+
try {
|
|
256
|
+
await launchInCategorizedTerminal(command, fullCmd, sessionName);
|
|
257
|
+
|
|
258
|
+
// Fire-and-forget: discover session PID, link to dispatch, and rename
|
|
259
|
+
discoverNewSession(getConfig().projectRoot, beforePids)
|
|
260
|
+
.then((session) => {
|
|
261
|
+
if (!session) return;
|
|
262
|
+
linkPidToDispatch(this.db, eventId, session.pid);
|
|
263
|
+
if (sessionName) renameSession(getConfig().projectRoot, session.sessionId, sessionName);
|
|
264
|
+
})
|
|
265
|
+
.catch(err => log.error('PID discovery failed', { error: err.message }));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
// Rollback scope status to previous value
|
|
268
|
+
this.scopeService.updateStatus(scopeId, previousStatus, 'rollback');
|
|
269
|
+
this.sprintService.updateScopeStatus(sprintId, scopeId, 'failed', `Launch failed: ${err}`);
|
|
270
|
+
resolveDispatchEvent(this.db, this.io, eventId, 'failed', `Launch failed: ${err}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Stagger launches to prevent AppleScript race conditions
|
|
274
|
+
if (i < toDispatch.length - 1) {
|
|
275
|
+
await sleep(LAUNCH_STAGGER_MS);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async dispatchUnblockedScopes(sprintId: number): Promise<void> {
|
|
281
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
282
|
+
if (!sprint) return;
|
|
283
|
+
|
|
284
|
+
const scopes = this.sprintService.getSprintScopes(sprintId);
|
|
285
|
+
const completedSet = new Set(
|
|
286
|
+
scopes.filter((ss) => ss.dispatch_status === 'completed').map((ss) => ss.scope_id),
|
|
287
|
+
);
|
|
288
|
+
const activeCount = scopes.filter(
|
|
289
|
+
(ss) => ss.dispatch_status === 'dispatched' || ss.dispatch_status === 'in_progress',
|
|
290
|
+
).length;
|
|
291
|
+
const available = sprint.concurrency_cap - activeCount;
|
|
292
|
+
if (available <= 0) return;
|
|
293
|
+
|
|
294
|
+
// Find pending scopes whose internal deps are all completed
|
|
295
|
+
const ready: number[] = [];
|
|
296
|
+
for (const ss of scopes) {
|
|
297
|
+
if (ss.dispatch_status !== 'pending') continue;
|
|
298
|
+
|
|
299
|
+
const scope = this.scopeService.getById(ss.scope_id);
|
|
300
|
+
if (!scope) continue;
|
|
301
|
+
const internalDeps = scope.blocked_by.filter((d) => sprint.scope_ids.includes(d));
|
|
302
|
+
const allMet = internalDeps.every((d) => completedSet.has(d));
|
|
303
|
+
|
|
304
|
+
if (allMet) ready.push(ss.scope_id);
|
|
305
|
+
if (ready.length >= available) break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (ready.length > 0) {
|
|
309
|
+
await this.dispatchLayer(sprintId, ready, available);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private skipDownstream(sprintId: number, failedScopeId: number): void {
|
|
314
|
+
const scopes = this.sprintService.getSprintScopes(sprintId);
|
|
315
|
+
const sprintScopeIds = scopes.map((ss) => ss.scope_id);
|
|
316
|
+
|
|
317
|
+
// Build reverse dependency map: scope → scopes that depend on it
|
|
318
|
+
const dependents = new Map<number, number[]>();
|
|
319
|
+
for (const scopeId of sprintScopeIds) {
|
|
320
|
+
const scope = this.scopeService.getById(scopeId);
|
|
321
|
+
if (!scope) continue;
|
|
322
|
+
for (const dep of scope.blocked_by) {
|
|
323
|
+
if (!dependents.has(dep)) dependents.set(dep, []);
|
|
324
|
+
dependents.get(dep)!.push(scopeId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// BFS to find all transitive dependents
|
|
329
|
+
const toSkip = new Set<number>();
|
|
330
|
+
const queue = [failedScopeId];
|
|
331
|
+
while (queue.length > 0) {
|
|
332
|
+
const current = queue.shift()!;
|
|
333
|
+
const downstream = dependents.get(current) ?? [];
|
|
334
|
+
for (const id of downstream) {
|
|
335
|
+
if (!toSkip.has(id)) {
|
|
336
|
+
toSkip.add(id);
|
|
337
|
+
queue.push(id);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const scopeId of toSkip) {
|
|
343
|
+
const ss = scopes.find((s) => s.scope_id === scopeId);
|
|
344
|
+
if (ss && ss.dispatch_status === 'pending') {
|
|
345
|
+
this.sprintService.updateScopeStatus(sprintId, scopeId, 'skipped', `Skipped: dependency ${failedScopeId} failed`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private checkSprintCompletion(sprintId: number): void {
|
|
351
|
+
const scopes = this.sprintService.getSprintScopes(sprintId);
|
|
352
|
+
const allDone = scopes.every(
|
|
353
|
+
(ss) => ss.dispatch_status === 'completed' || ss.dispatch_status === 'failed' || ss.dispatch_status === 'skipped',
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (!allDone) return;
|
|
357
|
+
|
|
358
|
+
const anyFailed = scopes.some((ss) => ss.dispatch_status === 'failed');
|
|
359
|
+
this.sprintService.updateStatus(sprintId, anyFailed ? 'failed' : 'completed');
|
|
360
|
+
}
|
|
361
|
+
}
|