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,275 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
import type { Server } from 'socket.io';
|
|
4
|
+
import type { ScopeService } from '../services/scope-service.js';
|
|
5
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
6
|
+
import { resolveDispatchEvent, resolveAbandonedDispatchesForScope, getActiveScopeIds, getAbandonedScopeIds, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
7
|
+
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
const log = createLogger('dispatch');
|
|
11
|
+
|
|
12
|
+
const MAX_BATCH_SIZE = 20;
|
|
13
|
+
|
|
14
|
+
interface DispatchBody {
|
|
15
|
+
scope_id?: number;
|
|
16
|
+
command: string;
|
|
17
|
+
prompt?: string;
|
|
18
|
+
transition?: { from: string; to: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DispatchRouteDeps {
|
|
22
|
+
db: Database.Database;
|
|
23
|
+
io: Server;
|
|
24
|
+
scopeService: ScopeService;
|
|
25
|
+
projectRoot: string;
|
|
26
|
+
engine: WorkflowEngine;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine }: DispatchRouteDeps): Router {
|
|
30
|
+
const router = Router();
|
|
31
|
+
|
|
32
|
+
router.get('/dispatch/active-scopes', (_req, res) => {
|
|
33
|
+
const scope_ids = getActiveScopeIds(db, scopeService, engine);
|
|
34
|
+
const abandoned_scopes = getAbandonedScopeIds(db, scopeService, engine, scope_ids);
|
|
35
|
+
res.json({ scope_ids, abandoned_scopes });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
router.get('/dispatch/active', (req, res) => {
|
|
39
|
+
const scopeId = Number(req.query.scope_id);
|
|
40
|
+
if (isNaN(scopeId) || scopeId <= 0) {
|
|
41
|
+
res.status(400).json({ error: 'Valid scope_id query param required' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const active = db.prepare(
|
|
45
|
+
`SELECT id, timestamp, JSON_EXTRACT(data, '$.command') as command
|
|
46
|
+
FROM events
|
|
47
|
+
WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
|
|
48
|
+
ORDER BY timestamp DESC LIMIT 1`
|
|
49
|
+
).get(scopeId) as { id: string; timestamp: string; command: string } | undefined;
|
|
50
|
+
|
|
51
|
+
res.json({ active: active ?? null });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
router.post('/dispatch', async (req, res) => {
|
|
55
|
+
const { scope_id, command, prompt, transition } = req.body as DispatchBody;
|
|
56
|
+
|
|
57
|
+
if (!command || !engine.isAllowedCommand(command)) {
|
|
58
|
+
res.status(400).json({ error: 'Command must start with /scope-, /git-, /test-, or /session-' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// W-11: Validate prompt field against allowed command prefixes
|
|
63
|
+
if (prompt && !engine.isAllowedCommand(prompt)) {
|
|
64
|
+
res.status(400).json({ error: 'Prompt must start with /scope-, /git-, /test-, or /session-' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Active session guard
|
|
69
|
+
if (scope_id != null) {
|
|
70
|
+
const active = db.prepare(
|
|
71
|
+
`SELECT id FROM events
|
|
72
|
+
WHERE type = 'DISPATCH' AND scope_id = ? AND JSON_EXTRACT(data, '$.resolved') IS NULL
|
|
73
|
+
ORDER BY timestamp DESC LIMIT 1`
|
|
74
|
+
).get(scope_id) as { id: string } | undefined;
|
|
75
|
+
|
|
76
|
+
if (active) {
|
|
77
|
+
res.status(409).json({ error: 'Active dispatch exists', dispatch_id: active.id });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Update scope status if transition provided
|
|
83
|
+
if (scope_id != null && transition?.to) {
|
|
84
|
+
const result = scopeService.updateStatus(scope_id, transition.to, 'dispatch');
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
res.status(400).json({ error: result.error });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Record DISPATCH event
|
|
92
|
+
const eventId = crypto.randomUUID();
|
|
93
|
+
const eventData = { command, transition: transition ?? null, resolved: null };
|
|
94
|
+
db.prepare(
|
|
95
|
+
`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
96
|
+
VALUES (?, 'DISPATCH', ?, NULL, 'dashboard', ?, ?)`
|
|
97
|
+
).run(eventId, scope_id ?? null, JSON.stringify(eventData), new Date().toISOString());
|
|
98
|
+
|
|
99
|
+
io.emit('event:new', {
|
|
100
|
+
id: eventId, type: 'DISPATCH', scope_id: scope_id ?? null,
|
|
101
|
+
session_id: null, agent: 'dashboard', data: eventData,
|
|
102
|
+
timestamp: new Date().toISOString(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Build scope-aware session name before launch
|
|
106
|
+
const scope = scope_id != null ? scopeService.getById(scope_id) : undefined;
|
|
107
|
+
const sessionName = buildSessionName({ scopeId: scope_id ?? undefined, title: scope?.title, command });
|
|
108
|
+
const beforePids = snapshotSessionPids(projectRoot);
|
|
109
|
+
|
|
110
|
+
// Launch in iTerm — interactive TUI mode (no -p) for full visibility
|
|
111
|
+
const promptText = prompt ?? command;
|
|
112
|
+
const escaped = escapeForAnsiC(promptText);
|
|
113
|
+
const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions $'${escaped}'`;
|
|
114
|
+
try {
|
|
115
|
+
await launchInCategorizedTerminal(command, fullCmd, sessionName);
|
|
116
|
+
res.json({ ok: true, dispatch_id: eventId, scope_id: scope_id ?? null });
|
|
117
|
+
|
|
118
|
+
// Fire-and-forget: discover session PID, link to dispatch, and rename
|
|
119
|
+
discoverNewSession(projectRoot, beforePids)
|
|
120
|
+
.then((session) => {
|
|
121
|
+
if (!session) return;
|
|
122
|
+
linkPidToDispatch(db, eventId, session.pid);
|
|
123
|
+
if (sessionName) renameSession(projectRoot, session.sessionId, sessionName);
|
|
124
|
+
})
|
|
125
|
+
.catch(err => log.error('PID discovery failed', { error: err.message }));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (scope_id != null && transition?.from) {
|
|
128
|
+
scopeService.updateStatus(scope_id, transition.from, 'rollback');
|
|
129
|
+
}
|
|
130
|
+
resolveDispatchEvent(db, io, eventId, 'failed', String(err));
|
|
131
|
+
res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
router.post('/dispatch/:id/resolve', (req, res) => {
|
|
136
|
+
const eventId = req.params.id;
|
|
137
|
+
const row = db.prepare('SELECT id FROM events WHERE id = ? AND type = ?')
|
|
138
|
+
.get(eventId, 'DISPATCH') as { id: string } | undefined;
|
|
139
|
+
|
|
140
|
+
if (!row) {
|
|
141
|
+
res.status(404).json({ error: 'Dispatch event not found' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
resolveDispatchEvent(db, io, eventId, 'completed');
|
|
146
|
+
res.json({ ok: true, dispatch_id: eventId });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/** Recover an abandoned scope by reverting it to its pre-dispatch status. */
|
|
150
|
+
router.post('/dispatch/recover/:scopeId', (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const scopeId = Number(req.params.scopeId);
|
|
153
|
+
if (isNaN(scopeId) || scopeId <= 0) {
|
|
154
|
+
res.status(400).json({ error: 'Valid scopeId required' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { from_status } = req.body as { from_status?: string };
|
|
159
|
+
if (!from_status) {
|
|
160
|
+
res.status(400).json({ error: 'from_status is required' });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Revert scope to its pre-dispatch status
|
|
165
|
+
const result = scopeService.updateStatus(scopeId, from_status, 'rollback');
|
|
166
|
+
if (!result.ok) {
|
|
167
|
+
res.status(400).json({ error: result.error });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
resolveAbandonedDispatchesForScope(db, io, scopeId);
|
|
172
|
+
res.json({ ok: true, scope_id: scopeId, reverted_to: from_status });
|
|
173
|
+
} catch (err) {
|
|
174
|
+
log.error('Error recovering scope', { error: String(err) });
|
|
175
|
+
res.status(500).json({ error: 'Internal server error', details: String(err) });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/** Dismiss abandoned state without reverting scope status. */
|
|
180
|
+
router.post('/dispatch/dismiss-abandoned/:scopeId', (req, res) => {
|
|
181
|
+
try {
|
|
182
|
+
const scopeId = Number(req.params.scopeId);
|
|
183
|
+
if (isNaN(scopeId) || scopeId <= 0) {
|
|
184
|
+
res.status(400).json({ error: 'Valid scopeId required' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const dismissed = resolveAbandonedDispatchesForScope(db, io, scopeId);
|
|
189
|
+
res.json({ ok: true, scope_id: scopeId, dismissed });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
log.error('Error dismissing abandoned dispatches', { error: String(err) });
|
|
192
|
+
res.status(500).json({ error: 'Internal server error', details: String(err) });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
router.post('/dispatch/batch', async (req, res) => {
|
|
197
|
+
const { scope_ids, command, transition } = req.body as {
|
|
198
|
+
scope_ids: number[];
|
|
199
|
+
command: string;
|
|
200
|
+
transition?: { from: string; to: string };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (!command || !engine.isAllowedCommand(command)) {
|
|
204
|
+
res.status(400).json({ error: 'Command must start with /scope-, /git-, /test-, or /session-' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!Array.isArray(scope_ids) || scope_ids.length === 0) {
|
|
209
|
+
res.status(400).json({ error: 'scope_ids must be a non-empty array' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// W-12: Validate batch size and scope ID types
|
|
214
|
+
if (scope_ids.length > MAX_BATCH_SIZE) {
|
|
215
|
+
res.status(400).json({ error: `Maximum batch size is ${MAX_BATCH_SIZE}` });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!scope_ids.every(id => Number.isInteger(id) && id > 0)) {
|
|
219
|
+
res.status(400).json({ error: 'scope_ids must contain positive integers' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Update all scope statuses
|
|
224
|
+
if (transition?.to) {
|
|
225
|
+
for (const id of scope_ids) {
|
|
226
|
+
const result = scopeService.updateStatus(id, transition.to, 'dispatch');
|
|
227
|
+
if (!result.ok) {
|
|
228
|
+
res.status(400).json({ error: `Scope ${id}: ${result.error}` });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Record single DISPATCH event for the batch
|
|
235
|
+
const eventId = crypto.randomUUID();
|
|
236
|
+
const eventData = { command, transition: transition ?? null, scope_ids, batch: true, resolved: null };
|
|
237
|
+
db.prepare(
|
|
238
|
+
`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
239
|
+
VALUES (?, 'DISPATCH', NULL, NULL, 'dashboard', ?, ?)`
|
|
240
|
+
).run(eventId, JSON.stringify(eventData), new Date().toISOString());
|
|
241
|
+
|
|
242
|
+
io.emit('event:new', {
|
|
243
|
+
id: eventId, type: 'DISPATCH', scope_id: null,
|
|
244
|
+
session_id: null, agent: 'dashboard', data: eventData,
|
|
245
|
+
timestamp: new Date().toISOString(),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Launch single CLI session
|
|
249
|
+
const batchEscaped = escapeForAnsiC(command);
|
|
250
|
+
const beforePids = snapshotSessionPids(projectRoot);
|
|
251
|
+
const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions -p $'${batchEscaped}'`;
|
|
252
|
+
try {
|
|
253
|
+
await launchInCategorizedTerminal(command, fullCmd);
|
|
254
|
+
res.json({ ok: true, dispatch_id: eventId, scope_ids });
|
|
255
|
+
|
|
256
|
+
// Fire-and-forget: discover session PID and link to dispatch
|
|
257
|
+
discoverNewSession(projectRoot, beforePids)
|
|
258
|
+
.then((session) => {
|
|
259
|
+
if (!session) return;
|
|
260
|
+
linkPidToDispatch(db, eventId, session.pid);
|
|
261
|
+
})
|
|
262
|
+
.catch(err => log.error('Batch PID discovery failed', { error: err.message }));
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (transition?.from) {
|
|
265
|
+
for (const id of scope_ids) {
|
|
266
|
+
scopeService.updateStatus(id, transition.from, 'rollback');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
resolveDispatchEvent(db, io, eventId, 'failed', String(err));
|
|
270
|
+
res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return router;
|
|
275
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type { GitService } from '../services/git-service.js';
|
|
3
|
+
import type { GitHubService } from '../services/github-service.js';
|
|
4
|
+
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
5
|
+
|
|
6
|
+
interface GitRoutesDeps {
|
|
7
|
+
gitService: GitService;
|
|
8
|
+
githubService: GitHubService;
|
|
9
|
+
engine: WorkflowEngine;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createGitRoutes({ gitService, githubService, engine }: GitRoutesDeps): Router {
|
|
13
|
+
const router = Router();
|
|
14
|
+
|
|
15
|
+
// ─── Git Overview ──────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
router.get('/git/overview', async (_req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const config = engine.getConfig();
|
|
20
|
+
const branchingMode = config.branchingMode ?? 'trunk';
|
|
21
|
+
const overview = await gitService.getOverview(branchingMode);
|
|
22
|
+
res.json(overview);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
res.status(500).json({ error: 'Failed to get git overview', details: String(err) });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ─── Commits ──────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
router.get('/git/commits', async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const branch = (req.query.branch as string) || undefined;
|
|
33
|
+
const limit = Number(req.query.limit) || 50;
|
|
34
|
+
const offset = Number(req.query.offset) || 0;
|
|
35
|
+
const commits = await gitService.getCommits({ branch, limit, offset });
|
|
36
|
+
res.json(commits);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
res.status(500).json({ error: 'Failed to get commits', details: String(err) });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── Branches ──────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
router.get('/git/branches', async (_req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const branches = await gitService.getBranches();
|
|
47
|
+
res.json(branches);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
res.status(500).json({ error: 'Failed to get branches', details: String(err) });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Enhanced Worktrees ────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
router.get('/git/worktrees', async (_req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const worktrees = await gitService.getEnhancedWorktrees();
|
|
58
|
+
res.json(worktrees);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
res.status(500).json({ error: 'Failed to get worktrees', details: String(err) });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ─── Dynamic Drift ─────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
router.get('/git/drift', async (_req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
// Build drift pairs from workflow lists that have gitBranch set
|
|
69
|
+
const config = engine.getConfig();
|
|
70
|
+
const listsWithBranch = config.lists
|
|
71
|
+
.filter(l => l.gitBranch)
|
|
72
|
+
.sort((a, b) => a.order - b.order);
|
|
73
|
+
|
|
74
|
+
const pairs: Array<{ from: string; to: string }> = [];
|
|
75
|
+
for (let i = 0; i < listsWithBranch.length - 1; i++) {
|
|
76
|
+
pairs.push({
|
|
77
|
+
from: listsWithBranch[i].gitBranch!,
|
|
78
|
+
to: listsWithBranch[i + 1].gitBranch!,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const drift = await gitService.getDrift(pairs);
|
|
83
|
+
res.json(drift);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
res.status(500).json({ error: 'Failed to compute drift', details: String(err) });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── GitHub Status ─────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
router.get('/github/status', async (_req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const status = await githubService.getStatus();
|
|
94
|
+
res.json(status);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
res.status(500).json({ error: 'Failed to get GitHub status', details: String(err) });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ─── GitHub PRs ────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
router.get('/github/prs', async (_req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const prs = await githubService.getOpenPRs();
|
|
105
|
+
res.json(prs);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
res.status(500).json({ error: 'Failed to get PRs', details: String(err) });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return router;
|
|
112
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import type Database from 'better-sqlite3';
|
|
4
|
+
import type { Server } from 'socket.io';
|
|
5
|
+
import type { ScopeService } from '../services/scope-service.js';
|
|
6
|
+
import type { ReadinessService } from '../services/readiness-service.js';
|
|
7
|
+
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
|
+
import { launchInTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
9
|
+
import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
10
|
+
import { getConfig } from '../config.js';
|
|
11
|
+
import { createLogger } from '../utils/logger.js';
|
|
12
|
+
|
|
13
|
+
const log = createLogger('dispatch');
|
|
14
|
+
|
|
15
|
+
interface ScopeRouteDeps {
|
|
16
|
+
db: Database.Database;
|
|
17
|
+
io: Server;
|
|
18
|
+
scopeService: ScopeService;
|
|
19
|
+
readinessService: ReadinessService;
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
engine: WorkflowEngine;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createScopeRoutes({ db, io, scopeService, readinessService, projectRoot, engine }: ScopeRouteDeps): Router {
|
|
25
|
+
const router = Router();
|
|
26
|
+
|
|
27
|
+
// ─── Scope CRUD ──────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
router.get('/scopes', (_req, res) => {
|
|
30
|
+
res.json(scopeService.getAll());
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ─── Transition Readiness ──────────────────────────────────
|
|
34
|
+
|
|
35
|
+
router.get('/scopes/:id/readiness', (req, res) => {
|
|
36
|
+
const readiness = readinessService.getReadiness(Number(req.params.id));
|
|
37
|
+
if (!readiness) {
|
|
38
|
+
res.status(404).json({ error: 'Scope not found' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
res.json(readiness);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Bulk update — must come before :id route to avoid matching "bulk" as an id */
|
|
45
|
+
router.patch('/scopes/bulk/status', (req, res) => {
|
|
46
|
+
const { scopes } = req.body as { scopes: Array<{ id: number; status: string }> };
|
|
47
|
+
if (!Array.isArray(scopes)) {
|
|
48
|
+
res.status(400).json({ error: 'Expected { scopes: [{id, status}] }' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let updated = 0;
|
|
52
|
+
for (const { id, status } of scopes) {
|
|
53
|
+
const result = scopeService.updateStatus(id, status, 'bulk-sync');
|
|
54
|
+
if (result.ok) updated++;
|
|
55
|
+
}
|
|
56
|
+
res.json({ updated, total: scopes.length });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
router.get('/scopes/:id', (req, res) => {
|
|
60
|
+
const scope = scopeService.getById(Number(req.params.id));
|
|
61
|
+
if (!scope) {
|
|
62
|
+
res.status(404).json({ error: 'Scope not found' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
res.json(scope);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
router.patch('/scopes/:id', (req, res) => {
|
|
69
|
+
const id = Number(req.params.id);
|
|
70
|
+
const result = scopeService.updateScopeFrontmatter(id, req.body);
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
const code = result.code === 'NOT_FOUND' ? 404 : 400;
|
|
73
|
+
res.status(code).json({ error: result.error, code: result.code });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const scope = scopeService.getById(id);
|
|
77
|
+
res.json(scope ?? { ok: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ─── Idea Routes ─────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
router.post('/ideas', (req, res) => {
|
|
83
|
+
const { title, description } = req.body as { title?: string; description?: string };
|
|
84
|
+
if (!title?.trim()) {
|
|
85
|
+
res.status(400).json({ error: 'title is required' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const idea = scopeService.createIdeaFile(title.trim(), (description ?? '').trim());
|
|
89
|
+
res.status(201).json(idea);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
router.patch('/ideas/:id', (req, res) => {
|
|
93
|
+
const id = Number(req.params.id);
|
|
94
|
+
const { title, description } = req.body as { title?: string; description?: string };
|
|
95
|
+
if (!title?.trim()) {
|
|
96
|
+
res.status(400).json({ error: 'title is required' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const updated = scopeService.updateIdeaFile(id, title.trim(), (description ?? '').trim());
|
|
100
|
+
if (!updated) {
|
|
101
|
+
res.status(404).json({ error: 'Idea not found' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
res.json({ ok: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
router.delete('/ideas/:id', (req, res) => {
|
|
108
|
+
const id = Number(req.params.id);
|
|
109
|
+
const deleted = scopeService.deleteIdeaFile(id);
|
|
110
|
+
if (!deleted) {
|
|
111
|
+
res.status(404).json({ error: 'Idea not found' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
res.json({ ok: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
router.post('/ideas/:id/promote', async (req, res) => {
|
|
118
|
+
const ideaId = Number(req.params.id);
|
|
119
|
+
const result = scopeService.promoteIdea(ideaId);
|
|
120
|
+
if (!result) {
|
|
121
|
+
res.status(404).json({ error: 'Idea not found' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const scopeId = result.id;
|
|
126
|
+
|
|
127
|
+
// Read command from workflow edge config (user-overridable)
|
|
128
|
+
const entryPoint = engine.getEntryPoint();
|
|
129
|
+
const targets = engine.getValidTargets(entryPoint.id);
|
|
130
|
+
const promoteTarget = targets[0] ?? 'planning';
|
|
131
|
+
const edge = engine.findEdge(entryPoint.id, promoteTarget);
|
|
132
|
+
const edgeCommand = edge ? engine.buildCommand(edge, scopeId) : null;
|
|
133
|
+
const command = edgeCommand ?? `/scope-create ${String(scopeId).padStart(3, '0')}`;
|
|
134
|
+
|
|
135
|
+
// Record DISPATCH event for audit trail
|
|
136
|
+
const eventId = crypto.randomUUID();
|
|
137
|
+
const eventData = {
|
|
138
|
+
command,
|
|
139
|
+
transition: { from: entryPoint.id, to: promoteTarget },
|
|
140
|
+
resolved: null,
|
|
141
|
+
};
|
|
142
|
+
db.prepare(
|
|
143
|
+
`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
144
|
+
VALUES (?, 'DISPATCH', ?, NULL, 'dashboard', ?, ?)`
|
|
145
|
+
).run(eventId, scopeId, JSON.stringify(eventData), new Date().toISOString());
|
|
146
|
+
|
|
147
|
+
io.emit('event:new', {
|
|
148
|
+
id: eventId, type: 'DISPATCH', scope_id: scopeId,
|
|
149
|
+
session_id: null, agent: 'dashboard', data: eventData,
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const escaped = escapeForAnsiC(command);
|
|
154
|
+
const fullCmd = `cd '${projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
|
|
155
|
+
|
|
156
|
+
const promoteSessionName = buildSessionName({ scopeId, title: result.title, command });
|
|
157
|
+
const promoteBeforePids = snapshotSessionPids(projectRoot);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await launchInTerminal(fullCmd);
|
|
161
|
+
res.json({ ok: true, id: scopeId, filePath: result.filePath });
|
|
162
|
+
|
|
163
|
+
discoverNewSession(projectRoot, promoteBeforePids)
|
|
164
|
+
.then((session) => {
|
|
165
|
+
if (!session) return;
|
|
166
|
+
linkPidToDispatch(db, eventId, session.pid);
|
|
167
|
+
if (promoteSessionName) renameSession(projectRoot, session.sessionId, promoteSessionName);
|
|
168
|
+
})
|
|
169
|
+
.catch(err => log.error('PID discovery failed', { error: err.message }));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
resolveDispatchEvent(db, io, eventId, 'failed', String(err));
|
|
172
|
+
res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ─── Surprise Me (AI idea generation) ────────────────────
|
|
177
|
+
|
|
178
|
+
let surpriseInProgress = false;
|
|
179
|
+
|
|
180
|
+
router.post('/ideas/surprise', (_req, res) => {
|
|
181
|
+
if (surpriseInProgress) {
|
|
182
|
+
res.status(409).json({ error: 'Surprise generation already in progress' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
surpriseInProgress = true;
|
|
186
|
+
|
|
187
|
+
const nextIdStart = scopeService.getNextIceboxId();
|
|
188
|
+
const today = new Date().toISOString().split('T')[0];
|
|
189
|
+
const idRange = Array.from({ length: 5 }, (_, i) => nextIdStart + i);
|
|
190
|
+
|
|
191
|
+
const prompt = `You are analyzing the ${getConfig().projectName} codebase to suggest feature ideas. Your ONLY job is to create markdown files.
|
|
192
|
+
|
|
193
|
+
Create exactly 3 idea files in the scopes/icebox/ directory. Each file must use this EXACT format:
|
|
194
|
+
|
|
195
|
+
File: scopes/icebox/{ID}-{kebab-slug}.md
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
id: {ID}
|
|
199
|
+
title: "{title}"
|
|
200
|
+
status: icebox
|
|
201
|
+
ghost: true
|
|
202
|
+
created: ${today}
|
|
203
|
+
updated: ${today}
|
|
204
|
+
blocked_by: []
|
|
205
|
+
blocks: []
|
|
206
|
+
tags: []
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
{2-3 sentence description of the feature, what problem it solves, and a rough approach.}
|
|
210
|
+
|
|
211
|
+
Use these IDs: ${idRange[0]}, ${idRange[1]}, ${idRange[2]}
|
|
212
|
+
|
|
213
|
+
Rules:
|
|
214
|
+
- Focus on practical improvements: performance, UX, security, developer experience, monitoring, or reliability
|
|
215
|
+
- Be specific and actionable — not vague architectural rewrites
|
|
216
|
+
- Keep descriptions concise (2-3 sentences max)
|
|
217
|
+
- Filenames must be {ID}-{kebab-case-slug}.md
|
|
218
|
+
- The ghost: true field is required in frontmatter
|
|
219
|
+
- Do NOT create any other files or make any other changes`;
|
|
220
|
+
|
|
221
|
+
const child = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
|
|
222
|
+
cwd: projectRoot,
|
|
223
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
224
|
+
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'orbital-surprise' },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
child.unref();
|
|
228
|
+
|
|
229
|
+
child.on('close', () => {
|
|
230
|
+
surpriseInProgress = false;
|
|
231
|
+
const eventId = crypto.randomUUID();
|
|
232
|
+
io.emit('event:new', {
|
|
233
|
+
id: eventId, type: 'AGENT_COMPLETED', scope_id: null,
|
|
234
|
+
session_id: null, agent: 'surprise-me',
|
|
235
|
+
data: { action: 'surprise-ideas-generated' },
|
|
236
|
+
timestamp: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
child.on('error', () => {
|
|
241
|
+
surpriseInProgress = false;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
res.json({ ok: true, status: 'generating' });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
router.post('/ideas/:id/approve', (req, res) => {
|
|
248
|
+
const id = Number(req.params.id);
|
|
249
|
+
const approved = scopeService.approveGhostIdea(id);
|
|
250
|
+
if (!approved) {
|
|
251
|
+
res.status(404).json({ error: 'Ghost idea not found' });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
res.json({ ok: true });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
router.get('/ideas/surprise/status', (_req, res) => {
|
|
258
|
+
res.json({ generating: surpriseInProgress });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return router;
|
|
262
|
+
}
|