@polymorphism-tech/morph-spec 7.2.0 → 7.4.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/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +4 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +29 -12
- package/framework/agents.json +132 -132
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-proposal.md +1 -1
- package/framework/evals/rubrics/README.md +9 -8
- package/framework/hooks/README.md +3 -1
- package/framework/hooks/claude-code/core/gate-guard.js +160 -0
- package/framework/hooks/claude-code/core/state-sync.js +15 -3
- package/framework/hooks/claude-code/core/validate-completion.js +241 -136
- package/framework/hooks/shared/state-reader.js +236 -221
- package/framework/phases.json +1 -1
- package/framework/skills/README.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +63 -21
- package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
- package/framework/skills/core/morph-eval/SKILL.md +4 -5
- package/framework/skills/core/morph-implement/SKILL.md +47 -19
- package/framework/skills/core/morph-plan/SKILL.md +8 -2
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +13 -6
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/setup.md +258 -244
- package/framework/templates/README.md +708 -709
- package/framework/templates/REGISTRY.json +1 -20
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/proposal.md +171 -171
- package/framework/templates/feature/recap.md +145 -145
- package/package.json +1 -1
- package/src/commands/project/doctor.js +941 -946
- package/src/commands/project/init.js +517 -515
- package/src/commands/project/status.js +373 -376
- package/src/commands/state/approve.js +251 -213
- package/src/commands/validation/validate-feature.js +356 -287
- package/src/core/state/state-manager.js +910 -934
- package/src/dashboard/app/App.jsx +29 -31
- package/src/dashboard/app/components/Sidebar.jsx +44 -45
- package/src/dashboard/scanner/index.js +20 -21
- package/src/lib/context/context-builder.js +3 -3
- package/src/lib/generators/context-generator.js +2 -3
- package/src/lib/generators/mandate-generator.js +48 -92
- package/src/lib/generators/metadata-extractor.js +385 -386
- package/src/lib/generators/recap-generator.js +285 -285
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/validators/content/content-validator.js +89 -286
- package/src/lib/validators/content/index.js +0 -1
- package/src/utils/hooks-installer.js +527 -516
- package/framework/templates/feature/tasks.md +0 -199
- package/framework/workflows/README.md +0 -1041
- package/framework/workflows/configs/fast-track.json +0 -46
- package/framework/workflows/configs/full.json +0 -64
- package/framework/workflows/configs/quick.json +0 -32
- package/framework/workflows/configs/standard.json +0 -57
- package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
- package/framework/workflows/docs/design-impl.md +0 -37
- package/framework/workflows/docs/enforcement-pipeline.md +0 -668
- package/framework/workflows/docs/fast-track.md +0 -29
- package/framework/workflows/docs/full-morph.md +0 -69
- package/framework/workflows/docs/standard.md +0 -49
- package/framework/workflows/docs/ui-refresh.md +0 -39
- package/src/commands/state/index.js +0 -7
- package/src/core/workflows/workflow-detector.js +0 -586
- package/src/core/workflows/workflow-router.js +0 -204
- package/src/dashboard/app/pages/Workflows.jsx +0 -346
- package/src/dashboard/scanner/workflows-scanner.js +0 -18
- package/src/lib/phase-chain/phase-validator.js +0 -359
- package/src/lib/validators/content/cross-phase-validator.js +0 -281
- package/src/lib/validators/spec-validator.js +0 -312
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Natural Language Workflow Router
|
|
3
|
-
*
|
|
4
|
-
* Classifies free-text input into morph-spec intents and routes to the
|
|
5
|
-
* appropriate command. Uses pattern matching for intent detection and
|
|
6
|
-
* delegates to detectWorkflow() for proposal classification.
|
|
7
|
-
*
|
|
8
|
-
* @module workflow-router
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { detectWorkflow } from './workflow-detector.js';
|
|
12
|
-
import { readFileSync, existsSync } from 'fs';
|
|
13
|
-
import { join } from 'path';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {Object} RoutedIntent
|
|
17
|
-
* @property {string} intent - 'proposal' | 'status' | 'approve' | 'fast-track'
|
|
18
|
-
* @property {string} command - The morph-spec command to execute
|
|
19
|
-
* @property {string[]} args - Arguments for the command
|
|
20
|
-
* @property {number} confidence - 0-1
|
|
21
|
-
* @property {string} explanation - Human-readable reason for routing decision
|
|
22
|
-
* @property {Object} [workflowDetection] - If intent is proposal/fast-track, includes detection result
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const INTENT_PATTERNS = [
|
|
26
|
-
// Status queries (highest priority)
|
|
27
|
-
{ pattern: /^(status|progress|where\s+am\s+i)\b/i, intent: 'status', confidence: 0.9 },
|
|
28
|
-
{ pattern: /what('s|\s+is)\s+(next|done|left|remaining)/i, intent: 'status', confidence: 0.85 },
|
|
29
|
-
{ pattern: /what\s+tasks/i, intent: 'status', confidence: 0.85 },
|
|
30
|
-
{ pattern: /tasks?\s+(left|remaining|pending|todo)/i, intent: 'status', confidence: 0.85 },
|
|
31
|
-
{ pattern: /^show\s+(me\s+)?status/i, intent: 'status', confidence: 0.9 },
|
|
32
|
-
|
|
33
|
-
// Approval actions
|
|
34
|
-
{ pattern: /^(approve|lgtm|looks?\s+good|ship\s+it)\b/i, intent: 'approve', confidence: 0.85 },
|
|
35
|
-
|
|
36
|
-
// Explicit simple/fix intents → fast-track proposal
|
|
37
|
-
{ pattern: /^(fix|typo|rename|toggle|bump|config)\b/i, intent: 'fast-track', confidence: 0.75 },
|
|
38
|
-
{ pattern: /\b(typo|spelling|label|icon|badge|tooltip|css\s+fix)\b/i, intent: 'fast-track', confidence: 0.7 },
|
|
39
|
-
|
|
40
|
-
// Everything else → proposal (further classified by detectWorkflow)
|
|
41
|
-
{ pattern: /.+/, intent: 'proposal', confidence: 0.5 },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Route natural language to the best morph-spec command.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} input - User's natural language description
|
|
48
|
-
* @param {Object} [options]
|
|
49
|
-
* @param {string} [options.projectPath='.'] - Project path for codebase analysis
|
|
50
|
-
* @param {string} [options.activeFeature] - Currently active feature name (for status/approve)
|
|
51
|
-
* @returns {Promise<RoutedIntent>}
|
|
52
|
-
*/
|
|
53
|
-
export async function routeIntent(input, options = {}) {
|
|
54
|
-
const { projectPath = '.' } = options;
|
|
55
|
-
const trimmed = (input || '').trim();
|
|
56
|
-
|
|
57
|
-
if (!trimmed) {
|
|
58
|
-
return {
|
|
59
|
-
intent: 'proposal',
|
|
60
|
-
command: 'morph-spec',
|
|
61
|
-
args: ['--help'],
|
|
62
|
-
confidence: 0,
|
|
63
|
-
explanation: 'Empty input — showing help'
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Match against patterns in order
|
|
68
|
-
let matchedIntent = null;
|
|
69
|
-
for (const { pattern, intent, confidence } of INTENT_PATTERNS) {
|
|
70
|
-
if (pattern.test(trimmed)) {
|
|
71
|
-
matchedIntent = { intent, confidence };
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const activeFeature = options.activeFeature || findActiveFeature(projectPath);
|
|
77
|
-
|
|
78
|
-
// Route based on intent
|
|
79
|
-
switch (matchedIntent.intent) {
|
|
80
|
-
case 'status': {
|
|
81
|
-
const feature = activeFeature || '<feature>';
|
|
82
|
-
return {
|
|
83
|
-
intent: 'status',
|
|
84
|
-
command: 'morph-spec',
|
|
85
|
-
args: ['status', feature],
|
|
86
|
-
confidence: activeFeature ? matchedIntent.confidence : matchedIntent.confidence * 0.6,
|
|
87
|
-
explanation: activeFeature
|
|
88
|
-
? `Status query for active feature "${feature}"`
|
|
89
|
-
: 'Status query — no active feature found'
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
case 'approve': {
|
|
94
|
-
const feature = activeFeature || '<feature>';
|
|
95
|
-
return {
|
|
96
|
-
intent: 'approve',
|
|
97
|
-
command: 'morph-spec',
|
|
98
|
-
args: ['approve', feature],
|
|
99
|
-
confidence: activeFeature ? matchedIntent.confidence : matchedIntent.confidence * 0.5,
|
|
100
|
-
explanation: activeFeature
|
|
101
|
-
? `Approve active feature "${feature}"`
|
|
102
|
-
: 'Approve — no active feature found'
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
case 'fast-track': {
|
|
107
|
-
// Validate with detectWorkflow — regex might be too eager
|
|
108
|
-
const detection = await detectWorkflow({
|
|
109
|
-
userRequest: trimmed,
|
|
110
|
-
projectPath,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// If detectWorkflow disagrees, defer to it
|
|
114
|
-
if (detection.workflowId !== 'fast-track' && detection.confidence > matchedIntent.confidence) {
|
|
115
|
-
const slug = slugify(trimmed);
|
|
116
|
-
return {
|
|
117
|
-
intent: 'proposal',
|
|
118
|
-
command: 'morph-spec',
|
|
119
|
-
args: ['state', 'init', slug, '--user-request', trimmed],
|
|
120
|
-
confidence: detection.confidence,
|
|
121
|
-
explanation: `Detected as ${detection.workflowId} (${detection.reasoning})`,
|
|
122
|
-
workflowDetection: detection
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const slug = slugify(trimmed);
|
|
127
|
-
return {
|
|
128
|
-
intent: 'fast-track',
|
|
129
|
-
command: 'morph-spec',
|
|
130
|
-
args: ['state', 'init', slug, '--workflow', 'fast-track', '--user-request', trimmed],
|
|
131
|
-
confidence: Math.max(matchedIntent.confidence, detection.confidence),
|
|
132
|
-
explanation: `Simple task → fast-track workflow (${detection.reasoning})`,
|
|
133
|
-
workflowDetection: detection
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
case 'proposal':
|
|
138
|
-
default: {
|
|
139
|
-
const detection = await detectWorkflow({
|
|
140
|
-
userRequest: trimmed,
|
|
141
|
-
projectPath,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const slug = slugify(trimmed);
|
|
145
|
-
const args = ['state', 'init', slug, '--user-request', trimmed];
|
|
146
|
-
if (detection.workflowId !== 'auto') {
|
|
147
|
-
args.push('--workflow', detection.workflowId);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
intent: 'proposal',
|
|
152
|
-
command: 'morph-spec',
|
|
153
|
-
args,
|
|
154
|
-
confidence: detection.confidence,
|
|
155
|
-
explanation: `New feature → ${detection.workflowId} workflow (${detection.reasoning})`,
|
|
156
|
-
workflowDetection: detection
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Find the currently active feature from state.json.
|
|
164
|
-
* Returns the most recently updated feature.
|
|
165
|
-
*
|
|
166
|
-
* @param {string} [projectPath='.']
|
|
167
|
-
* @returns {string|null} Feature name or null
|
|
168
|
-
*/
|
|
169
|
-
export function findActiveFeature(projectPath = '.') {
|
|
170
|
-
try {
|
|
171
|
-
const statePath = join(projectPath, '.morph', 'state.json');
|
|
172
|
-
if (!existsSync(statePath)) return null;
|
|
173
|
-
|
|
174
|
-
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
175
|
-
if (!state?.features) return null;
|
|
176
|
-
|
|
177
|
-
const entries = Object.entries(state.features)
|
|
178
|
-
.sort((a, b) => {
|
|
179
|
-
const dateA = a[1].updatedAt || a[1].createdAt || '';
|
|
180
|
-
const dateB = b[1].updatedAt || b[1].createdAt || '';
|
|
181
|
-
return dateB.localeCompare(dateA);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
return entries.length > 0 ? entries[0][0] : null;
|
|
185
|
-
} catch {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Convert a description into a feature slug.
|
|
192
|
-
* @param {string} text
|
|
193
|
-
* @returns {string}
|
|
194
|
-
*/
|
|
195
|
-
function slugify(text) {
|
|
196
|
-
return text
|
|
197
|
-
.toLowerCase()
|
|
198
|
-
.replace(/[^a-z0-9\s-]/g, '')
|
|
199
|
-
.trim()
|
|
200
|
-
.replace(/\s+/g, '-')
|
|
201
|
-
.replace(/-+/g, '-')
|
|
202
|
-
.slice(0, 50)
|
|
203
|
-
.replace(/-$/, '');
|
|
204
|
-
}
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
2
|
-
import { Handle, Position } from '@xyflow/react';
|
|
3
|
-
import morphData from 'virtual:morph-data';
|
|
4
|
-
import FlowGraph from '../components/FlowGraph.jsx';
|
|
5
|
-
import DetailPanel from '../components/DetailPanel.jsx';
|
|
6
|
-
import { useDetailPanel } from '../hooks/useDetailPanel.js';
|
|
7
|
-
|
|
8
|
-
// ─── Hover Context ────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
const HoverContext = createContext({ hoveredPhase: null, setHoveredPhase: () => {} });
|
|
11
|
-
|
|
12
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
// Canonical phase order (we render exactly these 5 columns)
|
|
15
|
-
const PHASE_ORDER = ['proposal', 'uiux', 'plan', 'implement', 'review'];
|
|
16
|
-
|
|
17
|
-
const PHASE_X_GAP = 200;
|
|
18
|
-
const PHASE_W = 160;
|
|
19
|
-
const ROW_H = 130;
|
|
20
|
-
const LABEL_W = 160;
|
|
21
|
-
const LABEL_X = -LABEL_W - 20;
|
|
22
|
-
const PHASE_START_X = 0;
|
|
23
|
-
|
|
24
|
-
// ─── Node Components ──────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
function WorkflowLabelNode({ data }) {
|
|
27
|
-
return (
|
|
28
|
-
<div
|
|
29
|
-
className="rounded-lg px-4 py-3 cursor-pointer select-none"
|
|
30
|
-
style={{
|
|
31
|
-
width: LABEL_W,
|
|
32
|
-
background: '#0f172a',
|
|
33
|
-
border: '1.5px solid #334155',
|
|
34
|
-
boxShadow: '0 0 8px #33415533',
|
|
35
|
-
}}
|
|
36
|
-
>
|
|
37
|
-
<Handle type="source" position={Position.Right} style={{ background: '#475569', width: 7, height: 7 }} />
|
|
38
|
-
<div className="text-white text-xs font-bold leading-tight truncate" title={data.label}>
|
|
39
|
-
{data.label}
|
|
40
|
-
</div>
|
|
41
|
-
<div className="text-gray-400 text-[10px] mt-1 leading-snug line-clamp-2" title={data.description}>
|
|
42
|
-
{data.description}
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function WorkflowPhaseNode({ data }) {
|
|
49
|
-
const { hoveredPhase, setHoveredPhase } = useContext(HoverContext);
|
|
50
|
-
const isSkipped = data.skipped;
|
|
51
|
-
const phaseName = data.phase;
|
|
52
|
-
|
|
53
|
-
const isHighlighted = hoveredPhase !== null && hoveredPhase === phaseName;
|
|
54
|
-
const isDimmed = hoveredPhase !== null && hoveredPhase !== phaseName;
|
|
55
|
-
|
|
56
|
-
const bg = isSkipped ? '#111827' : '#1e3a5f';
|
|
57
|
-
const border = isSkipped ? '#374151' : isHighlighted ? '#60a5fa' : '#3b82f6';
|
|
58
|
-
const textColor = isSkipped ? '#6b7280' : '#93c5fd';
|
|
59
|
-
|
|
60
|
-
let opacity = isSkipped ? 0.5 : 1;
|
|
61
|
-
if (isDimmed) opacity = isSkipped ? 0.25 : 0.35;
|
|
62
|
-
|
|
63
|
-
const boxShadow = isHighlighted
|
|
64
|
-
? '0 0 0 2px #60a5fa, 0 0 12px #3b82f6aa'
|
|
65
|
-
: isSkipped
|
|
66
|
-
? 'none'
|
|
67
|
-
: '0 0 6px #3b82f622';
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div
|
|
71
|
-
className="rounded-lg px-3 py-2 select-none cursor-default"
|
|
72
|
-
style={{
|
|
73
|
-
width: PHASE_W - 20,
|
|
74
|
-
background: isHighlighted ? '#1e4a7f' : bg,
|
|
75
|
-
border: `1.5px ${isSkipped ? 'dashed' : 'solid'} ${border}`,
|
|
76
|
-
boxShadow,
|
|
77
|
-
opacity,
|
|
78
|
-
transition: 'opacity 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, border-color 0.15s ease',
|
|
79
|
-
}}
|
|
80
|
-
onMouseEnter={() => setHoveredPhase(phaseName)}
|
|
81
|
-
onMouseLeave={() => setHoveredPhase(null)}
|
|
82
|
-
>
|
|
83
|
-
<Handle type="target" position={Position.Left} style={{ background: border, width: 6, height: 6 }} />
|
|
84
|
-
<Handle type="source" position={Position.Right} style={{ background: border, width: 6, height: 6 }} />
|
|
85
|
-
<div
|
|
86
|
-
className="text-xs font-semibold leading-tight"
|
|
87
|
-
style={{
|
|
88
|
-
color: isSkipped ? '#6b7280' : isHighlighted ? '#bfdbfe' : '#ffffff',
|
|
89
|
-
textDecoration: isSkipped ? 'line-through' : 'none',
|
|
90
|
-
}}
|
|
91
|
-
>
|
|
92
|
-
{data.label}
|
|
93
|
-
</div>
|
|
94
|
-
{isSkipped && (
|
|
95
|
-
<div className="text-[9px] mt-0.5" style={{ color: '#6b7280' }}>skip</div>
|
|
96
|
-
)}
|
|
97
|
-
{!isSkipped && (
|
|
98
|
-
<div className="text-[9px] mt-0.5" style={{ color: textColor }}>run</div>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const nodeTypes = { workflowLabelNode: WorkflowLabelNode, workflowPhaseNode: WorkflowPhaseNode };
|
|
105
|
-
|
|
106
|
-
// ─── Graph Builder ────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
function buildGraph(workflows) {
|
|
109
|
-
const nodes = [];
|
|
110
|
-
const edges = [];
|
|
111
|
-
|
|
112
|
-
// Sort workflows by priority (ascending: lower priority = simpler = shown first)
|
|
113
|
-
const sorted = [...workflows].sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
114
|
-
|
|
115
|
-
sorted.forEach((wf, rowIdx) => {
|
|
116
|
-
const rowY = rowIdx * ROW_H;
|
|
117
|
-
const skipSet = new Set(wf.phases.skip || []);
|
|
118
|
-
|
|
119
|
-
// Label node on the left
|
|
120
|
-
nodes.push({
|
|
121
|
-
id: `wf-label-${wf.id}`,
|
|
122
|
-
type: 'workflowLabelNode',
|
|
123
|
-
position: { x: LABEL_X, y: rowY + 10 },
|
|
124
|
-
data: {
|
|
125
|
-
label: wf.name,
|
|
126
|
-
description: wf.description,
|
|
127
|
-
wfRef: wf,
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Phase nodes
|
|
132
|
-
PHASE_ORDER.forEach((phase, colIdx) => {
|
|
133
|
-
const isSkipped = skipSet.has(phase);
|
|
134
|
-
const phaseNodeId = `wf-${wf.id}-phase-${phase}`;
|
|
135
|
-
|
|
136
|
-
nodes.push({
|
|
137
|
-
id: phaseNodeId,
|
|
138
|
-
type: 'workflowPhaseNode',
|
|
139
|
-
position: { x: PHASE_START_X + colIdx * PHASE_X_GAP, y: rowY },
|
|
140
|
-
data: {
|
|
141
|
-
label: phase,
|
|
142
|
-
skipped: isSkipped,
|
|
143
|
-
active: !isSkipped,
|
|
144
|
-
wfId: wf.id,
|
|
145
|
-
phase,
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Edge to next phase in this row
|
|
150
|
-
const nextPhase = PHASE_ORDER[colIdx + 1];
|
|
151
|
-
if (nextPhase) {
|
|
152
|
-
const nextNodeId = `wf-${wf.id}-phase-${nextPhase}`;
|
|
153
|
-
const isNextSkipped = skipSet.has(nextPhase);
|
|
154
|
-
edges.push({
|
|
155
|
-
id: `edge-${wf.id}-${phase}-${nextPhase}`,
|
|
156
|
-
source: phaseNodeId,
|
|
157
|
-
target: nextNodeId,
|
|
158
|
-
type: 'smoothstep',
|
|
159
|
-
style: {
|
|
160
|
-
stroke: (isSkipped || isNextSkipped) ? '#374151' : '#3b82f6',
|
|
161
|
-
strokeWidth: 1.2,
|
|
162
|
-
strokeDasharray: (isSkipped || isNextSkipped) ? '4 3' : undefined,
|
|
163
|
-
opacity: (isSkipped || isNextSkipped) ? 0.3 : 0.7,
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return { nodes, edges };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ─── Detail Content ───────────────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
function WorkflowDetail({ wf }) {
|
|
176
|
-
return (
|
|
177
|
-
<div className="text-sm text-gray-200 space-y-4">
|
|
178
|
-
<p className="text-gray-300 text-xs leading-relaxed">{wf.description}</p>
|
|
179
|
-
|
|
180
|
-
<div className="flex gap-2">
|
|
181
|
-
<span className="px-2 py-0.5 bg-blue-900/50 border border-blue-700 rounded text-xs text-blue-300">
|
|
182
|
-
priority {wf.priority}
|
|
183
|
-
</span>
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
{/* Phases */}
|
|
187
|
-
<div>
|
|
188
|
-
<p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Run Phases</p>
|
|
189
|
-
<div className="flex flex-wrap gap-1">
|
|
190
|
-
{(wf.phases.run || []).map((p) => (
|
|
191
|
-
<span key={p} className="px-2 py-0.5 bg-blue-900/50 border border-blue-700 rounded text-xs text-blue-300 font-mono">{p}</span>
|
|
192
|
-
))}
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
{wf.phases.skip?.length > 0 && (
|
|
197
|
-
<div>
|
|
198
|
-
<p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Skip Phases</p>
|
|
199
|
-
<div className="flex flex-wrap gap-1">
|
|
200
|
-
{wf.phases.skip.map((p) => (
|
|
201
|
-
<span key={p} className="px-2 py-0.5 bg-gray-800 border border-dashed border-gray-600 rounded text-xs text-gray-500 font-mono line-through">{p}</span>
|
|
202
|
-
))}
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
)}
|
|
206
|
-
|
|
207
|
-
{/* Pause points */}
|
|
208
|
-
{wf.pausePoints?.length > 0 && (
|
|
209
|
-
<div>
|
|
210
|
-
<p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Pause Points</p>
|
|
211
|
-
<div className="space-y-1.5">
|
|
212
|
-
{wf.pausePoints.map((pp, i) => (
|
|
213
|
-
<div key={i} className="px-2 py-1.5 bg-yellow-900/20 border border-yellow-700/40 rounded text-xs">
|
|
214
|
-
<div className="flex items-center gap-2 mb-0.5">
|
|
215
|
-
<span className="text-yellow-400 font-mono text-[10px]">{pp.phase}</span>
|
|
216
|
-
{pp.condition && (
|
|
217
|
-
<span className="text-gray-500 text-[10px]">if: {pp.condition}</span>
|
|
218
|
-
)}
|
|
219
|
-
</div>
|
|
220
|
-
<p className="text-yellow-200">{pp.question}</p>
|
|
221
|
-
</div>
|
|
222
|
-
))}
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
)}
|
|
226
|
-
|
|
227
|
-
{/* Keywords */}
|
|
228
|
-
{wf.keywords?.length > 0 && (
|
|
229
|
-
<div>
|
|
230
|
-
<p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Keywords</p>
|
|
231
|
-
<div className="flex flex-wrap gap-1">
|
|
232
|
-
{wf.keywords.map((k) => (
|
|
233
|
-
<span key={k} className="px-1.5 py-0.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-400">{k}</span>
|
|
234
|
-
))}
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
)}
|
|
238
|
-
|
|
239
|
-
{/* Outputs */}
|
|
240
|
-
{wf.outputs?.required?.length > 0 && (
|
|
241
|
-
<div>
|
|
242
|
-
<p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Required Outputs</p>
|
|
243
|
-
<div className="flex flex-wrap gap-1">
|
|
244
|
-
{wf.outputs.required.map((o) => (
|
|
245
|
-
<span key={o} className="px-2 py-0.5 bg-green-900/50 border border-green-700 rounded text-xs text-green-300 font-mono">{o}</span>
|
|
246
|
-
))}
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
|
|
251
|
-
{wf.outputs?.optional?.length > 0 && (
|
|
252
|
-
<div>
|
|
253
|
-
<p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Optional Outputs</p>
|
|
254
|
-
<div className="flex flex-wrap gap-1">
|
|
255
|
-
{wf.outputs.optional.map((o) => (
|
|
256
|
-
<span key={o} className="px-2 py-0.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-400 font-mono">{o}</span>
|
|
257
|
-
))}
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
|
-
{/* Criteria */}
|
|
263
|
-
{wf.criteria && Object.keys(wf.criteria).length > 0 && (
|
|
264
|
-
<div>
|
|
265
|
-
<p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Criteria</p>
|
|
266
|
-
<pre className="p-2 bg-gray-950 border border-gray-800 rounded text-[9px] font-mono text-gray-400 overflow-x-auto">
|
|
267
|
-
{JSON.stringify(wf.criteria, null, 2)}
|
|
268
|
-
</pre>
|
|
269
|
-
</div>
|
|
270
|
-
)}
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ─── Main Page ────────────────────────────────────────────────────────────────
|
|
276
|
-
|
|
277
|
-
export default function Workflows() {
|
|
278
|
-
const { workflows } = morphData;
|
|
279
|
-
const { panel, openPanel, closePanel } = useDetailPanel();
|
|
280
|
-
const [hoveredPhase, setHoveredPhase] = useState(null);
|
|
281
|
-
|
|
282
|
-
const { nodes, edges } = useMemo(() => buildGraph(workflows), [workflows]);
|
|
283
|
-
|
|
284
|
-
const handleNodeClick = useCallback(
|
|
285
|
-
(_event, node) => {
|
|
286
|
-
if (node.type === 'workflowLabelNode') {
|
|
287
|
-
const wf = node.data.wfRef;
|
|
288
|
-
if (wf) openPanel(wf.name, <WorkflowDetail wf={wf} />);
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
[openPanel]
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
const hoverContextValue = useMemo(
|
|
295
|
-
() => ({ hoveredPhase, setHoveredPhase }),
|
|
296
|
-
[hoveredPhase]
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
<div className="flex flex-col h-full">
|
|
301
|
-
{/* Header */}
|
|
302
|
-
<div className="px-6 py-4 border-b border-gray-800 flex-shrink-0">
|
|
303
|
-
<h1 className="text-white text-xl font-bold">Workflow Configs</h1>
|
|
304
|
-
<p className="text-gray-400 text-sm mt-0.5">
|
|
305
|
-
{workflows.length} workflow{workflows.length !== 1 ? 's' : ''} — each row shows 7 phases, grayed out phases are skipped. Click a workflow label to inspect.
|
|
306
|
-
</p>
|
|
307
|
-
</div>
|
|
308
|
-
|
|
309
|
-
<div className="flex-1 relative">
|
|
310
|
-
<HoverContext.Provider value={hoverContextValue}>
|
|
311
|
-
<FlowGraph
|
|
312
|
-
nodes={nodes}
|
|
313
|
-
edges={edges}
|
|
314
|
-
nodeTypes={nodeTypes}
|
|
315
|
-
onNodeClick={handleNodeClick}
|
|
316
|
-
fitView
|
|
317
|
-
/>
|
|
318
|
-
</HoverContext.Provider>
|
|
319
|
-
|
|
320
|
-
<div className="absolute top-3 left-3 bg-gray-900/90 border border-gray-700 rounded-lg p-3 text-xs space-y-2 z-10">
|
|
321
|
-
<p className="text-gray-400 font-semibold uppercase tracking-wide text-[10px] mb-2">Legend</p>
|
|
322
|
-
<div className="flex items-center gap-2">
|
|
323
|
-
<span className="inline-block w-10 h-4 rounded-sm" style={{ background: '#1e3a5f', border: '1.5px solid #3b82f6' }} />
|
|
324
|
-
<span className="text-gray-300">Active phase</span>
|
|
325
|
-
</div>
|
|
326
|
-
<div className="flex items-center gap-2">
|
|
327
|
-
<span className="inline-block w-10 h-4 rounded-sm opacity-50" style={{ background: '#111827', border: '1.5px dashed #374151' }} />
|
|
328
|
-
<span className="text-gray-300">Skipped phase</span>
|
|
329
|
-
</div>
|
|
330
|
-
<div className="flex items-center gap-2">
|
|
331
|
-
<span className="inline-block w-10 h-4 rounded-sm" style={{ background: '#0f172a', border: '1.5px solid #334155' }} />
|
|
332
|
-
<span className="text-gray-300">Workflow label</span>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
|
|
336
|
-
<div className="absolute top-3 right-3 bg-gray-900/90 border border-gray-700 rounded-lg px-3 py-1.5 text-xs text-gray-400 z-10">
|
|
337
|
-
{workflows.length} workflows · 7 phases
|
|
338
|
-
</div>
|
|
339
|
-
</div>
|
|
340
|
-
|
|
341
|
-
<DetailPanel open={panel.open} title={panel.title} onClose={closePanel}>
|
|
342
|
-
{panel.content}
|
|
343
|
-
</DetailPanel>
|
|
344
|
-
</div>
|
|
345
|
-
);
|
|
346
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
export function scanWorkflows(frameworkDir) {
|
|
5
|
-
const dir = join(frameworkDir, 'workflows', 'configs');
|
|
6
|
-
if (!existsSync(dir)) return [];
|
|
7
|
-
return readdirSync(dir).filter(f => f.endsWith('.json')).map(f => {
|
|
8
|
-
const d = JSON.parse(readFileSync(join(dir, f), 'utf8'));
|
|
9
|
-
return {
|
|
10
|
-
id: d.id, name: d.name, description: d.description || '',
|
|
11
|
-
criteria: d.criteria || {}, keywords: d.keywords || [],
|
|
12
|
-
phases: d.phases || { run: [], skip: [] },
|
|
13
|
-
pausePoints: d.pausePoints || [],
|
|
14
|
-
outputs: d.outputs || { required: [], optional: [], skip: [] },
|
|
15
|
-
priority: d.priority || 0,
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
}
|