@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.
Files changed (74) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +4 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +2 -2
  8. package/framework/MORPH.md +29 -12
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-proposal.md +1 -1
  12. package/framework/evals/rubrics/README.md +9 -8
  13. package/framework/hooks/README.md +3 -1
  14. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  15. package/framework/hooks/claude-code/core/state-sync.js +15 -3
  16. package/framework/hooks/claude-code/core/validate-completion.js +241 -136
  17. package/framework/hooks/shared/state-reader.js +236 -221
  18. package/framework/phases.json +1 -1
  19. package/framework/skills/README.md +2 -2
  20. package/framework/skills/core/morph-apply/SKILL.md +63 -21
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
  22. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  23. package/framework/skills/core/morph-implement/SKILL.md +47 -19
  24. package/framework/skills/core/morph-plan/SKILL.md +8 -2
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +13 -6
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/standards/ai-agents/setup.md +258 -244
  30. package/framework/templates/README.md +708 -709
  31. package/framework/templates/REGISTRY.json +1 -20
  32. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  33. package/framework/templates/docs/proposal.md +171 -171
  34. package/framework/templates/feature/recap.md +145 -145
  35. package/package.json +1 -1
  36. package/src/commands/project/doctor.js +941 -946
  37. package/src/commands/project/init.js +517 -515
  38. package/src/commands/project/status.js +373 -376
  39. package/src/commands/state/approve.js +251 -213
  40. package/src/commands/validation/validate-feature.js +356 -287
  41. package/src/core/state/state-manager.js +910 -934
  42. package/src/dashboard/app/App.jsx +29 -31
  43. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  44. package/src/dashboard/scanner/index.js +20 -21
  45. package/src/lib/context/context-builder.js +3 -3
  46. package/src/lib/generators/context-generator.js +2 -3
  47. package/src/lib/generators/mandate-generator.js +48 -92
  48. package/src/lib/generators/metadata-extractor.js +385 -386
  49. package/src/lib/generators/recap-generator.js +285 -285
  50. package/src/lib/risk-detector.js +2 -2
  51. package/src/lib/validators/content/content-validator.js +89 -286
  52. package/src/lib/validators/content/index.js +0 -1
  53. package/src/utils/hooks-installer.js +527 -516
  54. package/framework/templates/feature/tasks.md +0 -199
  55. package/framework/workflows/README.md +0 -1041
  56. package/framework/workflows/configs/fast-track.json +0 -46
  57. package/framework/workflows/configs/full.json +0 -64
  58. package/framework/workflows/configs/quick.json +0 -32
  59. package/framework/workflows/configs/standard.json +0 -57
  60. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  61. package/framework/workflows/docs/design-impl.md +0 -37
  62. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  63. package/framework/workflows/docs/fast-track.md +0 -29
  64. package/framework/workflows/docs/full-morph.md +0 -69
  65. package/framework/workflows/docs/standard.md +0 -49
  66. package/framework/workflows/docs/ui-refresh.md +0 -39
  67. package/src/commands/state/index.js +0 -7
  68. package/src/core/workflows/workflow-detector.js +0 -586
  69. package/src/core/workflows/workflow-router.js +0 -204
  70. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  71. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  72. package/src/lib/phase-chain/phase-validator.js +0 -359
  73. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  74. 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
- }