create-merlin-brain 3.7.3 → 3.8.0-beta.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/files/commands/merlin/workflow.md +285 -0
- package/files/loop/lib/workflow-run.sh +359 -0
- package/files/loop/lib/workflow.sh +367 -0
- package/files/loop/merlin-loop.sh +30 -0
- package/files/loop/workflows/bug-fix.json +64 -0
- package/files/loop/workflows/feature-dev.json +73 -0
- package/files/loop/workflows/refactor.json +64 -0
- package/files/loop/workflows/security-audit.json +65 -0
- package/package.json +1 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: merlin:workflow
|
|
3
|
+
description: Run predefined multi-agent workflows (feature-dev, bug-fix, security-audit, refactor)
|
|
4
|
+
argument-hint: "[list | run <id> \"<task>\" | status | resume | skip]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Glob
|
|
8
|
+
- Grep
|
|
9
|
+
- Bash
|
|
10
|
+
- Write
|
|
11
|
+
- AskUserQuestion
|
|
12
|
+
- mcp__merlin__merlin_get_context
|
|
13
|
+
- mcp__merlin__merlin_search
|
|
14
|
+
- mcp__merlin__merlin_find_files
|
|
15
|
+
- mcp__merlin__merlin_sync_task
|
|
16
|
+
- mcp__merlin__merlin_save_checkpoint
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<objective>
|
|
20
|
+
Orchestrate predefined multi-agent workflows from within Claude Code sessions.
|
|
21
|
+
|
|
22
|
+
Workflows are named pipelines (e.g., "feature-dev", "bug-fix") that chain specialist agents
|
|
23
|
+
through a fixed sequence automatically. Each step spawns a FRESH Claude process with 200K context.
|
|
24
|
+
|
|
25
|
+
This command wraps the Merlin Loop workflow engine for interactive use:
|
|
26
|
+
- `list` — show available workflows
|
|
27
|
+
- `run <id> "<task>"` — execute a workflow
|
|
28
|
+
- `status` — check current run progress
|
|
29
|
+
- `resume` — resume an interrupted workflow
|
|
30
|
+
- `skip` — skip current step and advance
|
|
31
|
+
|
|
32
|
+
Workflows use the Blend Engine for optimal agent selection per step, structured handoffs
|
|
33
|
+
between steps, and independent verification for high-stakes steps.
|
|
34
|
+
</objective>
|
|
35
|
+
|
|
36
|
+
<process>
|
|
37
|
+
|
|
38
|
+
## Step 1: Parse Arguments
|
|
39
|
+
|
|
40
|
+
Extract from $ARGUMENTS:
|
|
41
|
+
- **subcommand**: First word (list, run, status, resume, skip)
|
|
42
|
+
- **workflow-id**: Second word (only for `run`)
|
|
43
|
+
- **task**: Everything after workflow-id (only for `run`)
|
|
44
|
+
|
|
45
|
+
If no arguments:
|
|
46
|
+
```
|
|
47
|
+
Available workflow commands:
|
|
48
|
+
|
|
49
|
+
/merlin:workflow list — See available workflows
|
|
50
|
+
/merlin:workflow run feature-dev "Add X" — Run a workflow
|
|
51
|
+
/merlin:workflow status — Check progress
|
|
52
|
+
/merlin:workflow resume — Resume interrupted workflow
|
|
53
|
+
/merlin:workflow skip — Skip current step
|
|
54
|
+
|
|
55
|
+
Bundled workflows: feature-dev, bug-fix, security-audit, refactor
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Step 2: Route by Subcommand
|
|
59
|
+
|
|
60
|
+
### `list` — Show Available Workflows
|
|
61
|
+
|
|
62
|
+
Read workflow JSON files from two locations:
|
|
63
|
+
```bash
|
|
64
|
+
# Bundled workflows (shipped with Merlin)
|
|
65
|
+
ls ~/.claude/loop/workflows/*.json 2>/dev/null
|
|
66
|
+
|
|
67
|
+
# User-installed workflows
|
|
68
|
+
ls ~/.claude/loop/workflows/*.json 2>/dev/null
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For each workflow file, extract and display:
|
|
72
|
+
- id, name, description, step count
|
|
73
|
+
- Step pipeline (e.g., "plan -> setup -> implement -> verify -> test -> pr -> review")
|
|
74
|
+
|
|
75
|
+
Format:
|
|
76
|
+
```
|
|
77
|
+
Available Workflows
|
|
78
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
79
|
+
|
|
80
|
+
feature-dev [7 steps]
|
|
81
|
+
Feature Development — Drop in a feature request. Get back a tested PR.
|
|
82
|
+
Pipeline: Planning -> Setup -> Implementation -> Verification -> Testing -> PR -> Review
|
|
83
|
+
|
|
84
|
+
bug-fix [6 steps]
|
|
85
|
+
Bug Fix — Report a bug. Get back a fix with regression test and PR.
|
|
86
|
+
Pipeline: Triage -> Investigation -> Fix -> Regression Test -> Verification -> PR
|
|
87
|
+
|
|
88
|
+
security-audit [6 steps]
|
|
89
|
+
Security Audit — Run a full security sweep. Get fixes and a clean report.
|
|
90
|
+
Pipeline: Scan -> Prioritize -> Fix -> Verification -> Tests -> PR
|
|
91
|
+
|
|
92
|
+
refactor [6 steps]
|
|
93
|
+
Code Refactor — Analyze, plan, refactor, verify, test, and PR.
|
|
94
|
+
Pipeline: Analysis -> Plan -> Refactor -> Verification -> Tests -> PR
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `run` — Execute a Workflow
|
|
98
|
+
|
|
99
|
+
**Required args:** workflow-id and task description.
|
|
100
|
+
|
|
101
|
+
If missing:
|
|
102
|
+
```
|
|
103
|
+
Usage: /merlin:workflow run <workflow-id> "<task description>"
|
|
104
|
+
|
|
105
|
+
Example: /merlin:workflow run feature-dev "Add OAuth login with Google and GitHub providers"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Step 2a: Load Workflow
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
WORKFLOW_FILE=""
|
|
112
|
+
for dir in ~/.claude/loop/workflows; do
|
|
113
|
+
if [ -f "$dir/${WORKFLOW_ID}.json" ]; then
|
|
114
|
+
WORKFLOW_FILE="$dir/${WORKFLOW_ID}.json"
|
|
115
|
+
break
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
If not found, list available workflows and error.
|
|
121
|
+
|
|
122
|
+
#### Step 2b: Get Sights Context
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Call: merlin_get_context
|
|
126
|
+
Task: "{task description}"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Step 2c: Pre-Flight Summary
|
|
130
|
+
|
|
131
|
+
Parse the workflow JSON and show:
|
|
132
|
+
```
|
|
133
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
134
|
+
WORKFLOW: Feature Development
|
|
135
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
136
|
+
|
|
137
|
+
Task: Add OAuth login with Google and GitHub providers
|
|
138
|
+
Steps: 7
|
|
139
|
+
Pipeline: Planning -> Setup -> Implementation -> Verification -> Testing -> PR -> Review
|
|
140
|
+
|
|
141
|
+
Each step spawns a fresh Claude process with 200K context.
|
|
142
|
+
Estimated time: 15-30 minutes depending on complexity.
|
|
143
|
+
|
|
144
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Ask user to confirm or provide additional context:
|
|
148
|
+
- "Any constraints or notes before we start?"
|
|
149
|
+
- If none, proceed.
|
|
150
|
+
|
|
151
|
+
#### Step 2d: Execute via merlin-loop
|
|
152
|
+
|
|
153
|
+
Spawn the workflow engine as a shell process:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
cd "$(pwd)" && merlin-loop workflow run "${WORKFLOW_ID}" "${TASK}" 2>&1
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use Bash tool with `timeout: 600000` (10 minutes per step, up to 60 minutes total).
|
|
160
|
+
|
|
161
|
+
**IMPORTANT:** If `merlin-loop` is not in PATH, find it:
|
|
162
|
+
```bash
|
|
163
|
+
LOOP_SCRIPT="${HOME}/.claude/loop/merlin-loop.sh"
|
|
164
|
+
bash "$LOOP_SCRIPT" workflow run "${WORKFLOW_ID}" "${TASK}" 2>&1
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Step 2e: Monitor and Report
|
|
168
|
+
|
|
169
|
+
After each step completes, the workflow engine outputs progress. Capture and relay to user:
|
|
170
|
+
- Step name and status (done/failed/skipped)
|
|
171
|
+
- Overall progress (e.g., "3/7 steps complete")
|
|
172
|
+
|
|
173
|
+
If the workflow pauses on failure:
|
|
174
|
+
```
|
|
175
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
176
|
+
WORKFLOW PAUSED at step: implement
|
|
177
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
178
|
+
|
|
179
|
+
Step "Implementation" failed after 3 attempts.
|
|
180
|
+
|
|
181
|
+
[1] Resume workflow (/merlin:workflow resume)
|
|
182
|
+
[2] Skip this step (/merlin:workflow skip)
|
|
183
|
+
[3] Investigate the failure
|
|
184
|
+
[4] Abort
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Step 2f: Completion
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
191
|
+
WORKFLOW COMPLETE: Feature Development
|
|
192
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
193
|
+
|
|
194
|
+
Task: Add OAuth login
|
|
195
|
+
Steps: 7/7 complete
|
|
196
|
+
Duration: ~22 minutes
|
|
197
|
+
|
|
198
|
+
Steps completed:
|
|
199
|
+
✓ Planning
|
|
200
|
+
✓ Environment Setup
|
|
201
|
+
✓ Implementation
|
|
202
|
+
✓ Independent Verification
|
|
203
|
+
✓ Testing
|
|
204
|
+
✓ Pull Request
|
|
205
|
+
✓ Final Review
|
|
206
|
+
|
|
207
|
+
Next steps:
|
|
208
|
+
[1] Review the PR
|
|
209
|
+
[2] Run another workflow
|
|
210
|
+
[3] Continue in current context
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `status` — Show Current Run
|
|
214
|
+
|
|
215
|
+
Read the workflow run state:
|
|
216
|
+
```bash
|
|
217
|
+
cat .merlin-loop/workflow-run.json 2>/dev/null
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Parse and display:
|
|
221
|
+
```
|
|
222
|
+
Workflow: Feature Development
|
|
223
|
+
Task: Add OAuth login
|
|
224
|
+
Status: running
|
|
225
|
+
Step: implement (3/7)
|
|
226
|
+
|
|
227
|
+
✓ plan (done)
|
|
228
|
+
✓ setup (done)
|
|
229
|
+
▶ implement (running, attempt 1)
|
|
230
|
+
· verify (pending)
|
|
231
|
+
· test (pending)
|
|
232
|
+
· pr (pending)
|
|
233
|
+
· review (pending)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
If no active run: "No active workflow. Run one with: /merlin:workflow run <id> \"task\""
|
|
237
|
+
|
|
238
|
+
### `resume` — Resume Interrupted Workflow
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
LOOP_SCRIPT="${HOME}/.claude/loop/merlin-loop.sh"
|
|
242
|
+
bash "$LOOP_SCRIPT" workflow resume 2>&1
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Show progress as steps complete.
|
|
246
|
+
|
|
247
|
+
### `skip` — Skip Current Step
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
LOOP_SCRIPT="${HOME}/.claude/loop/merlin-loop.sh"
|
|
251
|
+
bash "$LOOP_SCRIPT" workflow skip 2>&1
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Confirm the skip and show updated status.
|
|
255
|
+
|
|
256
|
+
</process>
|
|
257
|
+
|
|
258
|
+
<critical_rules>
|
|
259
|
+
|
|
260
|
+
**ALWAYS use merlin-loop workflow engine** — do NOT try to orchestrate steps manually.
|
|
261
|
+
The workflow engine handles: blend selection, handoffs, retries, state tracking, resumability.
|
|
262
|
+
|
|
263
|
+
**FRESH PROCESS PER STEP** — each workflow step spawns a new `claude --agent` process.
|
|
264
|
+
This is handled by the workflow engine, not by this command.
|
|
265
|
+
|
|
266
|
+
**NEVER run specialist work in this orchestrator context** — only manage the workflow lifecycle.
|
|
267
|
+
|
|
268
|
+
**STATE IS PERSISTENT** — workflow-run.json survives crashes. Resume picks up exactly where it left off.
|
|
269
|
+
|
|
270
|
+
**TIMEOUT** — use 600000ms (10 min) for status/resume/skip, up to 3600000ms (60 min) for full workflow runs.
|
|
271
|
+
|
|
272
|
+
</critical_rules>
|
|
273
|
+
|
|
274
|
+
<error_handling>
|
|
275
|
+
|
|
276
|
+
| Condition | Action |
|
|
277
|
+
|-----------|--------|
|
|
278
|
+
| Workflow ID not found | List available workflows, suggest closest match |
|
|
279
|
+
| merlin-loop not found | Check ~/.claude/loop/merlin-loop.sh, suggest reinstall |
|
|
280
|
+
| Workflow step fails | Show failure, offer resume/skip/investigate/abort |
|
|
281
|
+
| Timeout on full run | Suggest using `merlin-loop workflow run` directly in terminal |
|
|
282
|
+
| No workflow-run.json | Report "no active run" for status/resume/skip |
|
|
283
|
+
| Invalid JSON in workflow | Report parse error, suggest checking file |
|
|
284
|
+
|
|
285
|
+
</error_handling>
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
4
|
+
# ║ MERLIN WORKFLOW ENGINE — Run, Resume, Status, CLI ║
|
|
5
|
+
# ║ Orchestration layer on top of workflow.sh core engine ║
|
|
6
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
7
|
+
#
|
|
8
|
+
# Requires: workflow.sh loaded first (provides core functions)
|
|
9
|
+
|
|
10
|
+
# Colors (inherit or set defaults)
|
|
11
|
+
: "${RESET:=\033[0m}"
|
|
12
|
+
: "${BOLD:=\033[1m}"
|
|
13
|
+
: "${DIM:=\033[2m}"
|
|
14
|
+
: "${RED:=\033[31m}"
|
|
15
|
+
: "${GREEN:=\033[32m}"
|
|
16
|
+
: "${YELLOW:=\033[33m}"
|
|
17
|
+
: "${BLUE:=\033[34m}"
|
|
18
|
+
: "${MAGENTA:=\033[35m}"
|
|
19
|
+
: "${CYAN:=\033[36m}"
|
|
20
|
+
|
|
21
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
# Main Run Orchestrator
|
|
23
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
# Run a complete workflow
|
|
26
|
+
workflow_run() {
|
|
27
|
+
local wf_id="$1"
|
|
28
|
+
local task="$2"
|
|
29
|
+
|
|
30
|
+
# Load workflow
|
|
31
|
+
local wf_json
|
|
32
|
+
wf_json=$(workflow_load "$wf_id") || return 1
|
|
33
|
+
|
|
34
|
+
local wf_name step_count on_failure
|
|
35
|
+
wf_name=$(echo "$wf_json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('name', '$wf_id'))")
|
|
36
|
+
step_count=$(echo "$wf_json" | python3 -c "import json,sys; print(len(json.load(sys.stdin)['steps']))")
|
|
37
|
+
on_failure=$(echo "$wf_json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('on_failure','pause'))")
|
|
38
|
+
|
|
39
|
+
# Banner
|
|
40
|
+
echo -e "${MAGENTA}${BOLD}"
|
|
41
|
+
echo " ╔═══════════════════════════════════════════════════════════╗"
|
|
42
|
+
echo " ║ MERLIN WORKFLOW: ${wf_name}"
|
|
43
|
+
echo " ╚═══════════════════════════════════════════════════════════╝"
|
|
44
|
+
echo -e "${RESET}"
|
|
45
|
+
echo -e " ${CYAN}Task:${RESET} $task"
|
|
46
|
+
echo -e " ${CYAN}Steps:${RESET} $step_count"
|
|
47
|
+
echo ""
|
|
48
|
+
|
|
49
|
+
# Show step pipeline
|
|
50
|
+
local step_labels
|
|
51
|
+
step_labels=$(echo "$wf_json" | python3 -c "
|
|
52
|
+
import json,sys
|
|
53
|
+
wf = json.load(sys.stdin)
|
|
54
|
+
labels = [s.get('label', s['id']) for s in wf['steps']]
|
|
55
|
+
print(' -> '.join(labels))
|
|
56
|
+
")
|
|
57
|
+
echo -e " ${DIM}Pipeline: ${step_labels}${RESET}"
|
|
58
|
+
echo ""
|
|
59
|
+
|
|
60
|
+
# Initialize run
|
|
61
|
+
local session_dir="${MERLIN_LOOP_DIR:-.merlin-loop}/workflow-session"
|
|
62
|
+
mkdir -p "$session_dir"
|
|
63
|
+
|
|
64
|
+
local run_id
|
|
65
|
+
run_id=$(workflow_run_init "$wf_json" "$task")
|
|
66
|
+
echo -e " ${BLUE}Run ID: ${BOLD}${run_id}${RESET}"
|
|
67
|
+
echo ""
|
|
68
|
+
|
|
69
|
+
# Create handoff chain
|
|
70
|
+
if type handoff_create &>/dev/null; then
|
|
71
|
+
handoff_create "$session_dir" >/dev/null
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Execute steps sequentially
|
|
75
|
+
local step_num=0
|
|
76
|
+
local total_steps="$step_count"
|
|
77
|
+
|
|
78
|
+
echo "$wf_json" | python3 -c "
|
|
79
|
+
import json,sys
|
|
80
|
+
wf = json.load(sys.stdin)
|
|
81
|
+
for s in wf['steps']:
|
|
82
|
+
print(json.dumps(s))
|
|
83
|
+
" | while IFS= read -r step_json; do
|
|
84
|
+
step_num=$((step_num + 1))
|
|
85
|
+
local step_id step_label
|
|
86
|
+
step_id=$(echo "$step_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
|
87
|
+
step_label=$(echo "$step_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('label', d['id']))")
|
|
88
|
+
|
|
89
|
+
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
90
|
+
echo -e "${BOLD} Step ${step_num}/${total_steps}: ${step_label}${RESET}"
|
|
91
|
+
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
92
|
+
|
|
93
|
+
# Get handoff context for this step
|
|
94
|
+
local handoff_context=""
|
|
95
|
+
if type handoff_format_for_agent &>/dev/null && [ -f "$session_dir/handoff.json" ]; then
|
|
96
|
+
handoff_context=$(handoff_format_for_agent "$session_dir/handoff.json")
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Execute step
|
|
100
|
+
local step_output
|
|
101
|
+
step_output=$(workflow_step_run "$step_json" "$task" "$handoff_context" "$session_dir")
|
|
102
|
+
local step_exit=$?
|
|
103
|
+
|
|
104
|
+
if [ $step_exit -eq 0 ]; then
|
|
105
|
+
echo -e " ${GREEN}${BOLD}✓ ${step_label} complete${RESET}"
|
|
106
|
+
else
|
|
107
|
+
echo -e " ${RED}${BOLD}✗ ${step_label} failed${RESET}"
|
|
108
|
+
case "$on_failure" in
|
|
109
|
+
pause)
|
|
110
|
+
echo -e " ${YELLOW}Workflow paused. Resume with: merlin-loop workflow resume${RESET}"
|
|
111
|
+
return 1 ;;
|
|
112
|
+
skip)
|
|
113
|
+
echo -e " ${YELLOW}Skipping failed step and continuing...${RESET}"
|
|
114
|
+
_workflow_update_step "$step_id" "skipped" ;;
|
|
115
|
+
abort)
|
|
116
|
+
echo -e " ${RED}Workflow aborted.${RESET}"
|
|
117
|
+
return 1 ;;
|
|
118
|
+
esac
|
|
119
|
+
fi
|
|
120
|
+
echo ""
|
|
121
|
+
done
|
|
122
|
+
|
|
123
|
+
# Completion banner
|
|
124
|
+
echo -e "${GREEN}${BOLD}"
|
|
125
|
+
echo " ╔═══════════════════════════════════════════════════════════╗"
|
|
126
|
+
echo " ║ WORKFLOW COMPLETE ║"
|
|
127
|
+
echo " ╚═══════════════════════════════════════════════════════════╝"
|
|
128
|
+
echo -e "${RESET}"
|
|
129
|
+
|
|
130
|
+
local on_complete
|
|
131
|
+
on_complete=$(echo "$wf_json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('on_complete','notify'))")
|
|
132
|
+
if [ "$on_complete" = "notify" ] && type send_notification &>/dev/null; then
|
|
133
|
+
send_notification "Workflow Complete" "${wf_name}: ${task}"
|
|
134
|
+
fi
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
139
|
+
# Resume from interrupted run
|
|
140
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
141
|
+
|
|
142
|
+
workflow_resume() {
|
|
143
|
+
local run_id="${1:-}"
|
|
144
|
+
|
|
145
|
+
if [ ! -f "$WORKFLOW_RUN_FILE" ]; then
|
|
146
|
+
echo -e "${RED}No workflow run to resume.${RESET}" >&2
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
local wf_id task current_step
|
|
151
|
+
wf_id=$(python3 -c "import json; r=json.load(open('$WORKFLOW_RUN_FILE')); print(r['workflow'])")
|
|
152
|
+
task=$(python3 -c "import json; r=json.load(open('$WORKFLOW_RUN_FILE')); print(r['task'])")
|
|
153
|
+
current_step=$(python3 -c "import json; r=json.load(open('$WORKFLOW_RUN_FILE')); print(r.get('current_step',''))")
|
|
154
|
+
|
|
155
|
+
if [ -z "$current_step" ] || [ "$current_step" = "None" ]; then
|
|
156
|
+
echo -e "${GREEN}Workflow already completed.${RESET}"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
echo -e "${BLUE}Resuming workflow '${wf_id}' from step '${current_step}'${RESET}"
|
|
161
|
+
echo -e "${BLUE}Task: ${task}${RESET}"
|
|
162
|
+
echo ""
|
|
163
|
+
|
|
164
|
+
local wf_json
|
|
165
|
+
wf_json=$(workflow_load "$wf_id") || return 1
|
|
166
|
+
|
|
167
|
+
local session_dir="${MERLIN_LOOP_DIR:-.merlin-loop}/workflow-session"
|
|
168
|
+
mkdir -p "$session_dir"
|
|
169
|
+
|
|
170
|
+
if [ ! -f "$session_dir/handoff.json" ] && type handoff_create &>/dev/null; then
|
|
171
|
+
handoff_create "$session_dir" >/dev/null
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
local step_num=0
|
|
175
|
+
echo "$wf_json" | python3 -c "
|
|
176
|
+
import json,sys
|
|
177
|
+
wf = json.load(sys.stdin)
|
|
178
|
+
for s in wf['steps']:
|
|
179
|
+
print(json.dumps(s))
|
|
180
|
+
" | while IFS= read -r step_json; do
|
|
181
|
+
step_num=$((step_num + 1))
|
|
182
|
+
local step_id
|
|
183
|
+
step_id=$(echo "$step_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
|
184
|
+
|
|
185
|
+
local step_status
|
|
186
|
+
step_status=$(python3 -c "import json; r=json.load(open('$WORKFLOW_RUN_FILE')); print(r['steps'].get('$step_id',{}).get('status','pending'))")
|
|
187
|
+
|
|
188
|
+
if [ "$step_status" = "done" ] || [ "$step_status" = "skipped" ]; then
|
|
189
|
+
echo -e " ${DIM}✓ ${step_id} (${step_status})${RESET}"
|
|
190
|
+
continue
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
local step_label
|
|
194
|
+
step_label=$(echo "$step_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('label', d['id']))")
|
|
195
|
+
|
|
196
|
+
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
197
|
+
echo -e "${BOLD} Resuming: ${step_label}${RESET}"
|
|
198
|
+
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
199
|
+
|
|
200
|
+
local handoff_context=""
|
|
201
|
+
if type handoff_format_for_agent &>/dev/null && [ -f "$session_dir/handoff.json" ]; then
|
|
202
|
+
handoff_context=$(handoff_format_for_agent "$session_dir/handoff.json")
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
local step_output
|
|
206
|
+
step_output=$(workflow_step_run "$step_json" "$task" "$handoff_context" "$session_dir")
|
|
207
|
+
local step_exit=$?
|
|
208
|
+
|
|
209
|
+
if [ $step_exit -eq 0 ]; then
|
|
210
|
+
echo -e " ${GREEN}${BOLD}✓ ${step_label} complete${RESET}"
|
|
211
|
+
else
|
|
212
|
+
echo -e " ${RED}${BOLD}✗ ${step_label} failed${RESET}"
|
|
213
|
+
echo -e " ${YELLOW}Paused. Resume with: merlin-loop workflow resume${RESET}"
|
|
214
|
+
return 1
|
|
215
|
+
fi
|
|
216
|
+
done
|
|
217
|
+
|
|
218
|
+
echo -e "${GREEN}${BOLD} Workflow resumed and completed.${RESET}"
|
|
219
|
+
return 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
223
|
+
# Status Display
|
|
224
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
workflow_status() {
|
|
227
|
+
if [ ! -f "$WORKFLOW_RUN_FILE" ]; then
|
|
228
|
+
echo -e "${DIM}No active workflow run.${RESET}"
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
python3 << PYEOF
|
|
233
|
+
import json
|
|
234
|
+
|
|
235
|
+
with open("$WORKFLOW_RUN_FILE", 'r') as f:
|
|
236
|
+
run = json.load(f)
|
|
237
|
+
|
|
238
|
+
print(f"\n \033[1mWorkflow:\033[0m {run.get('workflow_name', run['workflow'])}")
|
|
239
|
+
print(f" \033[1mTask:\033[0m {run['task']}")
|
|
240
|
+
print(f" \033[1mStatus:\033[0m {run['status']}")
|
|
241
|
+
print(f" \033[1mRun ID:\033[0m {run['run_id']}")
|
|
242
|
+
print(f" \033[1mStarted:\033[0m {run['started']}")
|
|
243
|
+
print()
|
|
244
|
+
|
|
245
|
+
for step_id, step in run['steps'].items():
|
|
246
|
+
status = step.get('status', 'pending')
|
|
247
|
+
icons = {'done': '\033[32m\u2713\033[0m', 'running': '\033[33m\u25b6\033[0m',
|
|
248
|
+
'failed': '\033[31m\u2717\033[0m', 'skipped': '\033[2m\u25cb\033[0m',
|
|
249
|
+
'pending': '\033[2m\u00b7\033[0m'}
|
|
250
|
+
icon = icons.get(status, '?')
|
|
251
|
+
current = " \u2190 current" if step_id == run.get('current_step') else ""
|
|
252
|
+
attempts = f" (attempts: {step.get('attempts', 0)})" if step.get('attempts', 0) > 1 else ""
|
|
253
|
+
print(f" {icon} {step_id}{attempts}{current}")
|
|
254
|
+
print()
|
|
255
|
+
PYEOF
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
259
|
+
# Skip & Install
|
|
260
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
261
|
+
|
|
262
|
+
workflow_skip() {
|
|
263
|
+
if [ ! -f "$WORKFLOW_RUN_FILE" ]; then
|
|
264
|
+
echo -e "${RED}No active workflow run.${RESET}" >&2
|
|
265
|
+
return 1
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
local current_step
|
|
269
|
+
current_step=$(python3 -c "import json; r=json.load(open('$WORKFLOW_RUN_FILE')); print(r.get('current_step',''))")
|
|
270
|
+
|
|
271
|
+
if [ -z "$current_step" ] || [ "$current_step" = "None" ]; then
|
|
272
|
+
echo -e "${GREEN}Workflow already completed.${RESET}"
|
|
273
|
+
return 0
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
_workflow_update_step "$current_step" "skipped"
|
|
277
|
+
echo -e "${YELLOW}Skipped step: ${current_step}${RESET}"
|
|
278
|
+
workflow_status
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
workflow_install() {
|
|
282
|
+
local source="$1"
|
|
283
|
+
mkdir -p "$WORKFLOW_DIR"
|
|
284
|
+
|
|
285
|
+
if [[ "$source" == http* ]] || [[ "$source" == github.com/* ]]; then
|
|
286
|
+
local url="$source"
|
|
287
|
+
[[ "$url" != http* ]] && url="https://${url}"
|
|
288
|
+
|
|
289
|
+
if echo "$url" | grep -q "github.com"; then
|
|
290
|
+
url=$(echo "$url" | sed 's|github.com|raw.githubusercontent.com|' | sed 's|/blob/||')
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
local filename
|
|
294
|
+
filename=$(basename "$url")
|
|
295
|
+
[[ "$filename" != *.json ]] && filename="${filename}.json"
|
|
296
|
+
|
|
297
|
+
echo -e "${BLUE}Downloading: ${url}${RESET}"
|
|
298
|
+
if curl -fsSL "$url" -o "$WORKFLOW_DIR/$filename" 2>/dev/null; then
|
|
299
|
+
if python3 -c "import json; json.load(open('$WORKFLOW_DIR/$filename'))" 2>/dev/null; then
|
|
300
|
+
echo -e "${GREEN}Installed: ${WORKFLOW_DIR}/${filename}${RESET}"
|
|
301
|
+
else
|
|
302
|
+
rm -f "$WORKFLOW_DIR/$filename"
|
|
303
|
+
echo -e "${RED}Invalid JSON. Installation failed.${RESET}" >&2
|
|
304
|
+
return 1
|
|
305
|
+
fi
|
|
306
|
+
else
|
|
307
|
+
echo -e "${RED}Download failed.${RESET}" >&2
|
|
308
|
+
return 1
|
|
309
|
+
fi
|
|
310
|
+
elif [ -f "$source" ]; then
|
|
311
|
+
local filename
|
|
312
|
+
filename=$(basename "$source")
|
|
313
|
+
cp "$source" "$WORKFLOW_DIR/$filename"
|
|
314
|
+
echo -e "${GREEN}Installed: ${WORKFLOW_DIR}/${filename}${RESET}"
|
|
315
|
+
else
|
|
316
|
+
echo -e "${RED}Source not found: ${source}${RESET}" >&2
|
|
317
|
+
return 1
|
|
318
|
+
fi
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
322
|
+
# CLI Dispatch
|
|
323
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
324
|
+
|
|
325
|
+
workflow_dispatch() {
|
|
326
|
+
local subcmd="${1:-list}"
|
|
327
|
+
shift 2>/dev/null || true
|
|
328
|
+
|
|
329
|
+
case "$subcmd" in
|
|
330
|
+
list) workflow_list ;;
|
|
331
|
+
run)
|
|
332
|
+
local wf_id="${1:-}"
|
|
333
|
+
shift 2>/dev/null || true
|
|
334
|
+
local task="$*"
|
|
335
|
+
if [ -z "$wf_id" ] || [ -z "$task" ]; then
|
|
336
|
+
echo -e "${RED}Usage: merlin-loop workflow run <id> \"<task>\"${RESET}" >&2
|
|
337
|
+
echo -e "${DIM}Example: merlin-loop workflow run feature-dev \"Add OAuth login\"${RESET}" >&2
|
|
338
|
+
return 1
|
|
339
|
+
fi
|
|
340
|
+
workflow_run "$wf_id" "$task"
|
|
341
|
+
;;
|
|
342
|
+
status) workflow_status ;;
|
|
343
|
+
resume) workflow_resume "$@" ;;
|
|
344
|
+
skip) workflow_skip ;;
|
|
345
|
+
install)
|
|
346
|
+
local source="${1:-}"
|
|
347
|
+
if [ -z "$source" ]; then
|
|
348
|
+
echo -e "${RED}Usage: merlin-loop workflow install <url|file>${RESET}" >&2
|
|
349
|
+
return 1
|
|
350
|
+
fi
|
|
351
|
+
workflow_install "$source"
|
|
352
|
+
;;
|
|
353
|
+
*)
|
|
354
|
+
echo -e "${RED}Unknown workflow command: ${subcmd}${RESET}" >&2
|
|
355
|
+
echo -e "Commands: list, run, status, resume, skip, install" >&2
|
|
356
|
+
return 1
|
|
357
|
+
;;
|
|
358
|
+
esac
|
|
359
|
+
}
|