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,395 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { Server } from 'socket.io';
|
|
3
|
+
import type { ScopeService } from '../services/scope-service.js';
|
|
4
|
+
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
5
|
+
import { isSessionPidAlive } from './terminal-launcher.js';
|
|
6
|
+
import { createLogger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
const log = createLogger('dispatch');
|
|
9
|
+
|
|
10
|
+
interface DispatchRow {
|
|
11
|
+
data: string;
|
|
12
|
+
scope_id: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Mark a DISPATCH event as resolved and emit socket notification. */
|
|
16
|
+
export function resolveDispatchEvent(
|
|
17
|
+
db: Database.Database,
|
|
18
|
+
io: Server,
|
|
19
|
+
eventId: string,
|
|
20
|
+
outcome: 'completed' | 'failed' | 'abandoned',
|
|
21
|
+
error?: string,
|
|
22
|
+
): void {
|
|
23
|
+
const row = db.prepare('SELECT data, scope_id FROM events WHERE id = ?')
|
|
24
|
+
.get(eventId) as DispatchRow | undefined;
|
|
25
|
+
if (!row) return;
|
|
26
|
+
|
|
27
|
+
let data: Record<string, unknown>;
|
|
28
|
+
try {
|
|
29
|
+
data = JSON.parse(row.data);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
log.error('Failed to parse DISPATCH event data', { eventId, error: String(e) });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
data.resolved = { outcome, at: new Date().toISOString(), ...(error ? { error } : {}) };
|
|
35
|
+
db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
|
|
36
|
+
|
|
37
|
+
io.emit('dispatch:resolved', {
|
|
38
|
+
event_id: eventId,
|
|
39
|
+
scope_id: row.scope_id,
|
|
40
|
+
scope_ids: data.scope_ids ?? null,
|
|
41
|
+
outcome,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Resolve all unresolved DISPATCH events for a given scope */
|
|
46
|
+
export function resolveActiveDispatchesForScope(
|
|
47
|
+
db: Database.Database,
|
|
48
|
+
io: Server,
|
|
49
|
+
scopeId: number,
|
|
50
|
+
outcome: 'completed' | 'failed' | 'abandoned',
|
|
51
|
+
): void {
|
|
52
|
+
const rows = db.prepare(
|
|
53
|
+
`SELECT id FROM events
|
|
54
|
+
WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
|
|
55
|
+
).all(scopeId) as Array<{ id: string }>;
|
|
56
|
+
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
resolveDispatchEvent(db, io, row.id, outcome);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Re-resolve abandoned DISPATCH events for a scope as completed.
|
|
63
|
+
* Used by both recover and dismiss-abandoned routes to clear abandoned state. */
|
|
64
|
+
export function resolveAbandonedDispatchesForScope(
|
|
65
|
+
db: Database.Database,
|
|
66
|
+
io: Server,
|
|
67
|
+
scopeId: number,
|
|
68
|
+
): number {
|
|
69
|
+
const rows = db.prepare(
|
|
70
|
+
`SELECT id FROM events
|
|
71
|
+
WHERE type = 'DISPATCH' AND scope_id = ?
|
|
72
|
+
AND JSON_EXTRACT(data, '$.resolved.outcome') = 'abandoned'`,
|
|
73
|
+
).all(scopeId) as Array<{ id: string }>;
|
|
74
|
+
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
resolveDispatchEvent(db, io, row.id, 'completed');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return rows.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Store the PID of the Claude session working on a dispatch.
|
|
83
|
+
* Called after discoverNewSession finds the launched session, or when
|
|
84
|
+
* a SESSION_START event includes ORBITAL_DISPATCH_ID from the env var. */
|
|
85
|
+
export function linkPidToDispatch(
|
|
86
|
+
db: Database.Database,
|
|
87
|
+
eventId: string,
|
|
88
|
+
pid: number,
|
|
89
|
+
): void {
|
|
90
|
+
const row = db.prepare('SELECT data FROM events WHERE id = ?')
|
|
91
|
+
.get(eventId) as { data: string } | undefined;
|
|
92
|
+
if (!row) return;
|
|
93
|
+
let data: Record<string, unknown>;
|
|
94
|
+
try {
|
|
95
|
+
data = JSON.parse(row.data);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
log.error('Failed to parse DISPATCH event data', { eventId, error: String(e) });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
data.pid = pid;
|
|
101
|
+
db.prepare('UPDATE events SET data = ? WHERE id = ?').run(JSON.stringify(data), eventId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve all unresolved DISPATCH events linked to a specific PID.
|
|
105
|
+
* Called when a SESSION_END event is received, indicating the Claude session
|
|
106
|
+
* process has exited and its dispatches should be cleared.
|
|
107
|
+
*
|
|
108
|
+
* NOTE: Does NOT revert scope status. Skills like /scope-implement intentionally
|
|
109
|
+
* keep scopes at the transition target (e.g. "implementing") after completion.
|
|
110
|
+
* Reverting on session end was destroying completed work and deleting scope files. */
|
|
111
|
+
export function resolveDispatchesByPid(
|
|
112
|
+
db: Database.Database,
|
|
113
|
+
io: Server,
|
|
114
|
+
pid: number,
|
|
115
|
+
): number {
|
|
116
|
+
const rows = db.prepare(
|
|
117
|
+
`SELECT id FROM events
|
|
118
|
+
WHERE type = 'DISPATCH'
|
|
119
|
+
AND JSON_EXTRACT(data, '$.resolved') IS NULL
|
|
120
|
+
AND JSON_EXTRACT(data, '$.pid') = ?`,
|
|
121
|
+
).all(pid) as Array<{ id: string }>;
|
|
122
|
+
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
resolveDispatchEvent(db, io, row.id, 'abandoned');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return rows.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Resolve all unresolved DISPATCH events linked to a specific dispatch ID.
|
|
131
|
+
* Called when a SESSION_END event includes dispatch_id from ORBITAL_DISPATCH_ID env var.
|
|
132
|
+
* Defaults to 'abandoned' — successful completions emit AGENT_COMPLETED first
|
|
133
|
+
* which resolves via inferScopeStatus as 'completed'. */
|
|
134
|
+
export function resolveDispatchesByDispatchId(
|
|
135
|
+
db: Database.Database,
|
|
136
|
+
io: Server,
|
|
137
|
+
dispatchId: string,
|
|
138
|
+
): number {
|
|
139
|
+
const row = db.prepare(
|
|
140
|
+
`SELECT id FROM events
|
|
141
|
+
WHERE id = ? AND type = 'DISPATCH' AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
|
|
142
|
+
).get(dispatchId) as { id: string } | undefined;
|
|
143
|
+
|
|
144
|
+
if (!row) return 0;
|
|
145
|
+
resolveDispatchEvent(db, io, row.id, 'abandoned');
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Fallback age threshold for dispatches without a linked PID (30 minutes). */
|
|
150
|
+
const STALE_AGE_MS = 30 * 60 * 1000;
|
|
151
|
+
|
|
152
|
+
/** Get all scope IDs that have actively running DISPATCH events.
|
|
153
|
+
* Uses PID liveness (process.kill(pid, 0)) when available, falls back to
|
|
154
|
+
* age-based heuristic for legacy dispatches without a linked PID. */
|
|
155
|
+
export function getActiveScopeIds(db: Database.Database, scopeService: ScopeService, engine: WorkflowEngine): number[] {
|
|
156
|
+
const rows = db.prepare(
|
|
157
|
+
`SELECT scope_id, data FROM events
|
|
158
|
+
WHERE type = 'DISPATCH'
|
|
159
|
+
AND scope_id IS NOT NULL
|
|
160
|
+
AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
|
|
161
|
+
).all() as Array<{ scope_id: number; data: string }>;
|
|
162
|
+
|
|
163
|
+
const cutoff = new Date(Date.now() - STALE_AGE_MS).toISOString();
|
|
164
|
+
const active = new Set<number>();
|
|
165
|
+
|
|
166
|
+
for (const row of rows) {
|
|
167
|
+
if (active.has(row.scope_id)) continue; // already confirmed active
|
|
168
|
+
|
|
169
|
+
// Skip scopes in terminal states
|
|
170
|
+
const scope = scopeService.getById(row.scope_id);
|
|
171
|
+
if (scope && engine.isTerminalStatus(scope.status)) continue;
|
|
172
|
+
|
|
173
|
+
let data: Record<string, unknown>;
|
|
174
|
+
try {
|
|
175
|
+
data = JSON.parse(row.data);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
log.error('Failed to parse DISPATCH event data', { scope_id: row.scope_id, error: String(e) });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (typeof data.pid === 'number') {
|
|
181
|
+
// Preferred: check if the Claude session process is still running
|
|
182
|
+
if (isSessionPidAlive(data.pid)) {
|
|
183
|
+
active.add(row.scope_id);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Fallback for legacy dispatches without PID: use age-based check
|
|
187
|
+
const dispatch = db.prepare(
|
|
188
|
+
`SELECT timestamp FROM events
|
|
189
|
+
WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
|
|
190
|
+
ORDER BY timestamp DESC LIMIT 1`,
|
|
191
|
+
).get(row.scope_id) as { timestamp: string } | undefined;
|
|
192
|
+
if (dispatch && dispatch.timestamp > cutoff) {
|
|
193
|
+
active.add(row.scope_id);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Also check batch dispatches (scope_id IS NULL, batch = true)
|
|
199
|
+
const batchRows = db.prepare(
|
|
200
|
+
`SELECT data FROM events
|
|
201
|
+
WHERE type = 'DISPATCH'
|
|
202
|
+
AND scope_id IS NULL
|
|
203
|
+
AND JSON_EXTRACT(data, '$.batch') = 1
|
|
204
|
+
AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
|
|
205
|
+
).all() as Array<{ data: string }>;
|
|
206
|
+
|
|
207
|
+
for (const batchRow of batchRows) {
|
|
208
|
+
let batchData: Record<string, unknown>;
|
|
209
|
+
try {
|
|
210
|
+
batchData = JSON.parse(batchRow.data);
|
|
211
|
+
} catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const scopeIds = batchData.scope_ids as number[] | undefined;
|
|
216
|
+
if (!Array.isArray(scopeIds)) continue;
|
|
217
|
+
|
|
218
|
+
let batchAlive = false;
|
|
219
|
+
if (typeof batchData.pid === 'number') {
|
|
220
|
+
batchAlive = isSessionPidAlive(batchData.pid);
|
|
221
|
+
} else {
|
|
222
|
+
// No PID — consider active (stale cleanup will catch it)
|
|
223
|
+
batchAlive = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (batchAlive) {
|
|
227
|
+
for (const id of scopeIds) {
|
|
228
|
+
const scope = scopeService.getById(id);
|
|
229
|
+
if (scope && !engine.isTerminalStatus(scope.status)) {
|
|
230
|
+
active.add(id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return [...active];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Resolve stale DISPATCH events. Three staleness criteria:
|
|
240
|
+
* 1. Scope already in a terminal state (as defined by workflow config)
|
|
241
|
+
* 2. Linked PID is no longer running (session ended/crashed)
|
|
242
|
+
* 3. No linked PID and dispatch older than STALE_AGE_MS (fallback)
|
|
243
|
+
* Called once at startup and periodically to clean up unresolved dispatches.
|
|
244
|
+
*
|
|
245
|
+
* NOTE: Does NOT revert scope status. Skills like /scope-implement intentionally
|
|
246
|
+
* keep scopes at the transition target after completion. Auto-reverting was
|
|
247
|
+
* destroying completed work and deleting scope files. Users can manually
|
|
248
|
+
* move scopes back from the dashboard if needed. */
|
|
249
|
+
export function resolveStaleDispatches(db: Database.Database, io: Server, scopeService: ScopeService, engine: WorkflowEngine): number {
|
|
250
|
+
const cutoff = new Date(Date.now() - STALE_AGE_MS).toISOString();
|
|
251
|
+
|
|
252
|
+
// Single query on events only — split by cache status
|
|
253
|
+
const rows = db.prepare(
|
|
254
|
+
`SELECT id, scope_id, data, timestamp FROM events
|
|
255
|
+
WHERE type = 'DISPATCH'
|
|
256
|
+
AND scope_id IS NOT NULL
|
|
257
|
+
AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
|
|
258
|
+
).all() as Array<{ id: string; scope_id: number; data: string; timestamp: string }>;
|
|
259
|
+
|
|
260
|
+
let resolved = 0;
|
|
261
|
+
|
|
262
|
+
for (const row of rows) {
|
|
263
|
+
const scope = scopeService.getById(row.scope_id);
|
|
264
|
+
const scopeStatus = scope?.status;
|
|
265
|
+
|
|
266
|
+
// Criterion 1: scope in terminal state
|
|
267
|
+
if (scopeStatus && engine.isTerminalStatus(scopeStatus)) {
|
|
268
|
+
resolveDispatchEvent(db, io, row.id, 'completed');
|
|
269
|
+
resolved++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Criteria 2+3: dead PID or old age
|
|
274
|
+
let data: Record<string, unknown>;
|
|
275
|
+
try {
|
|
276
|
+
data = JSON.parse(row.data);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
log.error('Failed to parse DISPATCH event data', { eventId: row.id, error: String(e) });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
let isStale = false;
|
|
282
|
+
|
|
283
|
+
if (typeof data.pid === 'number') {
|
|
284
|
+
isStale = !isSessionPidAlive(data.pid);
|
|
285
|
+
} else {
|
|
286
|
+
isStale = row.timestamp <= cutoff;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (isStale) {
|
|
290
|
+
resolveDispatchEvent(db, io, row.id, 'abandoned');
|
|
291
|
+
resolved++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Second pass: batch dispatches (scope_id IS NULL, batch = true)
|
|
296
|
+
const batchRows = db.prepare(
|
|
297
|
+
`SELECT id, data, timestamp FROM events
|
|
298
|
+
WHERE type = 'DISPATCH'
|
|
299
|
+
AND scope_id IS NULL
|
|
300
|
+
AND JSON_EXTRACT(data, '$.batch') = 1
|
|
301
|
+
AND JSON_EXTRACT(data, '$.resolved') IS NULL`,
|
|
302
|
+
).all() as Array<{ id: string; data: string; timestamp: string }>;
|
|
303
|
+
|
|
304
|
+
for (const batchRow of batchRows) {
|
|
305
|
+
let batchData: Record<string, unknown>;
|
|
306
|
+
try {
|
|
307
|
+
batchData = JSON.parse(batchRow.data);
|
|
308
|
+
} catch {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const scopeIds = batchData.scope_ids as number[] | undefined;
|
|
313
|
+
|
|
314
|
+
// Criterion 1: all batch scopes in terminal state
|
|
315
|
+
if (Array.isArray(scopeIds) && scopeIds.length > 0) {
|
|
316
|
+
const allTerminal = scopeIds.every(id => {
|
|
317
|
+
const scope = scopeService.getById(id);
|
|
318
|
+
return scope && engine.isTerminalStatus(scope.status);
|
|
319
|
+
});
|
|
320
|
+
if (allTerminal) {
|
|
321
|
+
resolveDispatchEvent(db, io, batchRow.id, 'completed');
|
|
322
|
+
resolved++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Criteria 2+3: dead PID or old age
|
|
328
|
+
if (typeof batchData.pid === 'number') {
|
|
329
|
+
if (!isSessionPidAlive(batchData.pid)) {
|
|
330
|
+
resolveDispatchEvent(db, io, batchRow.id, 'abandoned');
|
|
331
|
+
resolved++;
|
|
332
|
+
}
|
|
333
|
+
} else if (batchRow.timestamp <= cutoff) {
|
|
334
|
+
resolveDispatchEvent(db, io, batchRow.id, 'abandoned');
|
|
335
|
+
resolved++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return resolved;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Get scope IDs with recent abandoned dispatch outcomes.
|
|
343
|
+
* Returns an array of abandoned scope entries with scope_id, from_status, and abandoned_at.
|
|
344
|
+
* Only includes scopes that are NOT currently in a terminal state and
|
|
345
|
+
* do NOT have a newer active (unresolved) dispatch. */
|
|
346
|
+
export function getAbandonedScopeIds(
|
|
347
|
+
db: Database.Database,
|
|
348
|
+
scopeService: ScopeService,
|
|
349
|
+
engine: WorkflowEngine,
|
|
350
|
+
activeScopeIds?: number[],
|
|
351
|
+
): Array<{ scope_id: number; from_status: string | null; abandoned_at: string }> {
|
|
352
|
+
const rows = db.prepare(
|
|
353
|
+
`SELECT scope_id, data, timestamp FROM events
|
|
354
|
+
WHERE type = 'DISPATCH'
|
|
355
|
+
AND scope_id IS NOT NULL
|
|
356
|
+
AND JSON_EXTRACT(data, '$.resolved.outcome') = 'abandoned'
|
|
357
|
+
ORDER BY timestamp DESC`,
|
|
358
|
+
).all() as Array<{ scope_id: number; data: string; timestamp: string }>;
|
|
359
|
+
|
|
360
|
+
// Get active scope IDs to exclude scopes with new dispatches
|
|
361
|
+
const activeScopes = activeScopeIds ?? getActiveScopeIds(db, scopeService, engine);
|
|
362
|
+
const activeSet = new Set(activeScopes);
|
|
363
|
+
|
|
364
|
+
const seen = new Set<number>();
|
|
365
|
+
const result: Array<{ scope_id: number; from_status: string | null; abandoned_at: string }> = [];
|
|
366
|
+
|
|
367
|
+
for (const row of rows) {
|
|
368
|
+
if (seen.has(row.scope_id)) continue;
|
|
369
|
+
seen.add(row.scope_id);
|
|
370
|
+
|
|
371
|
+
// Skip if scope has a new active dispatch
|
|
372
|
+
if (activeSet.has(row.scope_id)) continue;
|
|
373
|
+
|
|
374
|
+
// Skip if scope is in terminal state
|
|
375
|
+
const scope = scopeService.getById(row.scope_id);
|
|
376
|
+
if (!scope) continue;
|
|
377
|
+
if (engine.isTerminalStatus(scope.status)) continue;
|
|
378
|
+
|
|
379
|
+
let data: Record<string, unknown>;
|
|
380
|
+
try {
|
|
381
|
+
data = JSON.parse(row.data);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
log.error('Failed to parse DISPATCH event data', { scope_id: row.scope_id, error: String(e) });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const transition = data.transition as Record<string, unknown> | null;
|
|
387
|
+
const resolved = data.resolved as Record<string, unknown> | null;
|
|
388
|
+
const fromStatus = transition?.from as string ?? null;
|
|
389
|
+
const abandonedAt = resolved?.at as string ?? row.timestamp;
|
|
390
|
+
|
|
391
|
+
result.push({ scope_id: row.scope_id, from_status: fromStatus, abandoned_at: abandonedAt });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ─── Lightweight Structured Logger ──────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Zero dependencies. Colored, timestamped, component-namespaced output.
|
|
4
|
+
// Usage:
|
|
5
|
+
// import { createLogger } from './utils/logger.js';
|
|
6
|
+
// const log = createLogger('scope');
|
|
7
|
+
// log.info('Status updated', { id: 3, from: 'backlog', to: 'implementing' });
|
|
8
|
+
// // => 12:34:56.789 INFO [scope] Status updated id=3 from=backlog to=implementing
|
|
9
|
+
|
|
10
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
11
|
+
|
|
12
|
+
const LEVEL_VALUE: Record<LogLevel, number> = {
|
|
13
|
+
debug: 0,
|
|
14
|
+
info: 1,
|
|
15
|
+
warn: 2,
|
|
16
|
+
error: 3,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let currentLevel: LogLevel = 'info';
|
|
20
|
+
|
|
21
|
+
export function setLogLevel(level: LogLevel): void {
|
|
22
|
+
currentLevel = level;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getLogLevel(): LogLevel {
|
|
26
|
+
return currentLevel;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Colors (ANSI) ──────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
32
|
+
|
|
33
|
+
const c = {
|
|
34
|
+
reset: useColor ? '\x1b[0m' : '',
|
|
35
|
+
dim: useColor ? '\x1b[2m' : '',
|
|
36
|
+
gray: useColor ? '\x1b[90m' : '',
|
|
37
|
+
cyan: useColor ? '\x1b[36m' : '',
|
|
38
|
+
yellow: useColor ? '\x1b[33m' : '',
|
|
39
|
+
red: useColor ? '\x1b[31m' : '',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const LEVEL_COLOR: Record<LogLevel, string> = {
|
|
43
|
+
debug: c.gray,
|
|
44
|
+
info: c.cyan,
|
|
45
|
+
warn: c.yellow,
|
|
46
|
+
error: c.red,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const LEVEL_LABEL: Record<LogLevel, string> = {
|
|
50
|
+
debug: 'DEBUG',
|
|
51
|
+
info: 'INFO ',
|
|
52
|
+
warn: 'WARN ',
|
|
53
|
+
error: 'ERROR',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ─── Formatting ─────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function timestamp(): string {
|
|
59
|
+
const d = new Date();
|
|
60
|
+
const h = String(d.getHours()).padStart(2, '0');
|
|
61
|
+
const m = String(d.getMinutes()).padStart(2, '0');
|
|
62
|
+
const s = String(d.getSeconds()).padStart(2, '0');
|
|
63
|
+
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
64
|
+
return `${h}:${m}:${s}.${ms}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatData(data?: Record<string, unknown>): string {
|
|
68
|
+
if (!data) return '';
|
|
69
|
+
const pairs: string[] = [];
|
|
70
|
+
for (const [k, v] of Object.entries(data)) {
|
|
71
|
+
if (v === undefined || v === null) continue;
|
|
72
|
+
const val = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
73
|
+
pairs.push(`${k}=${val}`);
|
|
74
|
+
}
|
|
75
|
+
return pairs.length > 0 ? ' ' + pairs.join(' ') : '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Logger Factory ─────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export interface Logger {
|
|
81
|
+
debug(msg: string, data?: Record<string, unknown>): void;
|
|
82
|
+
info(msg: string, data?: Record<string, unknown>): void;
|
|
83
|
+
warn(msg: string, data?: Record<string, unknown>): void;
|
|
84
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function write(level: LogLevel, component: string, msg: string, data?: Record<string, unknown>): void {
|
|
88
|
+
if (LEVEL_VALUE[level] < LEVEL_VALUE[currentLevel]) return;
|
|
89
|
+
|
|
90
|
+
const color = LEVEL_COLOR[level];
|
|
91
|
+
const label = LEVEL_LABEL[level];
|
|
92
|
+
const kv = formatData(data);
|
|
93
|
+
const line = `${c.dim}${timestamp()}${c.reset} ${color}${label}${c.reset} ${c.dim}[${component}]${c.reset} ${msg}${kv}\n`;
|
|
94
|
+
|
|
95
|
+
if (level === 'warn' || level === 'error') {
|
|
96
|
+
process.stderr.write(line);
|
|
97
|
+
} else {
|
|
98
|
+
process.stdout.write(line);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createLogger(component: string): Logger {
|
|
103
|
+
return {
|
|
104
|
+
debug: (msg, data) => write('debug', component, msg, data),
|
|
105
|
+
info: (msg, data) => write('info', component, msg, data),
|
|
106
|
+
warn: (msg, data) => write('warn', component, msg, data),
|
|
107
|
+
error: (msg, data) => write('error', component, msg, data),
|
|
108
|
+
};
|
|
109
|
+
}
|