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,170 @@
|
|
|
1
|
+
// --- Type Aliases ---
|
|
2
|
+
|
|
3
|
+
export type TransitionContext = 'patch' | 'dispatch' | 'event' | 'bulk-sync' | 'rollback';
|
|
4
|
+
export type TransitionResult = { ok: true } | { ok: false; error: string; code: string };
|
|
5
|
+
export type ConfirmLevel = 'quick' | 'full';
|
|
6
|
+
export type EdgeDirection = 'forward' | 'backward' | 'shortcut';
|
|
7
|
+
export type HookCategory = 'guard' | 'gate' | 'lifecycle' | 'observer';
|
|
8
|
+
export type HookEnforcement = 'blocker' | 'advisor' | 'operator' | 'silent';
|
|
9
|
+
|
|
10
|
+
// --- Claude Code Hook Types ---
|
|
11
|
+
|
|
12
|
+
export type CcHookEvent = 'SessionStart' | 'SessionEnd' | 'PreToolUse' | 'PostToolUse';
|
|
13
|
+
export type HookSource = 'workflow' | 'claude-code' | 'both';
|
|
14
|
+
|
|
15
|
+
export interface CcHookParsed {
|
|
16
|
+
id: string;
|
|
17
|
+
scriptPath: string;
|
|
18
|
+
scriptName: string;
|
|
19
|
+
event: CcHookEvent;
|
|
20
|
+
matcher: string | null;
|
|
21
|
+
statusMessage: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CcTrigger {
|
|
25
|
+
event: CcHookEvent;
|
|
26
|
+
matcher: string | null;
|
|
27
|
+
statusMessage: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UnifiedHook {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
scriptPath: string;
|
|
34
|
+
source: HookSource;
|
|
35
|
+
workflow?: {
|
|
36
|
+
timing: 'before' | 'after';
|
|
37
|
+
type: 'shell' | 'event' | 'webhook';
|
|
38
|
+
category: HookCategory;
|
|
39
|
+
blocking: boolean;
|
|
40
|
+
description?: string;
|
|
41
|
+
};
|
|
42
|
+
ccTriggers?: CcTrigger[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Interfaces ---
|
|
46
|
+
|
|
47
|
+
export interface WorkflowConfig {
|
|
48
|
+
version: 1;
|
|
49
|
+
name: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
branchingMode?: 'trunk' | 'worktree';
|
|
52
|
+
lists: WorkflowList[];
|
|
53
|
+
edges: WorkflowEdge[];
|
|
54
|
+
hooks?: WorkflowHook[];
|
|
55
|
+
groups?: ListGroup[];
|
|
56
|
+
eventInference?: EventInferenceRule[];
|
|
57
|
+
allowedCommandPrefixes?: string[];
|
|
58
|
+
terminalStatuses?: string[];
|
|
59
|
+
commitBranchPatterns?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WorkflowList {
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
order: number;
|
|
66
|
+
group?: string;
|
|
67
|
+
color: string;
|
|
68
|
+
hex: string;
|
|
69
|
+
isEntryPoint?: boolean;
|
|
70
|
+
supportsBatch?: boolean;
|
|
71
|
+
supportsSprint?: boolean;
|
|
72
|
+
hasDirectory: boolean;
|
|
73
|
+
gitBranch?: string;
|
|
74
|
+
sessionKey?: string;
|
|
75
|
+
activeHooks?: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface WorkflowEdge {
|
|
79
|
+
from: string;
|
|
80
|
+
to: string;
|
|
81
|
+
direction: EdgeDirection;
|
|
82
|
+
command: string | null;
|
|
83
|
+
confirmLevel: ConfirmLevel;
|
|
84
|
+
checklist?: string[];
|
|
85
|
+
label: string;
|
|
86
|
+
description: string;
|
|
87
|
+
skipServerTransition?: boolean;
|
|
88
|
+
dispatchOnly?: boolean;
|
|
89
|
+
humanOnly?: boolean;
|
|
90
|
+
hooks?: string[];
|
|
91
|
+
agents?: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface WorkflowHook {
|
|
95
|
+
id: string;
|
|
96
|
+
label: string;
|
|
97
|
+
timing: 'before' | 'after';
|
|
98
|
+
type: 'shell' | 'event' | 'webhook';
|
|
99
|
+
target: string;
|
|
100
|
+
blocking?: boolean;
|
|
101
|
+
description?: string;
|
|
102
|
+
category: HookCategory;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ListGroup {
|
|
106
|
+
id: string;
|
|
107
|
+
label: string;
|
|
108
|
+
order: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface EventInferenceRule {
|
|
112
|
+
eventType: string;
|
|
113
|
+
targetStatus: string;
|
|
114
|
+
dataField?: string;
|
|
115
|
+
dataMap?: Record<string, string>;
|
|
116
|
+
conditions?: Record<string, unknown>;
|
|
117
|
+
forwardOnly: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Type Guards ---
|
|
121
|
+
|
|
122
|
+
export function isWorkflowConfig(obj: unknown): obj is WorkflowConfig {
|
|
123
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
124
|
+
const o = obj as Record<string, unknown>;
|
|
125
|
+
return (
|
|
126
|
+
o.version === 1 &&
|
|
127
|
+
typeof o.name === 'string' &&
|
|
128
|
+
Array.isArray(o.lists) &&
|
|
129
|
+
Array.isArray(o.edges) &&
|
|
130
|
+
(o.lists as unknown[]).every(isWorkflowList) &&
|
|
131
|
+
(o.edges as unknown[]).every(isWorkflowEdge) &&
|
|
132
|
+
(o.branchingMode === undefined || o.branchingMode === 'trunk' || o.branchingMode === 'worktree')
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function isWorkflowList(obj: unknown): obj is WorkflowList {
|
|
137
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
138
|
+
const o = obj as Record<string, unknown>;
|
|
139
|
+
return (
|
|
140
|
+
typeof o.id === 'string' &&
|
|
141
|
+
typeof o.label === 'string' &&
|
|
142
|
+
typeof o.order === 'number' &&
|
|
143
|
+
typeof o.color === 'string' &&
|
|
144
|
+
typeof o.hex === 'string' &&
|
|
145
|
+
typeof o.hasDirectory === 'boolean'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const CATEGORY_TO_ENFORCEMENT: Record<HookCategory, HookEnforcement> = {
|
|
150
|
+
guard: 'blocker',
|
|
151
|
+
gate: 'advisor',
|
|
152
|
+
lifecycle: 'operator',
|
|
153
|
+
observer: 'silent',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function getHookEnforcement(hook: WorkflowHook): HookEnforcement {
|
|
157
|
+
return CATEGORY_TO_ENFORCEMENT[hook.category];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function isWorkflowEdge(obj: unknown): obj is WorkflowEdge {
|
|
161
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
162
|
+
const o = obj as Record<string, unknown>;
|
|
163
|
+
return (
|
|
164
|
+
typeof o.from === 'string' &&
|
|
165
|
+
typeof o.to === 'string' &&
|
|
166
|
+
typeof o.direction === 'string' &&
|
|
167
|
+
typeof o.label === 'string' &&
|
|
168
|
+
typeof o.description === 'string'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowConfig, WorkflowList, WorkflowEdge, WorkflowHook,
|
|
3
|
+
TransitionContext, TransitionResult, EdgeDirection,
|
|
4
|
+
HookCategory, HookEnforcement,
|
|
5
|
+
} from './workflow-config.js';
|
|
6
|
+
import { getHookEnforcement } from './workflow-config.js';
|
|
7
|
+
|
|
8
|
+
// ─── WorkflowEngine ─────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// Config-driven query layer over the WorkflowConfig JSON.
|
|
11
|
+
// Pure class — no I/O, no global state, no side effects.
|
|
12
|
+
|
|
13
|
+
export class WorkflowEngine {
|
|
14
|
+
private config!: WorkflowConfig;
|
|
15
|
+
private listMap!: Map<string, WorkflowList>;
|
|
16
|
+
private edgeMap!: Map<string, WorkflowEdge>;
|
|
17
|
+
private edgesByFrom!: Map<string, WorkflowEdge[]>;
|
|
18
|
+
private statusOrder!: Map<string, number>;
|
|
19
|
+
private hookMap!: Map<string, WorkflowHook>;
|
|
20
|
+
private terminalStatuses!: Set<string>;
|
|
21
|
+
private allowedPrefixes!: string[];
|
|
22
|
+
|
|
23
|
+
constructor(config: WorkflowConfig) {
|
|
24
|
+
this.init(config);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hot-reload the engine with a new config. All services holding a reference
|
|
29
|
+
* to this engine instance will see the updated config immediately.
|
|
30
|
+
*/
|
|
31
|
+
reload(config: WorkflowConfig): void {
|
|
32
|
+
this.init(config);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private init(config: WorkflowConfig): void {
|
|
36
|
+
this.config = config;
|
|
37
|
+
|
|
38
|
+
if (!config.lists.length) throw new Error('WorkflowConfig must have at least 1 list');
|
|
39
|
+
if (!config.edges.length) throw new Error('WorkflowConfig must have at least 1 edge');
|
|
40
|
+
|
|
41
|
+
const entryPoints = config.lists.filter((l) => l.isEntryPoint);
|
|
42
|
+
if (entryPoints.length !== 1) {
|
|
43
|
+
throw new Error(`WorkflowConfig must have exactly 1 entry point, found ${entryPoints.length}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.listMap = new Map(config.lists.map((l) => [l.id, l]));
|
|
47
|
+
this.edgeMap = new Map(config.edges.map((e) => [`${e.from}:${e.to}`, e]));
|
|
48
|
+
|
|
49
|
+
this.edgesByFrom = new Map<string, WorkflowEdge[]>();
|
|
50
|
+
for (const edge of config.edges) {
|
|
51
|
+
const existing = this.edgesByFrom.get(edge.from);
|
|
52
|
+
if (existing) existing.push(edge);
|
|
53
|
+
else this.edgesByFrom.set(edge.from, [edge]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.statusOrder = new Map(config.lists.map((l) => [l.id, l.order]));
|
|
57
|
+
this.hookMap = new Map((config.hooks ?? []).map((h) => [h.id, h]));
|
|
58
|
+
this.terminalStatuses = new Set(config.terminalStatuses ?? []);
|
|
59
|
+
this.allowedPrefixes = config.allowedCommandPrefixes ?? [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Config Access ──────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
getConfig(): Readonly<WorkflowConfig> {
|
|
65
|
+
return this.config;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getBranchingMode(): 'trunk' | 'worktree' {
|
|
69
|
+
return this.config.branchingMode ?? 'trunk';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── List Queries ───────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
getLists(): WorkflowList[] {
|
|
75
|
+
return [...this.config.lists].sort((a, b) => a.order - b.order);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getList(id: string): WorkflowList | undefined {
|
|
79
|
+
return this.listMap.get(id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getEntryPoint(): WorkflowList {
|
|
83
|
+
return this.config.lists.find((l) => l.isEntryPoint)!;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getBatchLists(): WorkflowList[] {
|
|
87
|
+
return this.config.lists.filter((l) => l.supportsBatch);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getSprintLists(): WorkflowList[] {
|
|
91
|
+
return this.config.lists.filter((l) => l.supportsSprint);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getBoardColumns(): Array<{ id: string; label: string; color: string }> {
|
|
95
|
+
return this.getLists().map((l) => ({ id: l.id, label: l.label, color: l.color }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Edge Queries ───────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
findEdge(from: string, to: string): WorkflowEdge | undefined {
|
|
101
|
+
return this.edgeMap.get(`${from}:${to}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
isValidTransition(from: string, to: string): boolean {
|
|
105
|
+
return this.edgeMap.has(`${from}:${to}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getValidTargets(from: string): string[] {
|
|
109
|
+
return (this.edgesByFrom.get(from) ?? []).map((e) => e.to);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getAllEdges(): readonly WorkflowEdge[] {
|
|
113
|
+
return this.config.edges;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getEdgesByDirection(dir: EdgeDirection): WorkflowEdge[] {
|
|
117
|
+
return this.config.edges.filter((e) => e.direction === dir);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Validation ─────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
validateTransition(from: string, to: string, context: TransitionContext): TransitionResult {
|
|
123
|
+
if (!this.listMap.has(to)) {
|
|
124
|
+
return { ok: false, error: `Invalid status: '${to}'`, code: 'INVALID_STATUS' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// bulk-sync and rollback are trusted contexts — skip transition checks
|
|
128
|
+
if (context === 'bulk-sync' || context === 'rollback') return { ok: true };
|
|
129
|
+
|
|
130
|
+
// Same status is a no-op, always allowed
|
|
131
|
+
if (from === to) return { ok: true };
|
|
132
|
+
|
|
133
|
+
const edge = this.findEdge(from, to);
|
|
134
|
+
if (!edge) {
|
|
135
|
+
return { ok: false, error: `Transition '${from}' -> '${to}' is not allowed`, code: 'INVALID_TRANSITION' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// patch context cannot trigger dispatch-only transitions
|
|
139
|
+
if (context === 'patch' && edge.dispatchOnly) {
|
|
140
|
+
return { ok: false, error: `Transition '${from}' -> '${to}' requires dispatch (use a skill command)`, code: 'DISPATCH_REQUIRED' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { ok: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
isValidStatus(status: string): boolean {
|
|
147
|
+
return this.listMap.has(status);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
isTerminalStatus(status: string): boolean {
|
|
151
|
+
return this.terminalStatuses.has(status);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Commands ───────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
buildCommand(edge: WorkflowEdge, scopeId: number): string | null {
|
|
157
|
+
if (!edge.command) return null;
|
|
158
|
+
return edge.command.replace('{id}', String(scopeId));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
isAllowedCommand(cmd: string): boolean {
|
|
162
|
+
return this.allowedPrefixes.some((prefix) => cmd.startsWith(prefix));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Batch / Sprint ─────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
getBatchTargetStatus(column: string): string | undefined {
|
|
168
|
+
const edges = this.edgesByFrom.get(column) ?? [];
|
|
169
|
+
const edge = edges.find(
|
|
170
|
+
(e) => (e.direction === 'forward' || e.direction === 'shortcut') && e.dispatchOnly,
|
|
171
|
+
);
|
|
172
|
+
return edge?.to;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getBatchCommand(column: string): string | undefined {
|
|
176
|
+
const edges = this.edgesByFrom.get(column) ?? [];
|
|
177
|
+
const edge = edges.find(
|
|
178
|
+
(e) => (e.direction === 'forward' || e.direction === 'shortcut') && e.dispatchOnly,
|
|
179
|
+
);
|
|
180
|
+
if (!edge?.command) return undefined;
|
|
181
|
+
// Strip {id} placeholder — batch consumers handle substitution themselves
|
|
182
|
+
return edge.command.replace(' {id}', '').replace('{id}', '');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Event Inference ────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
inferStatus(
|
|
188
|
+
eventType: string,
|
|
189
|
+
currentStatus: string,
|
|
190
|
+
data: Record<string, unknown>,
|
|
191
|
+
): string | null | { dispatchResolution: true; resolution: string } {
|
|
192
|
+
const rules = (this.config.eventInference ?? []).filter((r) => r.eventType === eventType);
|
|
193
|
+
if (!rules.length) return null;
|
|
194
|
+
|
|
195
|
+
// Check rules with conditions first (more specific), then without
|
|
196
|
+
const withConditions = rules.filter((r) => r.conditions && Object.keys(r.conditions).length > 0);
|
|
197
|
+
const withoutConditions = rules.filter((r) => !r.conditions || Object.keys(r.conditions).length === 0);
|
|
198
|
+
|
|
199
|
+
const matchedRule = withConditions.find((r) => this.matchesConditions(r.conditions!, data))
|
|
200
|
+
?? withoutConditions[0]
|
|
201
|
+
?? null;
|
|
202
|
+
|
|
203
|
+
if (!matchedRule) return null;
|
|
204
|
+
|
|
205
|
+
// Handle dispatch resolution (e.g. AGENT_COMPLETED with outcome)
|
|
206
|
+
if (matchedRule.conditions?.dispatchResolution === true) {
|
|
207
|
+
const outcome = data.outcome as string;
|
|
208
|
+
return { dispatchResolution: true, resolution: outcome === 'failure' ? 'failed' : 'completed' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Determine target status
|
|
212
|
+
let newStatus: string;
|
|
213
|
+
if (matchedRule.targetStatus === '' && matchedRule.dataField) {
|
|
214
|
+
const rawValue = String(data[matchedRule.dataField] ?? '');
|
|
215
|
+
if (matchedRule.dataMap) {
|
|
216
|
+
newStatus = matchedRule.dataMap[rawValue] ?? matchedRule.dataMap['_default'] ?? '';
|
|
217
|
+
} else {
|
|
218
|
+
newStatus = rawValue;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
newStatus = matchedRule.targetStatus;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!newStatus) return null;
|
|
225
|
+
|
|
226
|
+
// Forward-only guard: event inference must not regress scope status
|
|
227
|
+
if (matchedRule.forwardOnly) {
|
|
228
|
+
const currentOrder = this.statusOrder.get(currentStatus) ?? -1;
|
|
229
|
+
const newOrder = this.statusOrder.get(newStatus) ?? -1;
|
|
230
|
+
if (newOrder <= currentOrder) return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return newStatus;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private matchesConditions(conditions: Record<string, unknown>, data: Record<string, unknown>): boolean {
|
|
237
|
+
for (const [key, expected] of Object.entries(conditions)) {
|
|
238
|
+
// dispatchResolution is a flag, not a data check
|
|
239
|
+
if (key === 'dispatchResolution') continue;
|
|
240
|
+
|
|
241
|
+
const actual = data[key];
|
|
242
|
+
if (Array.isArray(expected)) {
|
|
243
|
+
if (!expected.includes(actual)) return false;
|
|
244
|
+
} else {
|
|
245
|
+
if (actual !== expected) return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── Git / Lifecycle ────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
getListByGitBranch(branch: string): WorkflowList | undefined {
|
|
254
|
+
return this.config.lists.find((l) => l.gitBranch === branch);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getGitBranch(listId: string): string | undefined {
|
|
258
|
+
return this.listMap.get(listId)?.gitBranch;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getSessionKey(listId: string): string | undefined {
|
|
262
|
+
return this.listMap.get(listId)?.sessionKey;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
getActiveHooksForList(listId: string): string[] {
|
|
266
|
+
return this.listMap.get(listId)?.activeHooks ?? [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getAgentsForEdge(from: string, to: string): string[] {
|
|
270
|
+
return this.findEdge(from, to)?.agents ?? [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Status Order ───────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
getStatusOrder(status: string): number {
|
|
276
|
+
return this.statusOrder.get(status) ?? -1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
isForwardMovement(from: string, to: string): boolean {
|
|
280
|
+
return this.getStatusOrder(to) > this.getStatusOrder(from);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Hooks ──────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
getHooksForEdge(from: string, to: string): WorkflowHook[] {
|
|
286
|
+
const edge = this.findEdge(from, to);
|
|
287
|
+
if (!edge?.hooks?.length) return [];
|
|
288
|
+
return edge.hooks
|
|
289
|
+
.map((id) => this.hookMap.get(id))
|
|
290
|
+
.filter((h): h is WorkflowHook => h !== undefined);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getAllHooks(): readonly WorkflowHook[] {
|
|
294
|
+
return this.config.hooks ?? [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getHookEnforcement(hook: WorkflowHook): HookEnforcement {
|
|
298
|
+
return getHookEnforcement(hook);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getHooksByCategory(category: HookCategory): WorkflowHook[] {
|
|
302
|
+
return (this.config.hooks ?? []).filter((h) => h.category === category);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── Generation ─────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
generateCSSVariables(): string {
|
|
308
|
+
return this.getLists()
|
|
309
|
+
.map((l) => `--status-${l.id}: ${l.color};`)
|
|
310
|
+
.join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
generateShellManifest(): string {
|
|
314
|
+
const lines: string[] = [];
|
|
315
|
+
const sorted = this.getLists();
|
|
316
|
+
|
|
317
|
+
// Header
|
|
318
|
+
lines.push('#!/bin/bash');
|
|
319
|
+
lines.push('# Auto-generated by WorkflowEngine — DO NOT EDIT');
|
|
320
|
+
lines.push(`# Generated: ${new Date().toISOString()}`);
|
|
321
|
+
lines.push(`# Workflow: "${this.config.name}" (version ${this.config.version})`);
|
|
322
|
+
lines.push('');
|
|
323
|
+
|
|
324
|
+
// Branching mode
|
|
325
|
+
lines.push('# ─── Branching mode (trunk or worktree) ───');
|
|
326
|
+
lines.push(`WORKFLOW_BRANCHING_MODE="${this.getBranchingMode()}"`);
|
|
327
|
+
lines.push('');
|
|
328
|
+
|
|
329
|
+
// Valid statuses
|
|
330
|
+
lines.push('# ─── Valid statuses (space-separated) ───');
|
|
331
|
+
lines.push(`WORKFLOW_STATUSES="${sorted.map((l) => l.id).join(' ')}"`);
|
|
332
|
+
lines.push('');
|
|
333
|
+
|
|
334
|
+
// Directory statuses
|
|
335
|
+
lines.push('# ─── Statuses that have a scopes/ subdirectory ───');
|
|
336
|
+
const dirStatuses = sorted.filter((l) => l.hasDirectory).map((l) => l.id);
|
|
337
|
+
lines.push(`WORKFLOW_DIR_STATUSES="${dirStatuses.join(' ')}"`);
|
|
338
|
+
lines.push('');
|
|
339
|
+
|
|
340
|
+
// Terminal statuses
|
|
341
|
+
lines.push('# ─── Terminal statuses ───');
|
|
342
|
+
lines.push(`WORKFLOW_TERMINAL_STATUSES="${[...this.terminalStatuses].join(' ')}"`);
|
|
343
|
+
lines.push('');
|
|
344
|
+
|
|
345
|
+
// Entry point
|
|
346
|
+
lines.push('# ─── Entry point status ───');
|
|
347
|
+
lines.push(`WORKFLOW_ENTRY_STATUS="${this.getEntryPoint().id}"`);
|
|
348
|
+
lines.push('');
|
|
349
|
+
|
|
350
|
+
// Edges
|
|
351
|
+
lines.push('# ─── Transition edges (from:to:sessionKey) ───');
|
|
352
|
+
lines.push('WORKFLOW_EDGES=(');
|
|
353
|
+
for (const edge of this.config.edges) {
|
|
354
|
+
const targetList = this.listMap.get(edge.to);
|
|
355
|
+
const sessionKey = targetList?.sessionKey ?? '';
|
|
356
|
+
lines.push(` "${edge.from}:${edge.to}:${sessionKey}"`);
|
|
357
|
+
}
|
|
358
|
+
lines.push(')');
|
|
359
|
+
lines.push('');
|
|
360
|
+
|
|
361
|
+
// Branch map
|
|
362
|
+
lines.push('# ─── Branch-to-transition mapping (gitBranch:from:to:sessionKey) ───');
|
|
363
|
+
lines.push('WORKFLOW_BRANCH_MAP=(');
|
|
364
|
+
for (const edge of this.config.edges) {
|
|
365
|
+
const targetList = this.listMap.get(edge.to);
|
|
366
|
+
if (targetList?.gitBranch) {
|
|
367
|
+
const sessionKey = targetList.sessionKey ?? '';
|
|
368
|
+
lines.push(` "${targetList.gitBranch}:${edge.from}:${edge.to}:${sessionKey}"`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
lines.push(')');
|
|
372
|
+
lines.push('');
|
|
373
|
+
|
|
374
|
+
// Commit branch patterns
|
|
375
|
+
lines.push('# ─── Commit session branch patterns (regex) ───');
|
|
376
|
+
lines.push(`WORKFLOW_COMMIT_BRANCHES="${this.config.commitBranchPatterns ?? ''}"`);
|
|
377
|
+
lines.push('');
|
|
378
|
+
|
|
379
|
+
// Direction aliases (deployment edges: forward+dispatchOnly targeting deployment-group lists)
|
|
380
|
+
lines.push('# ─── Backward-compat direction aliases (alias:from:to:sessionKey) ───');
|
|
381
|
+
lines.push('WORKFLOW_DIRECTION_ALIASES=(');
|
|
382
|
+
for (const edge of this.config.edges) {
|
|
383
|
+
if (edge.direction !== 'forward' || !edge.dispatchOnly) continue;
|
|
384
|
+
const targetList = this.listMap.get(edge.to);
|
|
385
|
+
if (!targetList) continue;
|
|
386
|
+
// Generate aliases for deployment-group targets (dev, staging, production)
|
|
387
|
+
const group = targetList.group;
|
|
388
|
+
if (group === 'deployment') {
|
|
389
|
+
const sessionKey = targetList.sessionKey ?? '';
|
|
390
|
+
lines.push(` "to-${edge.to}:${edge.from}:${edge.to}:${sessionKey}"`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
lines.push(')');
|
|
394
|
+
lines.push('');
|
|
395
|
+
|
|
396
|
+
// Helper functions
|
|
397
|
+
lines.push('# ─── Helper functions ──────────────────────────────');
|
|
398
|
+
lines.push('');
|
|
399
|
+
lines.push('status_to_dir() {');
|
|
400
|
+
lines.push(' local scope_status="$1"');
|
|
401
|
+
lines.push(' for s in $WORKFLOW_DIR_STATUSES; do');
|
|
402
|
+
lines.push(' [ "$s" = "$scope_status" ] && echo "$scope_status" && return 0');
|
|
403
|
+
lines.push(' done');
|
|
404
|
+
lines.push(' echo "$WORKFLOW_ENTRY_STATUS"');
|
|
405
|
+
lines.push('}');
|
|
406
|
+
lines.push('');
|
|
407
|
+
lines.push('status_to_branch() {');
|
|
408
|
+
lines.push(' local status="$1"');
|
|
409
|
+
lines.push(' for entry in "${WORKFLOW_BRANCH_MAP[@]}"; do');
|
|
410
|
+
lines.push(' IFS=\':\' read -r branch from to skey <<< "$entry"');
|
|
411
|
+
lines.push(' [ "$to" = "$status" ] && echo "$branch" && return 0');
|
|
412
|
+
lines.push(' done');
|
|
413
|
+
lines.push(' echo ""');
|
|
414
|
+
lines.push('}');
|
|
415
|
+
lines.push('');
|
|
416
|
+
lines.push('is_valid_status() {');
|
|
417
|
+
lines.push(' local status="$1"');
|
|
418
|
+
lines.push(' for s in $WORKFLOW_STATUSES; do');
|
|
419
|
+
lines.push(' [ "$s" = "$status" ] && return 0');
|
|
420
|
+
lines.push(' done');
|
|
421
|
+
lines.push(' return 1');
|
|
422
|
+
lines.push('}');
|
|
423
|
+
|
|
424
|
+
return lines.join('\n') + '\n';
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
export default WorkflowEngine;
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { lazy, Suspense } from 'react';
|
|
2
|
+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
3
|
+
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
4
|
+
import { DashboardLayout } from '@/layouts/DashboardLayout';
|
|
5
|
+
import { ScopeBoard } from '@/views/ScopeBoard';
|
|
6
|
+
import { PrimitivesConfig } from '@/views/PrimitivesConfig';
|
|
7
|
+
import { QualityGates } from '@/views/QualityGates';
|
|
8
|
+
import { SourceControl } from '@/views/SourceControl';
|
|
9
|
+
import { SessionTimeline } from '@/views/SessionTimeline';
|
|
10
|
+
import { Settings } from '@/views/Settings';
|
|
11
|
+
|
|
12
|
+
const WorkflowVisualizer = lazy(() => import('@/views/WorkflowVisualizer'));
|
|
13
|
+
|
|
14
|
+
export default function App() {
|
|
15
|
+
return (
|
|
16
|
+
<BrowserRouter>
|
|
17
|
+
<TooltipProvider>
|
|
18
|
+
<Routes>
|
|
19
|
+
<Route element={<DashboardLayout />}>
|
|
20
|
+
<Route index element={<ScopeBoard />} />
|
|
21
|
+
<Route path="primitives" element={<PrimitivesConfig />} />
|
|
22
|
+
<Route path="gates" element={<QualityGates />} />
|
|
23
|
+
<Route path="enforcement" element={<Navigate to="/gates" replace />} />
|
|
24
|
+
<Route path="repo" element={<SourceControl />} />
|
|
25
|
+
<Route path="sessions" element={<SessionTimeline />} />
|
|
26
|
+
<Route path="workflow" element={<Suspense fallback={<div className="flex h-64 items-center justify-center"><div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" /></div>}><WorkflowVisualizer /></Suspense>} />
|
|
27
|
+
<Route path="settings" element={<Settings />} />
|
|
28
|
+
</Route>
|
|
29
|
+
</Routes>
|
|
30
|
+
</TooltipProvider>
|
|
31
|
+
</BrowserRouter>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import type { AgentName } from '@/types';
|
|
3
|
+
|
|
4
|
+
const AGENT_CONFIG: Record<string, { emoji: string; label: string; color: string; glow: string }> = {
|
|
5
|
+
'attacker': { emoji: '\u{1F5E1}\u{FE0F}', label: 'Attacker', color: 'bg-agent-attacker/20 text-agent-attacker', glow: 'agent-glow-attacker' },
|
|
6
|
+
'chaos': { emoji: '\u{1F4A5}', label: 'Chaos', color: 'bg-agent-chaos/20 text-agent-chaos', glow: 'agent-glow-chaos' },
|
|
7
|
+
'solana-expert': { emoji: '\u{26D3}\u{FE0F}', label: 'Solana', color: 'bg-agent-solana/20 text-agent-solana', glow: 'agent-glow-solana' },
|
|
8
|
+
'frontend-designer': { emoji: '\u{1F3A8}', label: 'Frontend', color: 'bg-agent-frontend/20 text-agent-frontend', glow: 'agent-glow-frontend' },
|
|
9
|
+
'architect': { emoji: '\u{1F3D7}\u{FE0F}', label: 'Architect', color: 'bg-agent-architect/20 text-agent-architect', glow: 'agent-glow-architect' },
|
|
10
|
+
'rules-enforcer': { emoji: '\u{1F4CB}', label: 'Rules', color: 'bg-agent-rules/20 text-agent-rules', glow: 'agent-glow-rules' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface AgentBadgeProps {
|
|
14
|
+
agent: string;
|
|
15
|
+
showLabel?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AgentBadge({ agent, showLabel = true, className }: AgentBadgeProps) {
|
|
20
|
+
const config = AGENT_CONFIG[agent as AgentName] ?? {
|
|
21
|
+
emoji: '\u{1F916}',
|
|
22
|
+
label: agent,
|
|
23
|
+
color: 'bg-muted text-muted-foreground',
|
|
24
|
+
glow: '',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<span
|
|
29
|
+
className={cn(
|
|
30
|
+
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xxs font-normal',
|
|
31
|
+
config.color,
|
|
32
|
+
config.glow,
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<span>{config.emoji}</span>
|
|
37
|
+
{showLabel && <span>{config.label}</span>}
|
|
38
|
+
</span>
|
|
39
|
+
);
|
|
40
|
+
}
|