circuit_breaker-wf 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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +52 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +116 -0
  6. data/LICENSE +21 -0
  7. data/README.md +324 -0
  8. data/examples/document/README.md +150 -0
  9. data/examples/document/document_assistant.rb +535 -0
  10. data/examples/document/document_rules.rb +60 -0
  11. data/examples/document/document_token.rb +83 -0
  12. data/examples/document/document_workflow.rb +114 -0
  13. data/examples/document/mock_executor.rb +80 -0
  14. data/lib/circuit_breaker/executors/README.md +664 -0
  15. data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
  16. data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
  17. data/lib/circuit_breaker/executors/base_executor.rb +56 -0
  18. data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
  19. data/lib/circuit_breaker/executors/dsl.rb +97 -0
  20. data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
  21. data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
  22. data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
  23. data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
  24. data/lib/circuit_breaker/executors/step_executor.rb +47 -0
  25. data/lib/circuit_breaker/history.rb +81 -0
  26. data/lib/circuit_breaker/rules.rb +251 -0
  27. data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
  28. data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
  29. data/lib/circuit_breaker/token.rb +486 -0
  30. data/lib/circuit_breaker/visualizer.rb +173 -0
  31. data/lib/circuit_breaker/workflow_dsl.rb +359 -0
  32. data/lib/circuit_breaker.rb +236 -0
  33. data/workflow-editor/.gitignore +24 -0
  34. data/workflow-editor/README.md +106 -0
  35. data/workflow-editor/eslint.config.js +28 -0
  36. data/workflow-editor/index.html +13 -0
  37. data/workflow-editor/package-lock.json +6864 -0
  38. data/workflow-editor/package.json +50 -0
  39. data/workflow-editor/postcss.config.js +6 -0
  40. data/workflow-editor/public/vite.svg +1 -0
  41. data/workflow-editor/src/App.css +42 -0
  42. data/workflow-editor/src/App.tsx +365 -0
  43. data/workflow-editor/src/assets/react.svg +1 -0
  44. data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
  45. data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
  46. data/workflow-editor/src/components/NodeDetails.tsx +177 -0
  47. data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
  48. data/workflow-editor/src/components/SaveButton.tsx +45 -0
  49. data/workflow-editor/src/config/change_workflow.yaml +59 -0
  50. data/workflow-editor/src/config/constants.ts +11 -0
  51. data/workflow-editor/src/config/flowConfig.ts +189 -0
  52. data/workflow-editor/src/config/uiConfig.ts +77 -0
  53. data/workflow-editor/src/config/workflow.yaml +58 -0
  54. data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
  55. data/workflow-editor/src/index.css +34 -0
  56. data/workflow-editor/src/main.tsx +10 -0
  57. data/workflow-editor/src/server/saveWorkflow.ts +81 -0
  58. data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
  59. data/workflow-editor/src/utils/workflowLoader.ts +26 -0
  60. data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
  61. data/workflow-editor/src/vite-env.d.ts +1 -0
  62. data/workflow-editor/src/yaml.d.ts +4 -0
  63. data/workflow-editor/tailwind.config.js +15 -0
  64. data/workflow-editor/tsconfig.app.json +26 -0
  65. data/workflow-editor/tsconfig.json +7 -0
  66. data/workflow-editor/tsconfig.node.json +24 -0
  67. data/workflow-editor/vite.config.ts +8 -0
  68. metadata +267 -0
@@ -0,0 +1,175 @@
1
+ import { Edge, useNodes } from 'reactflow';
2
+ import { Card } from 'flowbite-react';
3
+ import { useState, useEffect } from 'react';
4
+
5
+ interface EdgeDetailsProps {
6
+ edge: Edge | null;
7
+ onChange: (changes: any[]) => void;
8
+ onSave: () => Promise<boolean>;
9
+ }
10
+
11
+ export const EdgeDetails = ({ edge, onChange, onSave }: EdgeDetailsProps) => {
12
+ const nodes = useNodes();
13
+ const [label, setLabel] = useState(edge?.startLabel || '');
14
+ const [requirements, setRequirements] = useState<string[]>(edge?.data?.requirements || []);
15
+ const [newRequirement, setNewRequirement] = useState('');
16
+ const [isSaving, setIsSaving] = useState(false);
17
+ const [saveMessage, setSaveMessage] = useState('');
18
+
19
+ // Update label and requirements state when edge changes
20
+ useEffect(() => {
21
+ setLabel(edge?.startLabel || '');
22
+ setRequirements(edge?.data?.requirements || []);
23
+ }, [edge]);
24
+
25
+ if (!edge) {
26
+ return (
27
+ <div className="p-6 text-center text-gray-500">
28
+ <p className="text-sm">Select a transition to view details</p>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ const sourceNode = nodes.find(n => n.id === edge.source);
34
+ const targetNode = nodes.find(n => n.id === edge.target);
35
+
36
+ const handleLabelChange = (newLabel: string) => {
37
+ setLabel(newLabel);
38
+ onChange([{
39
+ id: edge.id,
40
+ startLabel: newLabel,
41
+ data: {
42
+ ...edge.data,
43
+ label: newLabel,
44
+ requirements
45
+ }
46
+ }]);
47
+ };
48
+
49
+ const handleAddRequirement = () => {
50
+ if (!newRequirement.trim()) return;
51
+
52
+ const updatedRequirements = [...requirements, newRequirement.trim()];
53
+ setRequirements(updatedRequirements);
54
+ setNewRequirement('');
55
+
56
+ onChange([{
57
+ id: edge.id,
58
+ data: {
59
+ ...edge.data,
60
+ label,
61
+ requirements: updatedRequirements
62
+ }
63
+ }]);
64
+ };
65
+
66
+ const handleRemoveRequirement = (index: number) => {
67
+ const updatedRequirements = requirements.filter((_, i) => i !== index);
68
+ setRequirements(updatedRequirements);
69
+
70
+ onChange([{
71
+ id: edge.id,
72
+ data: {
73
+ ...edge.data,
74
+ label,
75
+ requirements: updatedRequirements
76
+ }
77
+ }]);
78
+ };
79
+
80
+ const handleSave = async () => {
81
+ setIsSaving(true);
82
+ setSaveMessage('');
83
+ try {
84
+ const success = await onSave();
85
+ setSaveMessage(success ? 'Changes saved!' : 'Failed to save changes');
86
+ setTimeout(() => setSaveMessage(''), 3000);
87
+ } catch (error) {
88
+ setSaveMessage('Error saving changes');
89
+ setTimeout(() => setSaveMessage(''), 3000);
90
+ } finally {
91
+ setIsSaving(false);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <div className="p-6 space-y-4">
97
+ <div className="bg-gray-100 p-4 rounded-lg border border-gray-200">
98
+ <h3 className="text-lg font-bold text-gray-900 m-0">
99
+ {label || 'Transition'}
100
+ </h3>
101
+ </div>
102
+
103
+ <Card className="shadow-sm">
104
+ <div className="space-y-4">
105
+ <div>
106
+ <h4 className="text-sm font-semibold text-gray-900 mb-2">Source</h4>
107
+ <p className="text-sm text-gray-600">{sourceNode?.data.label}</p>
108
+ </div>
109
+ <div>
110
+ <h4 className="text-sm font-semibold text-gray-900 mb-2">Target</h4>
111
+ <p className="text-sm text-gray-600">{targetNode?.data.label}</p>
112
+ </div>
113
+ <div>
114
+ <h4 className="text-sm font-semibold text-gray-900 mb-2">Label</h4>
115
+ <input
116
+ type="text"
117
+ value={label}
118
+ onChange={(e) => handleLabelChange(e.target.value)}
119
+ className="w-full p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
120
+ placeholder="Enter transition label"
121
+ />
122
+ </div>
123
+ <div>
124
+ <h4 className="text-sm font-semibold text-gray-900 mb-2">Requirements</h4>
125
+ <div className="space-y-2">
126
+ {requirements.map((req, index) => (
127
+ <div key={index} className="flex items-center justify-between bg-gray-50 p-2 rounded">
128
+ <span className="text-sm text-gray-700">{req}</span>
129
+ <button
130
+ onClick={() => handleRemoveRequirement(index)}
131
+ className="text-red-500 hover:text-red-700"
132
+ >
133
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
134
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
135
+ </svg>
136
+ </button>
137
+ </div>
138
+ ))}
139
+ <div className="flex gap-2">
140
+ <input
141
+ type="text"
142
+ value={newRequirement}
143
+ onChange={(e) => setNewRequirement(e.target.value)}
144
+ onKeyPress={(e) => e.key === 'Enter' && handleAddRequirement()}
145
+ className="flex-1 p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
146
+ placeholder="Add a requirement"
147
+ />
148
+ <button
149
+ onClick={handleAddRequirement}
150
+ className="bg-blue-500 text-white px-3 py-2 rounded hover:bg-blue-600 transition-colors"
151
+ >
152
+ Add
153
+ </button>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ <div className="flex items-center justify-between pt-4">
158
+ <button
159
+ onClick={handleSave}
160
+ disabled={isSaving}
161
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
162
+ >
163
+ {isSaving ? 'Saving...' : 'Save Details'}
164
+ </button>
165
+ {saveMessage && (
166
+ <span className={`text-sm ${saveMessage.includes('Error') || saveMessage.includes('Failed') ? 'text-red-600' : 'text-green-600'}`}>
167
+ {saveMessage}
168
+ </span>
169
+ )}
170
+ </div>
171
+ </div>
172
+ </Card>
173
+ </div>
174
+ );
175
+ };
@@ -0,0 +1,177 @@
1
+ import { Edge, Node, useEdges } from 'reactflow';
2
+ import { Card } from 'flowbite-react';
3
+ import { initialNodes } from '../config/flowConfig';
4
+ import { useState, useEffect } from 'react';
5
+
6
+ interface NodeDetailsProps {
7
+ node: Node;
8
+ onChange: (changes: any[]) => void;
9
+ onSave: () => Promise<boolean>;
10
+ }
11
+
12
+ export const NodeDetails = ({ node, onChange, onSave }: NodeDetailsProps) => {
13
+ const edges = useEdges();
14
+ const [label, setLabel] = useState(node?.data?.label || '');
15
+ const [description, setDescription] = useState(node?.data?.description || '');
16
+ const [isSaving, setIsSaving] = useState(false);
17
+ const [saveMessage, setSaveMessage] = useState('');
18
+
19
+ useEffect(() => {
20
+ setLabel(node?.data?.label || '');
21
+ setDescription(node?.data?.description || '');
22
+ }, [node]);
23
+
24
+ if (!node) {
25
+ return (
26
+ <div className="p-6 text-center text-gray-500">
27
+ <p className="text-sm">Select a node to view details</p>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ const incomingEdges = edges.filter(edge => edge.target === node.id);
33
+ const outgoingEdges = edges.filter(edge => edge.source === node.id);
34
+
35
+ const handleChange = (field: string, value: string) => {
36
+ if (field === 'label') setLabel(value);
37
+ if (field === 'description') setDescription(value);
38
+
39
+ onChange([{
40
+ id: node.id,
41
+ data: {
42
+ ...node.data,
43
+ [field]: value
44
+ }
45
+ }]);
46
+ };
47
+
48
+ const handleSave = async () => {
49
+ setIsSaving(true);
50
+ setSaveMessage('');
51
+ try {
52
+ const success = await onSave();
53
+ setSaveMessage(success ? 'Changes saved!' : 'Failed to save changes');
54
+ setTimeout(() => setSaveMessage(''), 3000);
55
+ } catch (error) {
56
+ setSaveMessage('Error saving changes');
57
+ setTimeout(() => setSaveMessage(''), 3000);
58
+ } finally {
59
+ setIsSaving(false);
60
+ }
61
+ };
62
+
63
+ return (
64
+ <div className="p-6 space-y-4">
65
+ <div className="bg-gray-100 p-4 rounded-lg border border-gray-200">
66
+ <h3 className="text-lg font-bold text-gray-900 m-0">
67
+ {label}
68
+ </h3>
69
+ <p className="text-sm text-gray-600 mt-1 mb-0">
70
+ {description}
71
+ </p>
72
+ </div>
73
+
74
+ <Card className="shadow-sm">
75
+ <div className="space-y-6">
76
+ <div>
77
+ <h4 className="text-sm font-semibold text-gray-900 mb-2">Name</h4>
78
+ <input
79
+ type="text"
80
+ value={label}
81
+ onChange={(e) => handleChange('label', e.target.value)}
82
+ className="w-full p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
83
+ placeholder="Enter node name"
84
+ />
85
+ </div>
86
+ <div>
87
+ <h4 className="text-sm font-semibold text-gray-900 mb-2">Description</h4>
88
+ <textarea
89
+ value={description}
90
+ onChange={(e) => handleChange('description', e.target.value)}
91
+ className="w-full p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
92
+ placeholder="Enter node description"
93
+ rows={3}
94
+ />
95
+ </div>
96
+
97
+ <div className="flex items-center justify-between pt-4">
98
+ <button
99
+ onClick={handleSave}
100
+ disabled={isSaving}
101
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
102
+ >
103
+ {isSaving ? 'Saving...' : 'Save Details'}
104
+ </button>
105
+ {saveMessage && (
106
+ <span className={`text-sm ${saveMessage.includes('Error') || saveMessage.includes('Failed') ? 'text-red-600' : 'text-green-600'}`}>
107
+ {saveMessage}
108
+ </span>
109
+ )}
110
+ </div>
111
+
112
+ <div>
113
+ <h4 className="text-sm font-semibold text-gray-900 mb-3">
114
+ Incoming Transitions
115
+ </h4>
116
+ {incomingEdges.length > 0 ? (
117
+ <div className="space-y-2">
118
+ {incomingEdges.map(edge => {
119
+ const sourceNode = initialNodes.find(n => n.id === edge.source);
120
+ return (
121
+ <div
122
+ key={edge.id}
123
+ className="flex items-center text-sm bg-gray-50 p-3 rounded-md hover:bg-gray-100 transition-colors"
124
+ >
125
+ <div className="flex items-center flex-1 min-w-0">
126
+ <span className="text-gray-600 truncate">{sourceNode?.data.label}</span>
127
+ <svg className="w-4 h-4 mx-2 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
128
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
129
+ </svg>
130
+ <span className="text-gray-900 font-medium truncate">{edge.label || 'Transition'}</span>
131
+ </div>
132
+ </div>
133
+ );
134
+ })}
135
+ </div>
136
+ ) : (
137
+ <p className="text-sm text-gray-500">No incoming transitions</p>
138
+ )}
139
+ </div>
140
+
141
+ {incomingEdges.length > 0 && outgoingEdges.length > 0 && (
142
+ <hr className="border-gray-200" />
143
+ )}
144
+
145
+ <div>
146
+ <h4 className="text-sm font-semibold text-gray-900 mb-3">
147
+ Outgoing Transitions
148
+ </h4>
149
+ {outgoingEdges.length > 0 ? (
150
+ <div className="space-y-2">
151
+ {outgoingEdges.map(edge => {
152
+ const targetNode = initialNodes.find(n => n.id === edge.target);
153
+ return (
154
+ <div
155
+ key={edge.id}
156
+ className="flex items-center text-sm bg-gray-50 p-3 rounded-md hover:bg-gray-100 transition-colors"
157
+ >
158
+ <div className="flex items-center flex-1 min-w-0">
159
+ <span className="text-gray-900 font-medium truncate">{edge.label || 'Transition'}</span>
160
+ <svg className="w-4 h-4 mx-2 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
161
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
162
+ </svg>
163
+ <span className="text-gray-600 truncate">{targetNode?.data.label}</span>
164
+ </div>
165
+ </div>
166
+ );
167
+ })}
168
+ </div>
169
+ ) : (
170
+ <p className="text-sm text-gray-500">No outgoing transitions</p>
171
+ )}
172
+ </div>
173
+ </div>
174
+ </Card>
175
+ </div>
176
+ );
177
+ };
@@ -0,0 +1,74 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+
3
+ interface ResizablePanelProps {
4
+ children: React.ReactNode;
5
+ defaultWidth?: number;
6
+ minWidth?: number;
7
+ maxWidth?: number;
8
+ }
9
+
10
+ export const ResizablePanel: React.FC<ResizablePanelProps> = ({
11
+ children,
12
+ defaultWidth = 400,
13
+ minWidth = 350,
14
+ maxWidth = 800
15
+ }) => {
16
+ const [width, setWidth] = useState(defaultWidth);
17
+ const [isDragging, setIsDragging] = useState(false);
18
+ const [startX, setStartX] = useState(0);
19
+ const [startWidth, setStartWidth] = useState(width);
20
+
21
+ const startResizing = useCallback((e: React.MouseEvent) => {
22
+ setIsDragging(true);
23
+ setStartX(e.pageX);
24
+ setStartWidth(width);
25
+ }, [width]);
26
+
27
+ const stopResizing = useCallback(() => {
28
+ setIsDragging(false);
29
+ }, []);
30
+
31
+ const resize = useCallback((e: MouseEvent) => {
32
+ if (isDragging) {
33
+ const diff = startX - e.pageX;
34
+ const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
35
+ setWidth(newWidth);
36
+ }
37
+ }, [isDragging, startX, startWidth, minWidth, maxWidth]);
38
+
39
+ useEffect(() => {
40
+ if (isDragging) {
41
+ document.addEventListener('mousemove', resize);
42
+ document.addEventListener('mouseup', stopResizing);
43
+ return () => {
44
+ document.removeEventListener('mousemove', resize);
45
+ document.removeEventListener('mouseup', stopResizing);
46
+ };
47
+ }
48
+ }, [isDragging, resize, stopResizing]);
49
+
50
+ return (
51
+ <div
52
+ className="relative"
53
+ style={{
54
+ width: `${width}px`,
55
+ minWidth: `${minWidth}px`,
56
+ maxWidth: `${maxWidth}px`
57
+ }}
58
+ >
59
+ <div
60
+ className="absolute left-0 top-0 w-1 h-full cursor-ew-resize group"
61
+ onMouseDown={startResizing}
62
+ style={{
63
+ transform: 'translateX(-50%)',
64
+ touchAction: 'none'
65
+ }}
66
+ >
67
+ <div className="absolute inset-y-0 left-1/2 w-4 -translate-x-1/2 group-hover:bg-blue-500/20 transition-colors" />
68
+ </div>
69
+ <div className="h-full bg-white border-l border-gray-200">
70
+ {children}
71
+ </div>
72
+ </div>
73
+ );
74
+ };
@@ -0,0 +1,45 @@
1
+ import { useState } from 'react';
2
+
3
+ interface SaveButtonProps {
4
+ onSave: () => Promise<boolean>;
5
+ }
6
+
7
+ export function SaveButton({ onSave }: SaveButtonProps) {
8
+ const [saving, setSaving] = useState(false);
9
+ const [message, setMessage] = useState('');
10
+
11
+ const handleSave = async () => {
12
+ setSaving(true);
13
+ setMessage('');
14
+
15
+ try {
16
+ const success = await onSave();
17
+ setMessage(success ? 'Saved successfully!' : 'Failed to save');
18
+ setTimeout(() => setMessage(''), 3000);
19
+ } catch (error) {
20
+ setMessage('Error saving workflow');
21
+ setTimeout(() => setMessage(''), 3000);
22
+ } finally {
23
+ setSaving(false);
24
+ }
25
+ };
26
+
27
+ return (
28
+ <div className="absolute top-4 right-4 z-50">
29
+ <button
30
+ onClick={handleSave}
31
+ disabled={saving}
32
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
33
+ >
34
+ {saving ? 'Saving...' : 'Save Workflow'}
35
+ </button>
36
+ {message && (
37
+ <div className={`mt-2 p-2 rounded text-sm ${
38
+ message.includes('success') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
39
+ }`}>
40
+ {message}
41
+ </div>
42
+ )}
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,59 @@
1
+ ---
2
+ object_type: Issue
3
+ places:
4
+ states:
5
+ - backlog
6
+ - sprint_planning
7
+ - sprint_backlog
8
+ - in_progress
9
+ - in_review
10
+ - testing
11
+ - done
12
+ special_states:
13
+ - blocked
14
+ transitions:
15
+ regular:
16
+ - name: move_to_sprint
17
+ from: backlog
18
+ to: sprint_planning
19
+ requires:
20
+ - description
21
+ - priority
22
+ - name: plan_issue
23
+ from: sprint_planning
24
+ to: sprint_backlog
25
+ requires:
26
+ - story_points
27
+ - sprint
28
+ - name: start_work
29
+ from: sprint_backlog
30
+ to: in_progress
31
+ requires:
32
+ - assignee
33
+ - name: submit_for_review
34
+ from: in_progress
35
+ to: in_review
36
+ requires:
37
+ - pull_request_url
38
+ - name: approve_review
39
+ from: in_review
40
+ to: testing
41
+ requires:
42
+ - review_approvals
43
+ - test_coverage
44
+ - name: pass_testing
45
+ from: testing
46
+ to: done
47
+ requires:
48
+ - qa_approved
49
+ - deployment_approved
50
+ - name: reject_review
51
+ from: in_review
52
+ to: in_progress
53
+ requires:
54
+ - review_comments
55
+ - name: fail_testing
56
+ from: testing
57
+ to: in_progress
58
+ requires:
59
+ - test_failure_reason
@@ -0,0 +1,11 @@
1
+ import path from 'path';
2
+
3
+ // This is used for server-side file operations
4
+ export const WORKFLOW_FILE = 'workflow.yaml';
5
+ export const WORKFLOW_PATH = path.join('src', 'config', WORKFLOW_FILE);
6
+
7
+ // Add a YAML header when saving
8
+ export const YAML_HEADER = '---\n';
9
+
10
+ // Note: For client-side imports, we must use the hardcoded path:
11
+ // import workflowConfig from './workflow.yaml';