create-merlin-brain 3.7.2 → 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/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +60 -57
- package/dist/server/server.js.map +1 -1
- 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,367 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
4
|
+
# ║ MERLIN WORKFLOW ENGINE - Core Engine ║
|
|
5
|
+
# ║ Load, validate, execute steps, manage state ║
|
|
6
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
7
|
+
#
|
|
8
|
+
# A thin orchestration layer on top of existing primitives:
|
|
9
|
+
# - Agent selection → blend_for_task() (blend.sh)
|
|
10
|
+
# - Context passing → handoff_append() (blend-handoff.sh)
|
|
11
|
+
# - Verification → blend_and_verify() (blend-verify.sh)
|
|
12
|
+
# - Parallel steps → execute_wave_teams() (teams.sh)
|
|
13
|
+
# - Learning → learn_record() (blend-learn.sh)
|
|
14
|
+
#
|
|
15
|
+
# This file: core engine (load, template, step execution, state)
|
|
16
|
+
# See workflow-run.sh: orchestration (run, resume, status, CLI dispatch)
|
|
17
|
+
#
|
|
18
|
+
# Requires: blend.sh, blend-handoff.sh loaded first
|
|
19
|
+
|
|
20
|
+
# Colors
|
|
21
|
+
: "${RESET:=\033[0m}"
|
|
22
|
+
: "${BOLD:=\033[1m}"
|
|
23
|
+
: "${DIM:=\033[2m}"
|
|
24
|
+
: "${RED:=\033[31m}"
|
|
25
|
+
: "${GREEN:=\033[32m}"
|
|
26
|
+
: "${YELLOW:=\033[33m}"
|
|
27
|
+
: "${BLUE:=\033[34m}"
|
|
28
|
+
: "${MAGENTA:=\033[35m}"
|
|
29
|
+
: "${CYAN:=\033[36m}"
|
|
30
|
+
|
|
31
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
# Configuration
|
|
33
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
WORKFLOW_DIR="${HOME}/.claude/loop/workflows"
|
|
36
|
+
WORKFLOW_BUNDLED_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/workflows"
|
|
37
|
+
WORKFLOW_RUN_FILE="${MERLIN_LOOP_DIR:-.merlin-loop}/workflow-run.json"
|
|
38
|
+
|
|
39
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
40
|
+
# Load & Validate
|
|
41
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
# Load a workflow definition by ID
|
|
44
|
+
# Returns the JSON content on stdout, errors to stderr
|
|
45
|
+
workflow_load() {
|
|
46
|
+
local id="$1"
|
|
47
|
+
local wf_file=""
|
|
48
|
+
|
|
49
|
+
# Search order: user dir → bundled dir
|
|
50
|
+
if [ -f "${WORKFLOW_DIR}/${id}.json" ]; then
|
|
51
|
+
wf_file="${WORKFLOW_DIR}/${id}.json"
|
|
52
|
+
elif [ -f "${WORKFLOW_BUNDLED_DIR}/${id}.json" ]; then
|
|
53
|
+
wf_file="${WORKFLOW_BUNDLED_DIR}/${id}.json"
|
|
54
|
+
else
|
|
55
|
+
echo -e "${RED}Workflow '${id}' not found.${RESET}" >&2
|
|
56
|
+
echo -e "${DIM}Searched: ${WORKFLOW_DIR}/ and ${WORKFLOW_BUNDLED_DIR}/${RESET}" >&2
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Validate: valid JSON with required fields
|
|
61
|
+
if ! python3 -c "
|
|
62
|
+
import json, sys
|
|
63
|
+
with open('$wf_file') as f:
|
|
64
|
+
wf = json.load(f)
|
|
65
|
+
assert 'id' in wf, 'Missing id'
|
|
66
|
+
assert 'steps' in wf, 'Missing steps'
|
|
67
|
+
assert len(wf['steps']) > 0, 'No steps defined'
|
|
68
|
+
print(json.dumps(wf))
|
|
69
|
+
" 2>/dev/null; then
|
|
70
|
+
echo -e "${RED}Invalid workflow definition: ${wf_file}${RESET}" >&2
|
|
71
|
+
return 1
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# List all available workflows
|
|
76
|
+
workflow_list() {
|
|
77
|
+
echo -e "${MAGENTA}${BOLD} Available Workflows${RESET}"
|
|
78
|
+
echo -e "${MAGENTA} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
79
|
+
|
|
80
|
+
local found=0
|
|
81
|
+
for dir in "$WORKFLOW_BUNDLED_DIR" "$WORKFLOW_DIR"; do
|
|
82
|
+
[ ! -d "$dir" ] && continue
|
|
83
|
+
for wf_file in "$dir"/*.json; do
|
|
84
|
+
[ ! -f "$wf_file" ] && continue
|
|
85
|
+
found=1
|
|
86
|
+
|
|
87
|
+
local id name desc step_count
|
|
88
|
+
id=$(python3 -c "import json; wf=json.load(open('$wf_file')); print(wf.get('id','?'))" 2>/dev/null)
|
|
89
|
+
name=$(python3 -c "import json; wf=json.load(open('$wf_file')); print(wf.get('name','?'))" 2>/dev/null)
|
|
90
|
+
desc=$(python3 -c "import json; wf=json.load(open('$wf_file')); print(wf.get('description',''))" 2>/dev/null)
|
|
91
|
+
step_count=$(python3 -c "import json; wf=json.load(open('$wf_file')); print(len(wf.get('steps',[])))" 2>/dev/null)
|
|
92
|
+
|
|
93
|
+
local source_tag=""
|
|
94
|
+
[[ "$dir" == "$WORKFLOW_DIR" ]] && source_tag="${CYAN}[user]${RESET}" || source_tag="${DIM}[bundled]${RESET}"
|
|
95
|
+
|
|
96
|
+
echo -e " ${GREEN}${BOLD}${id}${RESET} ${source_tag} ${step_count} steps"
|
|
97
|
+
echo -e " ${name} — ${DIM}${desc}${RESET}"
|
|
98
|
+
echo ""
|
|
99
|
+
done
|
|
100
|
+
done
|
|
101
|
+
|
|
102
|
+
[ "$found" -eq 0 ] && echo -e " ${DIM}No workflows found. Place .json files in: ${WORKFLOW_DIR}/${RESET}"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
106
|
+
# Run Initialization
|
|
107
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
108
|
+
|
|
109
|
+
workflow_run_init() {
|
|
110
|
+
local workflow_json="$1"
|
|
111
|
+
local task="$2"
|
|
112
|
+
|
|
113
|
+
local wf_id run_id
|
|
114
|
+
wf_id=$(echo "$workflow_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
|
115
|
+
run_id="${wf_id}-$(date +%s | tail -c 7)"
|
|
116
|
+
|
|
117
|
+
mkdir -p "$(dirname "$WORKFLOW_RUN_FILE")"
|
|
118
|
+
|
|
119
|
+
python3 << PYEOF
|
|
120
|
+
import json, sys
|
|
121
|
+
from datetime import datetime, timezone
|
|
122
|
+
|
|
123
|
+
wf = json.loads('''$workflow_json''')
|
|
124
|
+
task = """$task"""
|
|
125
|
+
|
|
126
|
+
run = {
|
|
127
|
+
"run_id": "$run_id",
|
|
128
|
+
"workflow": wf["id"],
|
|
129
|
+
"workflow_name": wf.get("name", wf["id"]),
|
|
130
|
+
"task": task,
|
|
131
|
+
"started": datetime.now(timezone.utc).isoformat(),
|
|
132
|
+
"status": "running",
|
|
133
|
+
"current_step": wf["steps"][0]["id"],
|
|
134
|
+
"steps": {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for step in wf["steps"]:
|
|
138
|
+
run["steps"][step["id"]] = {"status": "pending"}
|
|
139
|
+
|
|
140
|
+
with open("$WORKFLOW_RUN_FILE", 'w') as f:
|
|
141
|
+
json.dump(run, f, indent=2)
|
|
142
|
+
|
|
143
|
+
print("$run_id")
|
|
144
|
+
PYEOF
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
148
|
+
# Template Substitution
|
|
149
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
150
|
+
|
|
151
|
+
workflow_apply_template() {
|
|
152
|
+
local template="$1"
|
|
153
|
+
local task="$2"
|
|
154
|
+
local handoff_context="$3"
|
|
155
|
+
local session_dir="$4"
|
|
156
|
+
|
|
157
|
+
python3 << PYEOF
|
|
158
|
+
import re, os
|
|
159
|
+
|
|
160
|
+
template = """$template"""
|
|
161
|
+
task = """$task"""
|
|
162
|
+
handoff = """$handoff_context"""
|
|
163
|
+
session_dir = "$session_dir"
|
|
164
|
+
|
|
165
|
+
result = template.replace("{{task}}", task).replace("{{handoff}}", handoff)
|
|
166
|
+
|
|
167
|
+
def replace_file_ref(match):
|
|
168
|
+
filename = match.group(1)
|
|
169
|
+
filepath = os.path.join(session_dir, filename)
|
|
170
|
+
if os.path.isfile(filepath):
|
|
171
|
+
with open(filepath, 'r') as f:
|
|
172
|
+
return f.read()
|
|
173
|
+
return f"[File not found: {filename}]"
|
|
174
|
+
|
|
175
|
+
result = re.sub(r'\{\{([a-zA-Z0-9_.-]+)\}\}', replace_file_ref, result)
|
|
176
|
+
print(result)
|
|
177
|
+
PYEOF
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
181
|
+
# Step Execution
|
|
182
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
183
|
+
|
|
184
|
+
# Execute a single workflow step with retry, blend engine, and handoff support
|
|
185
|
+
workflow_step_run() {
|
|
186
|
+
local step_json="$1"
|
|
187
|
+
local task="$2"
|
|
188
|
+
local handoff_context="$3"
|
|
189
|
+
local session_dir="$4"
|
|
190
|
+
|
|
191
|
+
# Parse step fields via single python call for efficiency
|
|
192
|
+
local step_fields
|
|
193
|
+
step_fields=$(echo "$step_json" | python3 -c "
|
|
194
|
+
import json, sys
|
|
195
|
+
s = json.load(sys.stdin)
|
|
196
|
+
print(s['id'])
|
|
197
|
+
print(s.get('label', s['id']))
|
|
198
|
+
print(s.get('agent_hint', 'impl'))
|
|
199
|
+
print(s.get('agent_override', ''))
|
|
200
|
+
print(s.get('input_template', '{{task}}\n\n{{handoff}}'))
|
|
201
|
+
print('---FIELD_SEP---')
|
|
202
|
+
print(s.get('expects', ''))
|
|
203
|
+
print(s.get('retry', 1))
|
|
204
|
+
print(s.get('blend', False))
|
|
205
|
+
print(s.get('independent', False))
|
|
206
|
+
print(s.get('parallel', False))
|
|
207
|
+
print(s.get('output_file', ''))
|
|
208
|
+
print(s.get('action', ''))
|
|
209
|
+
")
|
|
210
|
+
|
|
211
|
+
local step_id step_label agent_hint agent_override input_template
|
|
212
|
+
local expects_signal retry_max use_blend is_independent is_parallel output_file action
|
|
213
|
+
|
|
214
|
+
step_id=$(echo "$step_fields" | sed -n '1p')
|
|
215
|
+
step_label=$(echo "$step_fields" | sed -n '2p')
|
|
216
|
+
agent_hint=$(echo "$step_fields" | sed -n '3p')
|
|
217
|
+
agent_override=$(echo "$step_fields" | sed -n '4p')
|
|
218
|
+
# input_template spans lines 5 to the separator
|
|
219
|
+
input_template=$(echo "$step_fields" | sed -n '5,/---FIELD_SEP---/p' | sed '$d')
|
|
220
|
+
expects_signal=$(echo "$step_fields" | sed -n '/---FIELD_SEP---/,$ p' | sed -n '2p')
|
|
221
|
+
retry_max=$(echo "$step_fields" | sed -n '/---FIELD_SEP---/,$ p' | sed -n '3p')
|
|
222
|
+
use_blend=$(echo "$step_fields" | sed -n '/---FIELD_SEP---/,$ p' | sed -n '4p')
|
|
223
|
+
is_independent=$(echo "$step_fields" | sed -n '/---FIELD_SEP---/,$ p' | sed -n '5p')
|
|
224
|
+
is_parallel=$(echo "$step_fields" | sed -n '/---FIELD_SEP---/,$ p' | sed -n '6p')
|
|
225
|
+
output_file=$(echo "$step_fields" | sed -n '/---FIELD_SEP---/,$ p' | sed -n '7p')
|
|
226
|
+
action=$(echo "$step_fields" | sed -n '/---FIELD_SEP---/,$ p' | sed -n '8p')
|
|
227
|
+
|
|
228
|
+
local prompt
|
|
229
|
+
prompt=$(workflow_apply_template "$input_template" "$task" "$handoff_context" "$session_dir")
|
|
230
|
+
|
|
231
|
+
_workflow_update_step "$step_id" "running"
|
|
232
|
+
|
|
233
|
+
local attempt=0 output="" exit_code=1
|
|
234
|
+
|
|
235
|
+
while [ "$attempt" -lt "$retry_max" ]; do
|
|
236
|
+
attempt=$((attempt + 1))
|
|
237
|
+
echo -e " ${CYAN}[attempt ${attempt}/${retry_max}]${RESET}" >&2
|
|
238
|
+
|
|
239
|
+
# Choose execution method based on step flags
|
|
240
|
+
if [ "$is_independent" = "True" ] && type blend_and_verify &>/dev/null; then
|
|
241
|
+
output=$(blend_and_verify "$prompt" "" "$session_dir/step-${step_id}")
|
|
242
|
+
exit_code=$?
|
|
243
|
+
elif [ "$is_parallel" = "True" ] && type execute_wave_teams &>/dev/null && teams_enabled 2>/dev/null; then
|
|
244
|
+
output=$(blend_and_spawn "$prompt" "" "$session_dir/step-${step_id}")
|
|
245
|
+
exit_code=$?
|
|
246
|
+
elif [ -n "$agent_override" ] && [ "$use_blend" != "True" ]; then
|
|
247
|
+
local agent_path="${HOME}/.claude/agents/${agent_override}.md"
|
|
248
|
+
if [ -f "$agent_path" ]; then
|
|
249
|
+
output=$(echo "$prompt" | claude --agent "$agent_override" -p --permission-mode acceptEdits --output-format text 2>&1)
|
|
250
|
+
exit_code=$?
|
|
251
|
+
else
|
|
252
|
+
output=$(blend_and_spawn "$prompt" "" "$session_dir/step-${step_id}" 2>/dev/null || echo "$prompt" | claude -p --output-format text 2>&1)
|
|
253
|
+
exit_code=$?
|
|
254
|
+
fi
|
|
255
|
+
elif type blend_and_spawn &>/dev/null; then
|
|
256
|
+
output=$(blend_and_spawn "$prompt" "" "$session_dir/step-${step_id}")
|
|
257
|
+
exit_code=$?
|
|
258
|
+
else
|
|
259
|
+
output=$(echo "$prompt" | claude -p --output-format text 2>&1)
|
|
260
|
+
exit_code=$?
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# Check expects signal
|
|
264
|
+
if [ $exit_code -eq 0 ]; then
|
|
265
|
+
if [ -z "$expects_signal" ] || echo "$output" | grep -qi "$expects_signal"; then
|
|
266
|
+
break
|
|
267
|
+
else
|
|
268
|
+
echo -e " ${YELLOW}Expected signal '${expects_signal}' not found, retrying...${RESET}" >&2
|
|
269
|
+
exit_code=1
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
done
|
|
273
|
+
|
|
274
|
+
# Save output file if specified
|
|
275
|
+
[ -n "$output_file" ] && [ $exit_code -eq 0 ] && echo "$output" > "$session_dir/$output_file"
|
|
276
|
+
|
|
277
|
+
# Update handoff chain
|
|
278
|
+
if type handoff_append &>/dev/null && [ -f "$session_dir/handoff.json" ]; then
|
|
279
|
+
handoff_append "$session_dir/handoff.json" "$step_id" "$output"
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Run post-step action
|
|
283
|
+
[ $exit_code -eq 0 ] && [ -n "$action" ] && _workflow_run_action "$action" "$output" "$session_dir"
|
|
284
|
+
|
|
285
|
+
# Update run state
|
|
286
|
+
[ $exit_code -eq 0 ] && _workflow_update_step "$step_id" "done" "$attempt" || _workflow_update_step "$step_id" "failed" "$attempt"
|
|
287
|
+
|
|
288
|
+
echo "$output"
|
|
289
|
+
return $exit_code
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
293
|
+
# State Management
|
|
294
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
295
|
+
|
|
296
|
+
_workflow_update_step() {
|
|
297
|
+
local step_id="$1"
|
|
298
|
+
local status="$2"
|
|
299
|
+
local attempts="${3:-1}"
|
|
300
|
+
|
|
301
|
+
[ ! -f "$WORKFLOW_RUN_FILE" ] && return
|
|
302
|
+
|
|
303
|
+
python3 << PYEOF
|
|
304
|
+
import json
|
|
305
|
+
from datetime import datetime, timezone
|
|
306
|
+
|
|
307
|
+
with open("$WORKFLOW_RUN_FILE", 'r') as f:
|
|
308
|
+
run = json.load(f)
|
|
309
|
+
|
|
310
|
+
step = run["steps"].get("$step_id", {})
|
|
311
|
+
step["status"] = "$status"
|
|
312
|
+
step["attempts"] = $attempts
|
|
313
|
+
|
|
314
|
+
if "$status" == "running":
|
|
315
|
+
step["started"] = datetime.now(timezone.utc).isoformat()
|
|
316
|
+
elif "$status" in ("done", "failed", "skipped"):
|
|
317
|
+
step["completed"] = datetime.now(timezone.utc).isoformat()
|
|
318
|
+
|
|
319
|
+
run["steps"]["$step_id"] = step
|
|
320
|
+
|
|
321
|
+
if "$status" == "done":
|
|
322
|
+
wf_steps = list(run["steps"].keys())
|
|
323
|
+
idx = wf_steps.index("$step_id") if "$step_id" in wf_steps else -1
|
|
324
|
+
if idx >= 0 and idx + 1 < len(wf_steps):
|
|
325
|
+
run["current_step"] = wf_steps[idx + 1]
|
|
326
|
+
else:
|
|
327
|
+
run["current_step"] = None
|
|
328
|
+
run["status"] = "completed"
|
|
329
|
+
elif "$status" == "failed":
|
|
330
|
+
run["status"] = "failed"
|
|
331
|
+
|
|
332
|
+
with open("$WORKFLOW_RUN_FILE", 'w') as f:
|
|
333
|
+
json.dump(run, f, indent=2)
|
|
334
|
+
PYEOF
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
338
|
+
# Post-Step Actions
|
|
339
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
340
|
+
|
|
341
|
+
_workflow_run_action() {
|
|
342
|
+
local action="$1"
|
|
343
|
+
local output="$2"
|
|
344
|
+
local session_dir="$3"
|
|
345
|
+
|
|
346
|
+
case "$action" in
|
|
347
|
+
gh_pr_create)
|
|
348
|
+
echo -e "${CYAN} Action: Create PR${RESET}" >&2
|
|
349
|
+
echo "$output" | grep -qi "pull request\|PR created\|pr/" || \
|
|
350
|
+
echo -e "${YELLOW} PR may not have been created. Check manually.${RESET}" >&2
|
|
351
|
+
;;
|
|
352
|
+
git_commit)
|
|
353
|
+
echo -e "${CYAN} Action: Git commit${RESET}" >&2
|
|
354
|
+
git add -A && git commit -m "workflow: auto-commit from step" 2>/dev/null || true
|
|
355
|
+
;;
|
|
356
|
+
notify)
|
|
357
|
+
type send_notification &>/dev/null && send_notification "Workflow Step Complete" "Step finished"
|
|
358
|
+
;;
|
|
359
|
+
esac
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
363
|
+
# Load orchestration module
|
|
364
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
365
|
+
|
|
366
|
+
_WORKFLOW_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
367
|
+
source "${_WORKFLOW_LIB_DIR}/workflow-run.sh" 2>/dev/null || true
|
|
@@ -47,6 +47,7 @@ source "$SCRIPT_DIR/lib/teams.sh" 2>/dev/null || true # Agent Teams integra
|
|
|
47
47
|
# Stage 2: blend-parallel.sh — Parallel Agent Execution
|
|
48
48
|
# Stage 3: blend-handoff.sh — Structured Handoffs
|
|
49
49
|
# Stage 4: blend-learn.sh — Blend Learning
|
|
50
|
+
source "$SCRIPT_DIR/lib/workflow.sh" 2>/dev/null || true # Workflow engine (multi-agent pipelines)
|
|
50
51
|
source "$SCRIPT_DIR/lib/boot.sh" 2>/dev/null || true # Boot sequence
|
|
51
52
|
source "$SCRIPT_DIR/lib/session-end.sh" 2>/dev/null || true # Session end protocol
|
|
52
53
|
source "$SCRIPT_DIR/lib/tui.sh" 2>/dev/null || true # Interactive TUI
|
|
@@ -769,6 +770,13 @@ usage() {
|
|
|
769
770
|
echo " status Show current loop status"
|
|
770
771
|
echo " reset Reset loop state (keeps history)"
|
|
771
772
|
echo ""
|
|
773
|
+
echo " workflow list List available workflows"
|
|
774
|
+
echo " workflow run <id> \"<task>\" Run a named workflow"
|
|
775
|
+
echo " workflow status Show workflow run progress"
|
|
776
|
+
echo " workflow resume Resume interrupted workflow"
|
|
777
|
+
echo " workflow skip Skip current step and advance"
|
|
778
|
+
echo " workflow install <url|file> Install community workflow"
|
|
779
|
+
echo ""
|
|
772
780
|
echo "Options:"
|
|
773
781
|
echo " --mode MODE Loop mode: auto, interactive, hybrid (default: hybrid)"
|
|
774
782
|
echo " auto - Only pause on explicit checkpoints"
|
|
@@ -792,6 +800,11 @@ usage() {
|
|
|
792
800
|
echo " merlin-loop --max 100 build # Allow up to 100 iterations"
|
|
793
801
|
echo " merlin-loop --teams build # Parallel wave execution (experimental)"
|
|
794
802
|
echo " merlin-loop --afk auto # Run unattended (auto mode)"
|
|
803
|
+
echo ""
|
|
804
|
+
echo " merlin-loop workflow list # See available workflows"
|
|
805
|
+
echo " merlin-loop workflow run feature-dev \"Add OAuth\" # Run a workflow"
|
|
806
|
+
echo " merlin-loop workflow status # Check progress"
|
|
807
|
+
echo " merlin-loop workflow resume # Resume after failure"
|
|
795
808
|
}
|
|
796
809
|
|
|
797
810
|
parse_args() {
|
|
@@ -854,6 +867,13 @@ parse_args() {
|
|
|
854
867
|
COMMAND="$1"
|
|
855
868
|
shift
|
|
856
869
|
;;
|
|
870
|
+
workflow)
|
|
871
|
+
COMMAND="workflow"
|
|
872
|
+
shift
|
|
873
|
+
# Capture remaining args for workflow subcommand
|
|
874
|
+
WORKFLOW_ARGS=("$@")
|
|
875
|
+
break
|
|
876
|
+
;;
|
|
857
877
|
-h|--help)
|
|
858
878
|
usage
|
|
859
879
|
exit 0
|
|
@@ -869,6 +889,7 @@ parse_args() {
|
|
|
869
889
|
|
|
870
890
|
main() {
|
|
871
891
|
COMMAND="${COMMAND:-auto}"
|
|
892
|
+
WORKFLOW_ARGS=()
|
|
872
893
|
|
|
873
894
|
parse_args "$@"
|
|
874
895
|
|
|
@@ -904,6 +925,15 @@ main() {
|
|
|
904
925
|
reset_state
|
|
905
926
|
echo -e "${GREEN}Loop state reset${RESET}"
|
|
906
927
|
;;
|
|
928
|
+
workflow)
|
|
929
|
+
merlin_banner
|
|
930
|
+
if type workflow_dispatch &>/dev/null; then
|
|
931
|
+
workflow_dispatch "${WORKFLOW_ARGS[@]}"
|
|
932
|
+
else
|
|
933
|
+
echo -e "${RED}Workflow engine not available. Check lib/workflow.sh${RESET}"
|
|
934
|
+
exit 1
|
|
935
|
+
fi
|
|
936
|
+
;;
|
|
907
937
|
*)
|
|
908
938
|
usage
|
|
909
939
|
exit 1
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "bug-fix",
|
|
3
|
+
"name": "Bug Fix",
|
|
4
|
+
"description": "Report a bug. Get back a fix with regression test and PR.",
|
|
5
|
+
"version": "1.0",
|
|
6
|
+
"steps": [
|
|
7
|
+
{
|
|
8
|
+
"id": "triage",
|
|
9
|
+
"label": "Triage & Reproduce",
|
|
10
|
+
"agent_hint": "debug",
|
|
11
|
+
"input_template": "Triage this bug report and reproduce it:\n\n{{task}}\n\nSteps:\n1. Understand the reported behavior\n2. Find the relevant code\n3. Reproduce the issue (or confirm from code analysis)\n4. Identify root cause vs symptoms\n5. Assess severity and blast radius\n\nOutput TRIAGE_COMPLETE with root cause analysis.",
|
|
12
|
+
"expects": "TRIAGE_COMPLETE",
|
|
13
|
+
"output_file": "triage.md",
|
|
14
|
+
"retry": 2
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "investigate",
|
|
18
|
+
"label": "Deep Investigation",
|
|
19
|
+
"agent_hint": "debug",
|
|
20
|
+
"input_template": "Deep-dive into the root cause identified in triage:\n\n{{triage.md}}\n\nPrevious context:\n{{handoff}}\n\nTrace the exact code path. Identify all affected branches. Find related bugs that might share the same root cause.\n\nOutput INVESTIGATION_COMPLETE with fix strategy.",
|
|
21
|
+
"expects": "INVESTIGATION_COMPLETE",
|
|
22
|
+
"output_file": "investigation.md",
|
|
23
|
+
"retry": 1
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "fix",
|
|
27
|
+
"label": "Apply Fix",
|
|
28
|
+
"agent_hint": "impl",
|
|
29
|
+
"input_template": "Apply the fix based on investigation:\n\n{{investigation.md}}\n\nPrevious context:\n{{handoff}}\n\nFix the root cause, not just the symptom. Keep changes minimal and focused. Commit atomically.\n\nOutput FIX_COMPLETE when the fix is applied.",
|
|
30
|
+
"expects": "FIX_COMPLETE",
|
|
31
|
+
"retry": 2,
|
|
32
|
+
"blend": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "regression-test",
|
|
36
|
+
"label": "Regression Test",
|
|
37
|
+
"agent_hint": "test",
|
|
38
|
+
"input_template": "Write a regression test that:\n1. Fails WITHOUT the fix (proves the bug existed)\n2. Passes WITH the fix (proves the fix works)\n3. Covers edge cases identified in investigation\n\nContext:\n{{handoff}}\n\nOutput TESTS_PASS when regression test passes.",
|
|
39
|
+
"expects": "TESTS_PASS",
|
|
40
|
+
"retry": 2
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "verify",
|
|
44
|
+
"label": "Independent Verification",
|
|
45
|
+
"agent_hint": "architect",
|
|
46
|
+
"agent_override": "merlin-verifier",
|
|
47
|
+
"input_template": "Verify this bug fix cold. Check:\n- Fix addresses root cause (not just symptom)\n- No regressions introduced\n- Edge cases handled\n- Test coverage adequate\n\nContext:\n{{handoff}}\n\nOutput VERIFIED when review passes.",
|
|
48
|
+
"expects": "VERIFIED",
|
|
49
|
+
"independent": true,
|
|
50
|
+
"retry": 1
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "pr",
|
|
54
|
+
"label": "Pull Request",
|
|
55
|
+
"agent_hint": "impl",
|
|
56
|
+
"input_template": "Create a PR for this bug fix.\n\nContext:\n{{handoff}}\n\nPR should reference the bug, explain root cause, and describe the fix. Include before/after behavior.\n\nOutput PR_CREATED when done.",
|
|
57
|
+
"expects": "PR_CREATED",
|
|
58
|
+
"action": "gh_pr_create",
|
|
59
|
+
"retry": 1
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"on_failure": "pause",
|
|
63
|
+
"on_complete": "notify"
|
|
64
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feature-dev",
|
|
3
|
+
"name": "Feature Development",
|
|
4
|
+
"description": "Drop in a feature request. Get back a tested PR.",
|
|
5
|
+
"version": "1.0",
|
|
6
|
+
"steps": [
|
|
7
|
+
{
|
|
8
|
+
"id": "plan",
|
|
9
|
+
"label": "Planning",
|
|
10
|
+
"agent_hint": "spec",
|
|
11
|
+
"agent_override": "merlin-planner",
|
|
12
|
+
"input_template": "Create a detailed implementation plan for: {{task}}\n\nInclude: file changes, acceptance criteria, edge cases, and testing strategy.\n\nOutput PLAN_COMPLETE when done.",
|
|
13
|
+
"expects": "PLAN_COMPLETE",
|
|
14
|
+
"output_file": "plan.md",
|
|
15
|
+
"retry": 2
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "setup",
|
|
19
|
+
"label": "Environment Setup",
|
|
20
|
+
"agent_hint": "architect",
|
|
21
|
+
"input_template": "Set up environment and scaffolding for this plan:\n\n{{plan.md}}\n\nCreate any new files, install dependencies, set up config.\nHandoff context:\n{{handoff}}\n\nOutput SETUP_COMPLETE when done.",
|
|
22
|
+
"expects": "SETUP_COMPLETE",
|
|
23
|
+
"retry": 1
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "implement",
|
|
27
|
+
"label": "Implementation",
|
|
28
|
+
"agent_hint": "impl",
|
|
29
|
+
"input_template": "Implement according to this plan:\n\n{{plan.md}}\n\nPrevious context:\n{{handoff}}\n\nFollow existing patterns. Keep files under 400 lines. Commit after each logical change.\n\nOutput IMPL_COMPLETE when done.",
|
|
30
|
+
"expects": "IMPL_COMPLETE",
|
|
31
|
+
"retry": 3,
|
|
32
|
+
"blend": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "verify",
|
|
36
|
+
"label": "Independent Verification",
|
|
37
|
+
"agent_hint": "architect",
|
|
38
|
+
"agent_override": "merlin-verifier",
|
|
39
|
+
"input_template": "Review this implementation cold. Do not read the original task. Only evaluate the code quality, correctness, and completeness based on:\n\n{{handoff}}\n\nCheck files changed, look for bugs, missing error handling, security issues.\n\nOutput VERIFIED when review is complete.",
|
|
40
|
+
"expects": "VERIFIED",
|
|
41
|
+
"independent": true,
|
|
42
|
+
"retry": 1
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "test",
|
|
46
|
+
"label": "Testing",
|
|
47
|
+
"agent_hint": "test",
|
|
48
|
+
"input_template": "Write and run tests for the work described in:\n\n{{handoff}}\n\nWrite unit tests for core logic. Write integration tests for API endpoints. Ensure all tests pass.\n\nOutput TESTS_PASS when all tests pass.",
|
|
49
|
+
"expects": "TESTS_PASS",
|
|
50
|
+
"retry": 2
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "pr",
|
|
54
|
+
"label": "Pull Request",
|
|
55
|
+
"agent_hint": "impl",
|
|
56
|
+
"input_template": "Create a PR for the completed work.\n\nContext:\n{{handoff}}\n\nWrite a clear PR title and description summarizing all changes. Use `gh pr create`.\n\nOutput PR_CREATED when the PR is created.",
|
|
57
|
+
"expects": "PR_CREATED",
|
|
58
|
+
"action": "gh_pr_create",
|
|
59
|
+
"retry": 1
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "review",
|
|
63
|
+
"label": "Final Review",
|
|
64
|
+
"agent_hint": "spec",
|
|
65
|
+
"agent_override": "merlin-milestone-auditor",
|
|
66
|
+
"input_template": "Audit the completed work. Verify all acceptance criteria from the original plan are met.\n\nContext:\n{{handoff}}\n\nOriginal task: {{task}}\n\nCheck each criterion against actual code. Report gaps if any.\n\nOutput APPROVED if all criteria met.",
|
|
67
|
+
"expects": "APPROVED",
|
|
68
|
+
"retry": 1
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"on_failure": "pause",
|
|
72
|
+
"on_complete": "notify"
|
|
73
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "refactor",
|
|
3
|
+
"name": "Code Refactor",
|
|
4
|
+
"description": "Analyze, plan, refactor, verify, test, and PR. Clean code pipeline.",
|
|
5
|
+
"version": "1.0",
|
|
6
|
+
"steps": [
|
|
7
|
+
{
|
|
8
|
+
"id": "analyze",
|
|
9
|
+
"label": "Code Analysis",
|
|
10
|
+
"agent_hint": "refactor",
|
|
11
|
+
"input_template": "Analyze the codebase for refactoring opportunities:\n\n{{task}}\n\nLook for:\n- Files over 400 lines\n- Duplicated code patterns\n- God objects / functions doing too much\n- Circular dependencies\n- Dead code\n- Inconsistent patterns\n- Missing abstractions\n\nProduce a detailed analysis with specific file paths and line counts.\n\nOutput ANALYSIS_COMPLETE with findings.",
|
|
12
|
+
"expects": "ANALYSIS_COMPLETE",
|
|
13
|
+
"output_file": "analysis.md",
|
|
14
|
+
"retry": 1
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "plan",
|
|
18
|
+
"label": "Refactor Plan",
|
|
19
|
+
"agent_hint": "architect",
|
|
20
|
+
"input_template": "Create a safe refactoring plan based on analysis:\n\n{{analysis.md}}\n\nPrevious context:\n{{handoff}}\n\nFor each refactoring:\n1. What changes (specific files and functions)\n2. Why it improves the code\n3. Risk level (low/medium/high)\n4. Order (safest first, riskiest last)\n\nEnsure each step is atomic and independently testable.\n\nOutput PLAN_COMPLETE with ordered refactoring steps.",
|
|
21
|
+
"expects": "PLAN_COMPLETE",
|
|
22
|
+
"output_file": "refactor-plan.md",
|
|
23
|
+
"retry": 1
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "refactor",
|
|
27
|
+
"label": "Apply Refactoring",
|
|
28
|
+
"agent_hint": "refactor",
|
|
29
|
+
"input_template": "Execute the refactoring plan:\n\n{{refactor-plan.md}}\n\nPrevious context:\n{{handoff}}\n\nRules:\n- One atomic commit per refactoring step\n- Run existing tests after each step\n- If tests break, fix before moving on\n- Keep files under 400 lines\n- Preserve all existing behavior\n\nOutput REFACTOR_COMPLETE when all steps done.",
|
|
30
|
+
"expects": "REFACTOR_COMPLETE",
|
|
31
|
+
"retry": 3,
|
|
32
|
+
"blend": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "verify",
|
|
36
|
+
"label": "Behavior Verification",
|
|
37
|
+
"agent_hint": "architect",
|
|
38
|
+
"agent_override": "merlin-verifier",
|
|
39
|
+
"input_template": "Verify that refactoring preserved all behavior.\n\nReview the changes cold — do not read the refactor plan.\nCheck:\n- No behavior changes (only structural)\n- All public APIs unchanged\n- No missing imports or broken references\n- Tests still pass\n\nContext:\n{{handoff}}\n\nOutput VERIFIED if behavior is preserved.",
|
|
40
|
+
"expects": "VERIFIED",
|
|
41
|
+
"independent": true,
|
|
42
|
+
"retry": 1
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "test",
|
|
46
|
+
"label": "Test Suite",
|
|
47
|
+
"agent_hint": "test",
|
|
48
|
+
"input_template": "Run the full test suite and add tests for any untested refactored code:\n\nContext:\n{{handoff}}\n\n1. Run all existing tests — they MUST pass\n2. Add tests for newly extracted functions/modules\n3. Verify test coverage didn't decrease\n\nOutput TESTS_PASS when all tests pass.",
|
|
49
|
+
"expects": "TESTS_PASS",
|
|
50
|
+
"retry": 2
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "pr",
|
|
54
|
+
"label": "Refactor PR",
|
|
55
|
+
"agent_hint": "impl",
|
|
56
|
+
"input_template": "Create a PR for the refactoring work.\n\nContext:\n{{handoff}}\n\nPR should explain:\n- What was refactored and why\n- Key structural changes\n- That all tests pass and behavior is preserved\n\nOutput PR_CREATED when done.",
|
|
57
|
+
"expects": "PR_CREATED",
|
|
58
|
+
"action": "gh_pr_create",
|
|
59
|
+
"retry": 1
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"on_failure": "pause",
|
|
63
|
+
"on_complete": "notify"
|
|
64
|
+
}
|