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,286 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { Server } from 'socket.io';
|
|
3
|
+
import type { SprintService } from './sprint-service.js';
|
|
4
|
+
import type { ScopeService } from './scope-service.js';
|
|
5
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, snapshotSessionPids, discoverNewSession, isSessionPidAlive } from '../utils/terminal-launcher.js';
|
|
6
|
+
import { linkPidToDispatch, resolveDispatchEvent } 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('batch');
|
|
12
|
+
const VALID_MERGE_MODES = ['push', 'pr'] as const;
|
|
13
|
+
|
|
14
|
+
// ─── Orchestrator ───────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export class BatchOrchestrator {
|
|
17
|
+
constructor(
|
|
18
|
+
private db: Database.Database,
|
|
19
|
+
private io: Server,
|
|
20
|
+
private sprintService: SprintService,
|
|
21
|
+
private scopeService: ScopeService,
|
|
22
|
+
private engine: WorkflowEngine,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
/** Dispatch a batch — validates constraints and routes to column-specific handler */
|
|
26
|
+
async dispatch(batchId: number, mergeMode?: string): Promise<{ ok: boolean; error?: string }> {
|
|
27
|
+
const batch = this.sprintService.getById(batchId);
|
|
28
|
+
if (!batch) return { ok: false, error: 'Batch not found' };
|
|
29
|
+
if (batch.group_type !== 'batch') return { ok: false, error: 'Not a batch group' };
|
|
30
|
+
if (batch.status !== 'assembling') return { ok: false, error: `Batch status is '${batch.status}', expected 'assembling'` };
|
|
31
|
+
if (batch.scope_ids.length === 0) return { ok: false, error: 'Batch has no scopes' };
|
|
32
|
+
|
|
33
|
+
// W-4: One active batch per column
|
|
34
|
+
const existingActive = this.sprintService.findActiveBatchForColumn(batch.target_column);
|
|
35
|
+
if (existingActive && existingActive.id !== batchId) {
|
|
36
|
+
return { ok: false, error: `Column '${batch.target_column}' already has an active batch (ID: ${existingActive.id})` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const command = this.engine.getBatchCommand(batch.target_column);
|
|
40
|
+
if (!command) return { ok: false, error: `No dispatch command for column '${batch.target_column}'` };
|
|
41
|
+
|
|
42
|
+
// Mark batch as dispatched
|
|
43
|
+
this.sprintService.updateStatus(batchId, 'dispatched');
|
|
44
|
+
log.info('Batch dispatched', { id: batchId, target_column: batch.target_column, scope_ids: batch.scope_ids });
|
|
45
|
+
|
|
46
|
+
// Build scope IDs env var prefix (W-1: prepend to command, not process.env)
|
|
47
|
+
const scopeIdsStr = batch.scope_ids.join(',');
|
|
48
|
+
const mergeModeStr = (VALID_MERGE_MODES as readonly string[]).includes(mergeMode ?? '') ? mergeMode! : 'push';
|
|
49
|
+
|
|
50
|
+
// Record DISPATCH event
|
|
51
|
+
const eventId = crypto.randomUUID();
|
|
52
|
+
const eventData = {
|
|
53
|
+
command,
|
|
54
|
+
batch_id: batchId,
|
|
55
|
+
scope_ids: batch.scope_ids,
|
|
56
|
+
target_column: batch.target_column,
|
|
57
|
+
batch: true,
|
|
58
|
+
resolved: null,
|
|
59
|
+
};
|
|
60
|
+
this.db.prepare(
|
|
61
|
+
`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
62
|
+
VALUES (?, 'DISPATCH', NULL, NULL, 'batch-orchestrator', ?, ?)`,
|
|
63
|
+
).run(eventId, JSON.stringify(eventData), new Date().toISOString());
|
|
64
|
+
|
|
65
|
+
this.io.emit('event:new', {
|
|
66
|
+
id: eventId, type: 'DISPATCH', scope_id: null,
|
|
67
|
+
session_id: null, agent: 'batch-orchestrator',
|
|
68
|
+
data: eventData, timestamp: new Date().toISOString(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Launch single CLI session with BATCH_SCOPE_IDS prepended to command
|
|
72
|
+
const escaped = escapeForAnsiC(command);
|
|
73
|
+
const fullCmd = `cd '${getConfig().projectRoot}' && BATCH_SCOPE_IDS='${scopeIdsStr}' MERGE_MODE='${mergeModeStr}' claude --dangerously-skip-permissions $'${escaped}'`;
|
|
74
|
+
const beforePids = snapshotSessionPids(getConfig().projectRoot);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await launchInCategorizedTerminal(command, fullCmd);
|
|
78
|
+
|
|
79
|
+
// Store dispatch result timestamp
|
|
80
|
+
this.sprintService.updateDispatchResult(batchId, {
|
|
81
|
+
dispatched_at: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Fire-and-forget: discover session PID and link to dispatch
|
|
85
|
+
discoverNewSession(getConfig().projectRoot, beforePids)
|
|
86
|
+
.then((session) => {
|
|
87
|
+
if (!session) return;
|
|
88
|
+
linkPidToDispatch(this.db, eventId, session.pid);
|
|
89
|
+
// Store PID on the batch for two-phase completion
|
|
90
|
+
const currentResult = this.sprintService.getById(batchId)?.dispatch_result ?? {};
|
|
91
|
+
this.sprintService.updateDispatchResult(batchId, {
|
|
92
|
+
...currentResult,
|
|
93
|
+
dispatched_at: currentResult.dispatched_at ?? new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
// Store PID in event data for later liveness checking
|
|
96
|
+
const row = this.db.prepare('SELECT data FROM events WHERE id = ?').get(eventId) as { data: string } | undefined;
|
|
97
|
+
if (row) {
|
|
98
|
+
const data = JSON.parse(row.data);
|
|
99
|
+
data.pid = session.pid;
|
|
100
|
+
this.db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.catch(err => log.error('PID discovery failed', { error: err.message }));
|
|
104
|
+
|
|
105
|
+
return { ok: true };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
this.sprintService.updateStatus(batchId, 'failed');
|
|
108
|
+
resolveDispatchEvent(this.db, this.io, eventId, 'failed', String(err));
|
|
109
|
+
return { ok: false, error: `Failed to launch terminal: ${err}` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Called when a scope reaches a new status — check if it satisfies a batch,
|
|
114
|
+
* or remove the scope from the batch if its status diverged from the target. */
|
|
115
|
+
onScopeStatusChanged(scopeId: number, newStatus: string): void {
|
|
116
|
+
// Find any active batch containing this scope
|
|
117
|
+
const match = this.sprintService.findActiveSprintForScope(scopeId);
|
|
118
|
+
if (!match) return;
|
|
119
|
+
|
|
120
|
+
const batch = this.sprintService.getById(match.sprint_id);
|
|
121
|
+
if (!batch || batch.group_type !== 'batch') return;
|
|
122
|
+
|
|
123
|
+
const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
|
|
124
|
+
if (newStatus === expectedStatus || this.engine.isTerminalStatus(newStatus)) {
|
|
125
|
+
this.sprintService.updateScopeStatus(batch.id, scopeId, 'completed');
|
|
126
|
+
|
|
127
|
+
// Check if all scopes have transitioned
|
|
128
|
+
if (batch.status === 'dispatched') {
|
|
129
|
+
this.sprintService.updateStatus(batch.id, 'in_progress');
|
|
130
|
+
}
|
|
131
|
+
} else if (newStatus !== batch.target_column) {
|
|
132
|
+
// Scope diverged from batch target — remove it from the batch
|
|
133
|
+
this.sprintService.forceRemoveScope(batch.id, scopeId);
|
|
134
|
+
|
|
135
|
+
// If batch is now empty, mark it as failed
|
|
136
|
+
const remaining = this.sprintService.getSprintScopes(batch.id);
|
|
137
|
+
if (remaining.length === 0 && batch.status !== 'assembling') {
|
|
138
|
+
this.sprintService.updateStatus(batch.id, 'failed');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Called when a dispatched session PID dies — second phase of two-phase completion.
|
|
144
|
+
* Reverts un-transitioned scopes to their pre-dispatch status. */
|
|
145
|
+
onSessionPidDied(batchId: number): void {
|
|
146
|
+
const batch = this.sprintService.getById(batchId);
|
|
147
|
+
if (!batch || batch.group_type !== 'batch') return;
|
|
148
|
+
if (batch.status !== 'dispatched' && batch.status !== 'in_progress') return;
|
|
149
|
+
|
|
150
|
+
const scopes = this.sprintService.getSprintScopes(batchId);
|
|
151
|
+
const allTransitioned = scopes.every((ss) => ss.dispatch_status === 'completed');
|
|
152
|
+
|
|
153
|
+
if (allTransitioned) {
|
|
154
|
+
this.sprintService.updateStatus(batchId, 'completed');
|
|
155
|
+
} else {
|
|
156
|
+
const pending = scopes.filter((ss) => ss.dispatch_status !== 'completed').map((ss) => ss.scope_id);
|
|
157
|
+
this.sprintService.updateStatus(batchId, 'failed');
|
|
158
|
+
// Mark un-transitioned scopes as failed and revert their status
|
|
159
|
+
for (const scopeId of pending) {
|
|
160
|
+
this.sprintService.updateScopeStatus(batchId, scopeId, 'failed', 'Session exited before scope transitioned');
|
|
161
|
+
// Revert scope to pre-dispatch status (the batch's source column)
|
|
162
|
+
this.scopeService.updateStatus(scopeId, batch.target_column, 'rollback');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolve stale batches — catches batches stuck due to lost PIDs, Orbital downtime, or
|
|
169
|
+
* missing PID records. Unlike recoverActiveBatches (which focuses on PID polling),
|
|
170
|
+
* this also resolves batches where no PID was ever recorded.
|
|
171
|
+
*/
|
|
172
|
+
resolveStaleBatches(): number {
|
|
173
|
+
const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
174
|
+
|
|
175
|
+
const active = this.db.prepare(
|
|
176
|
+
`SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`,
|
|
177
|
+
).all() as Array<{ id: number }>;
|
|
178
|
+
|
|
179
|
+
if (active.length > 0) {
|
|
180
|
+
log.debug('Checking stale batches', { activeCount: active.length });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let resolved = 0;
|
|
184
|
+
|
|
185
|
+
for (const { id } of active) {
|
|
186
|
+
const batch = this.sprintService.getById(id);
|
|
187
|
+
if (!batch) continue;
|
|
188
|
+
|
|
189
|
+
const scopes = this.sprintService.getSprintScopes(id);
|
|
190
|
+
const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
|
|
191
|
+
|
|
192
|
+
// Phase 1: auto-complete scopes that reached or passed target status
|
|
193
|
+
for (const ss of scopes) {
|
|
194
|
+
if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
|
|
195
|
+
const scope = this.scopeService.getById(ss.scope_id);
|
|
196
|
+
if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
|
|
197
|
+
this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Phase 2: check PID liveness (check both unresolved and resolved events —
|
|
203
|
+
// SESSION_END may have resolved the dispatch event before we get here)
|
|
204
|
+
const dispatchEvent = this.db.prepare(
|
|
205
|
+
`SELECT data FROM events
|
|
206
|
+
WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
|
|
207
|
+
ORDER BY timestamp DESC LIMIT 1`,
|
|
208
|
+
).get(id) as { data: string } | undefined;
|
|
209
|
+
|
|
210
|
+
let pidDead = false;
|
|
211
|
+
|
|
212
|
+
if (dispatchEvent) {
|
|
213
|
+
const data = JSON.parse(dispatchEvent.data) as Record<string, unknown>;
|
|
214
|
+
// If the dispatch event is already resolved, the session is definitely done
|
|
215
|
+
if (data.resolved != null) {
|
|
216
|
+
pidDead = true;
|
|
217
|
+
} else if (typeof data.pid === 'number') {
|
|
218
|
+
pidDead = !isSessionPidAlive(data.pid);
|
|
219
|
+
} else {
|
|
220
|
+
// No PID recorded — check if batch is old enough to consider stale
|
|
221
|
+
const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
|
|
222
|
+
pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// No dispatch event at all — check age
|
|
226
|
+
const dispatchedAt = batch.dispatched_at ? new Date(batch.dispatched_at).getTime() : 0;
|
|
227
|
+
pidDead = Date.now() - dispatchedAt > STALE_THRESHOLD_MS;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (pidDead) {
|
|
231
|
+
this.onSessionPidDied(id);
|
|
232
|
+
resolved++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return resolved;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Recover active batches after server restart (W-3) */
|
|
240
|
+
async recoverActiveBatches(): Promise<void> {
|
|
241
|
+
const active = this.db.prepare(
|
|
242
|
+
`SELECT id FROM sprints WHERE group_type = 'batch' AND status IN ('dispatched', 'in_progress')`,
|
|
243
|
+
).all() as Array<{ id: number }>;
|
|
244
|
+
|
|
245
|
+
if (active.length > 0) {
|
|
246
|
+
log.debug('Recovering active batches', { count: active.length });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const { id } of active) {
|
|
250
|
+
const batch = this.sprintService.getById(id);
|
|
251
|
+
if (!batch) continue;
|
|
252
|
+
|
|
253
|
+
const scopes = this.sprintService.getSprintScopes(id);
|
|
254
|
+
const expectedStatus = this.engine.getBatchTargetStatus(batch.target_column);
|
|
255
|
+
|
|
256
|
+
// Check if scopes reached or passed target status while server was down
|
|
257
|
+
for (const ss of scopes) {
|
|
258
|
+
if (ss.dispatch_status === 'pending' || ss.dispatch_status === 'dispatched') {
|
|
259
|
+
const scope = this.scopeService.getById(ss.scope_id);
|
|
260
|
+
if (scope && (scope.status === expectedStatus || this.engine.isTerminalStatus(scope.status))) {
|
|
261
|
+
this.sprintService.updateScopeStatus(id, ss.scope_id, 'completed');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check if dispatch PID is still alive (include resolved events —
|
|
267
|
+
// SESSION_END may have resolved the dispatch before server restart)
|
|
268
|
+
const dispatchEvent = this.db.prepare(
|
|
269
|
+
`SELECT data FROM events
|
|
270
|
+
WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.batch_id') = ?
|
|
271
|
+
ORDER BY timestamp DESC LIMIT 1`,
|
|
272
|
+
).get(id) as { data: string } | undefined;
|
|
273
|
+
|
|
274
|
+
if (dispatchEvent) {
|
|
275
|
+
const data = JSON.parse(dispatchEvent.data);
|
|
276
|
+
if (data.resolved != null) {
|
|
277
|
+
// Dispatch already resolved — session is done
|
|
278
|
+
this.onSessionPidDied(id);
|
|
279
|
+
} else if (typeof data.pid === 'number' && !isSessionPidAlive(data.pid)) {
|
|
280
|
+
// PID is dead — trigger two-phase completion check
|
|
281
|
+
this.onSessionPidDied(id);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|