eagle-mem 4.7.1 → 4.8.1

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.
@@ -0,0 +1,1268 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Orchestrator
4
+ # Durable orchestrator/worker lane tracking for Claude Code and Codex.
5
+ # ═══════════════════════════════════════════════════════════
6
+ set -euo pipefail
7
+
8
+ SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ LIB_DIR="$SCRIPTS_DIR/../lib"
10
+
11
+ . "$SCRIPTS_DIR/style.sh"
12
+ . "$LIB_DIR/common.sh"
13
+ . "$LIB_DIR/db.sh"
14
+ . "$LIB_DIR/provider.sh"
15
+
16
+ project=""
17
+ json_output=false
18
+ name="main"
19
+ agent=""
20
+ agent_explicit=false
21
+ action="status"
22
+ action_explicit=false
23
+ args=()
24
+
25
+ show_help() {
26
+ echo -e " ${BOLD}eagle-mem orchestrate${RESET} — Coordinate worker lanes"
27
+ echo ""
28
+ echo -e " ${BOLD}Usage:${RESET}"
29
+ echo -e " eagle-mem orchestrate ${DIM}# show active orchestration${RESET}"
30
+ echo -e " eagle-mem orchestrate ${CYAN}init${RESET} <goal> ${DIM}# create/update orchestration${RESET}"
31
+ echo -e " eagle-mem orchestrate ${CYAN}lane add${RESET} <key> ${DIM}# add worker lane${RESET}"
32
+ echo -e " eagle-mem orchestrate ${CYAN}lane start${RESET} <key> ${DIM}# mark lane in progress${RESET}"
33
+ echo -e " eagle-mem orchestrate ${CYAN}lane block${RESET} <key> ${DIM}# mark lane blocked${RESET}"
34
+ echo -e " eagle-mem orchestrate ${CYAN}lane complete${RESET} <key> ${DIM}# mark lane complete${RESET}"
35
+ echo -e " eagle-mem orchestrate ${CYAN}lane cancel${RESET} <key> ${DIM}# cancel lane${RESET}"
36
+ echo -e " eagle-mem orchestrate ${CYAN}spawn${RESET} <key> ${DIM}# create worktree + launch worker${RESET}"
37
+ echo -e " eagle-mem orchestrate ${CYAN}sync${RESET} [key] ${DIM}# reconcile worker process status${RESET}"
38
+ echo -e " eagle-mem orchestrate ${CYAN}complete${RESET} ${DIM}# mark orchestration complete${RESET}"
39
+ echo -e " eagle-mem orchestrate ${CYAN}cancel${RESET} ${DIM}# cancel orchestration${RESET}"
40
+ echo -e " eagle-mem orchestrate ${CYAN}handoff${RESET} ${DIM}# print markdown handoff${RESET}"
41
+ echo ""
42
+ echo -e " ${BOLD}Options:${RESET}"
43
+ echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current dir)"
44
+ echo -e " ${CYAN}--name${RESET} <name> Orchestration name (default: main)"
45
+ echo -e " ${CYAN}--agent${RESET} <name> Worker agent: codex or claude-code"
46
+ echo -e " ${CYAN}--title${RESET} <text> Lane title"
47
+ echo -e " ${CYAN}--desc${RESET} <text> Lane description"
48
+ echo -e " ${CYAN}--worktree${RESET} <path> Suggested lane worktree"
49
+ echo -e " ${CYAN}--validate${RESET} <command> Validation command"
50
+ echo -e " ${CYAN}--notes${RESET} <text> Status note"
51
+ echo -e " ${CYAN}--write${RESET} <path> Write handoff markdown to a file"
52
+ echo -e " ${CYAN}--no-launch${RESET} Prepare worktree without starting worker"
53
+ echo -e " ${CYAN}--no-worktree${RESET} Run worker in current repo instead"
54
+ echo -e " ${CYAN}--foreground${RESET} Run worker synchronously"
55
+ echo -e " ${CYAN}--dry-run${RESET} Print launch plan only"
56
+ echo -e " ${CYAN}-j, --json${RESET} Output JSON where supported"
57
+ echo ""
58
+ echo -e " ${BOLD}Example:${RESET}"
59
+ echo -e " ${DIM}\$${RESET} eagle-mem orchestrate init \"Ship auth cleanup\""
60
+ echo -e " ${DIM}\$${RESET} eagle-mem orchestrate lane add api --agent codex --desc \"Routes + tests\" --validate \"npm test\""
61
+ echo -e " ${DIM}\$${RESET} eagle-mem orchestrate spawn api"
62
+ echo ""
63
+ exit 0
64
+ }
65
+
66
+ while [ $# -gt 0 ]; do
67
+ case "$1" in
68
+ -p|--project) project="$2"; shift 2 ;;
69
+ --name) name="$2"; shift 2 ;;
70
+ --agent) agent="$2"; agent_explicit=true; shift 2 ;;
71
+ -j|--json) json_output=true; shift ;;
72
+ --help|-h) show_help ;;
73
+ *)
74
+ if [ "$action_explicit" = false ]; then
75
+ action="$1"
76
+ action_explicit=true
77
+ else
78
+ args+=("$1")
79
+ fi
80
+ shift
81
+ ;;
82
+ esac
83
+ done
84
+
85
+ [ -z "$project" ] && project=$(eagle_project_from_cwd "$(pwd)")
86
+ [ -z "$project" ] && { eagle_err "Cannot determine project. Use --project <name>."; exit 1; }
87
+
88
+ if [ "$json_output" = true ]; then
89
+ eagle_ensure_db >/dev/null
90
+ else
91
+ eagle_ensure_db
92
+ fi
93
+
94
+ project_sql=$(eagle_sql_escape "$project")
95
+ name_sql=$(eagle_sql_escape "$name")
96
+ active_agent=$(eagle_agent_source)
97
+
98
+ orchestration_id() {
99
+ eagle_db "SELECT id FROM orchestrations
100
+ WHERE project = '$project_sql' AND name = '$name_sql'
101
+ ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, updated_at DESC
102
+ LIMIT 1;"
103
+ }
104
+
105
+ require_orchestration_id() {
106
+ local oid
107
+ oid=$(orchestration_id)
108
+ if [ -z "$oid" ]; then
109
+ eagle_err "No orchestration found for '$project' ($name). Run: eagle-mem orchestrate init <goal>"
110
+ exit 1
111
+ fi
112
+ printf '%s\n' "$oid"
113
+ }
114
+
115
+ active_orchestration_id() {
116
+ eagle_db "SELECT id FROM orchestrations
117
+ WHERE project = '$project_sql' AND name = '$name_sql' AND status = 'active'
118
+ ORDER BY updated_at DESC
119
+ LIMIT 1;"
120
+ }
121
+
122
+ require_active_orchestration_id() {
123
+ local oid
124
+ oid=$(active_orchestration_id)
125
+ if [ -z "$oid" ]; then
126
+ eagle_err "No active orchestration found for '$project' ($name). Run: eagle-mem orchestrate init <goal>"
127
+ exit 1
128
+ fi
129
+ printf '%s\n' "$oid"
130
+ }
131
+
132
+ lane_source_task_id() {
133
+ local lane_key="$1"
134
+ printf 'lane-%s-%s\n' "$name" "$lane_key"
135
+ }
136
+
137
+ lane_file_path() {
138
+ local lane_key="$1"
139
+ printf 'orchestration-lane://%s/%s/%s\n' "$project" "$name" "$lane_key"
140
+ }
141
+
142
+ orchestrate_slug() {
143
+ local value="${1:-lane}"
144
+ value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')
145
+ [ -z "$value" ] && value="lane"
146
+ printf '%s\n' "$value"
147
+ }
148
+
149
+ orchestrate_project_hash() {
150
+ printf '%s' "$project" | eagle_sha256_stream | cut -c1-10
151
+ }
152
+
153
+ orchestrate_run_key() {
154
+ local oid="${1:-}"
155
+ local key
156
+ [ -z "$oid" ] && oid=$(orchestration_id)
157
+ if [ -n "$oid" ]; then
158
+ key=$(eagle_db "SELECT run_key FROM orchestrations WHERE id = $oid LIMIT 1;")
159
+ else
160
+ key=""
161
+ fi
162
+ [ -z "$key" ] && key="r${oid:-new}"
163
+ orchestrate_slug "$key"
164
+ }
165
+
166
+ orchestrate_default_worker_agent() {
167
+ local route
168
+ route=$(eagle_config_get "orchestration" "route" "opposite")
169
+ case "$route" in
170
+ codex|openai-codex) echo "codex" ;;
171
+ claude|claude-code|cloud-code) echo "claude-code" ;;
172
+ current) echo "$active_agent" ;;
173
+ opposite|*)
174
+ case "$active_agent" in
175
+ codex) echo "claude-code" ;;
176
+ *) echo "codex" ;;
177
+ esac
178
+ ;;
179
+ esac
180
+ }
181
+
182
+ orchestrate_normalize_agent() {
183
+ local value="${1:-}"
184
+ case "$value" in
185
+ "") orchestrate_default_worker_agent ;;
186
+ codex|openai-codex) echo "codex" ;;
187
+ claude|claude-code|cloud-code) echo "claude-code" ;;
188
+ *) return 1 ;;
189
+ esac
190
+ }
191
+
192
+ orchestrate_worker_model() {
193
+ case "$1" in
194
+ codex) eagle_config_get "orchestration" "codex_worker_model" "gpt-5.5" ;;
195
+ *) eagle_config_get "orchestration" "claude_worker_model" "claude-opus-4-7" ;;
196
+ esac
197
+ }
198
+
199
+ orchestrate_worker_effort() {
200
+ case "$1" in
201
+ codex) eagle_config_get "orchestration" "codex_worker_effort" "xhigh" ;;
202
+ *) eagle_config_get "orchestration" "claude_worker_effort" "xhigh" ;;
203
+ esac
204
+ }
205
+
206
+ orchestrate_require_worker_cli() {
207
+ case "$1" in
208
+ codex)
209
+ command -v codex >/dev/null 2>&1 || { eagle_err "Codex worker requested, but 'codex' was not found on PATH"; exit 1; }
210
+ ;;
211
+ claude-code)
212
+ command -v claude >/dev/null 2>&1 || { eagle_err "Claude Code worker requested, but 'claude' was not found on PATH"; exit 1; }
213
+ ;;
214
+ esac
215
+ }
216
+
217
+ orchestrate_repo_root() {
218
+ git -C "$(pwd)" rev-parse --show-toplevel 2>/dev/null
219
+ }
220
+
221
+ orchestrate_branch_name() {
222
+ local lane_key="$1"
223
+ printf 'eagle/%s/%s/%s\n' "$(orchestrate_slug "$name")" "$(orchestrate_run_key)" "$(orchestrate_slug "$lane_key")"
224
+ }
225
+
226
+ orchestrate_default_worktree_path() {
227
+ local repo_root="$1" lane_key="$2"
228
+ local configured_root repo_name root
229
+ configured_root=$(eagle_config_get "orchestration" "worktree_root" "")
230
+ repo_name=$(basename "$repo_root")
231
+ if [ -n "$configured_root" ]; then
232
+ case "$configured_root" in
233
+ /*) root="$configured_root" ;;
234
+ *) root="$(dirname "$repo_root")/$configured_root" ;;
235
+ esac
236
+ else
237
+ root="$(dirname "$repo_root")/.eagle-worktrees/$repo_name"
238
+ fi
239
+ printf '%s/%s-%s-%s\n' "$root" "$(orchestrate_slug "$name")" "$(orchestrate_run_key)" "$(orchestrate_slug "$lane_key")"
240
+ }
241
+
242
+ orchestrate_run_dir() {
243
+ local lane_key="$1"
244
+ local attempt="${2:-current}"
245
+ printf '%s/orchestrations/%s-%s/%s/%s/%s/%s\n' \
246
+ "$EAGLE_MEM_DIR" \
247
+ "$(orchestrate_slug "$project")" \
248
+ "$(orchestrate_project_hash)" \
249
+ "$(orchestrate_slug "$name")" \
250
+ "$(orchestrate_run_key)" \
251
+ "$(orchestrate_slug "$lane_key")" \
252
+ "$(orchestrate_slug "$attempt")"
253
+ }
254
+
255
+ orchestrate_lane_json() {
256
+ local oid="$1" lane_key="$2" key_sql
257
+ key_sql=$(eagle_sql_escape "$lane_key")
258
+ eagle_db_json "SELECT id, lane_key, title, description, agent, worktree_path, validation, status, notes,
259
+ branch_name, worker_agent, worker_model, worker_effort, worker_pid,
260
+ worker_log_path, worker_exit_path, worker_prompt_path, worker_command,
261
+ worker_started_at, worker_finished_at
262
+ FROM orchestration_lanes
263
+ WHERE orchestration_id = $oid AND lane_key = '$key_sql'
264
+ LIMIT 1;"
265
+ }
266
+
267
+ orchestrate_shell_join() {
268
+ local out="" arg
269
+ for arg in "$@"; do
270
+ if [ -n "$out" ]; then out+=" "; fi
271
+ out+="$(printf '%q' "$arg")"
272
+ done
273
+ printf '%s\n' "$out"
274
+ }
275
+
276
+ orchestrate_prepare_prompt() {
277
+ local prompt_file="$1" lane_key="$2" lane_title="$3" lane_desc="$4" lane_validation="$5" worker_agent="$6" worker_model="$7" worker_effort="$8" worktree="$9" branch="${10}" goal="${11}"
278
+ cat > "$prompt_file" <<PROMPT
279
+ You are an Eagle Mem worker agent.
280
+
281
+ Project: $project
282
+ Orchestration: $name
283
+ Goal: $goal
284
+ Lane: $lane_key — $lane_title
285
+ Assigned worker: $(eagle_agent_label "$worker_agent")
286
+ Model: $worker_model
287
+ Reasoning effort: $worker_effort
288
+ Worktree: $worktree
289
+ Branch: $branch
290
+
291
+ Lane scope:
292
+ $lane_desc
293
+
294
+ Validation command:
295
+ $lane_validation
296
+
297
+ Rules:
298
+ - Work only inside this lane and this worktree.
299
+ - Do not revert or overwrite work from other lanes.
300
+ - Read Eagle Mem recall if hooks provide it, but keep outputs concise.
301
+ - If blocked, run: eagle-mem orchestrate lane --project "$project" --name "$name" block "$lane_key" --notes "<concrete blocker>"
302
+ - If you finish manually before the wrapper updates status, run: eagle-mem orchestrate lane --project "$project" --name "$name" complete "$lane_key" --notes "<validation result>"
303
+ - Run the validation command when one is provided and it is safe for the lane.
304
+ - Before final response, emit an <eagle-summary> with request, completed, learned, decisions, gotchas, next_steps, key_files, files_read, files_modified, affected_features, verified_features, and regression_risks.
305
+ PROMPT
306
+ }
307
+
308
+ orchestrate_prepare_worktree() {
309
+ local lane_key="$1" lane_worktree="$2" no_worktree="$3" dry_run="${4:-false}"
310
+ local repo_root branch worktree configured repo_common worktree_common worktree_branch
311
+ repo_root=$(orchestrate_repo_root)
312
+ [ -z "$repo_root" ] && { eagle_err "Orchestration workers require a git repository."; exit 1; }
313
+
314
+ branch=$(orchestrate_branch_name "$lane_key")
315
+ if [ "$no_worktree" = true ] || [ "$(eagle_config_get "orchestration" "auto_worktree" "true")" = "false" ]; then
316
+ worktree="$repo_root"
317
+ elif [ -n "$lane_worktree" ]; then
318
+ case "$lane_worktree" in
319
+ /*) worktree="$lane_worktree" ;;
320
+ *) worktree="$repo_root/$lane_worktree" ;;
321
+ esac
322
+ else
323
+ worktree=$(orchestrate_default_worktree_path "$repo_root" "$lane_key")
324
+ fi
325
+
326
+ if [ "$dry_run" = true ]; then
327
+ :
328
+ elif [ "$worktree" != "$repo_root" ]; then
329
+ if [ -e "$worktree" ]; then
330
+ if [ ! -d "$worktree/.git" ] && [ ! -f "$worktree/.git" ]; then
331
+ eagle_err "Worktree path exists but is not a git worktree: $worktree"
332
+ exit 1
333
+ fi
334
+ repo_common=$(cd "$repo_root" && { git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || git rev-parse --git-common-dir 2>/dev/null; })
335
+ worktree_common=$(cd "$worktree" && { git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || git rev-parse --git-common-dir 2>/dev/null; })
336
+ case "$repo_common" in /*) ;; *) repo_common="$repo_root/$repo_common" ;; esac
337
+ case "$worktree_common" in /*) ;; *) worktree_common="$worktree/$worktree_common" ;; esac
338
+ if [ -z "$repo_common" ] || [ "$repo_common" != "$worktree_common" ]; then
339
+ eagle_err "Worktree path belongs to a different git repository: $worktree"
340
+ exit 1
341
+ fi
342
+ worktree_branch=$(git -C "$worktree" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
343
+ if [ "$worktree_branch" != "$branch" ]; then
344
+ eagle_err "Worktree path is on '$worktree_branch', expected '$branch': $worktree"
345
+ exit 1
346
+ fi
347
+ else
348
+ mkdir -p "$(dirname "$worktree")"
349
+ if git -C "$repo_root" show-ref --verify --quiet "refs/heads/$branch"; then
350
+ if ! git -C "$repo_root" worktree add -q "$worktree" "$branch" >/dev/null; then
351
+ eagle_err "Failed to create worktree for existing branch '$branch': $worktree"
352
+ exit 1
353
+ fi
354
+ else
355
+ if ! git -C "$repo_root" worktree add -q -b "$branch" "$worktree" HEAD >/dev/null; then
356
+ eagle_err "Failed to create worktree branch '$branch': $worktree"
357
+ exit 1
358
+ fi
359
+ fi
360
+ fi
361
+ else
362
+ branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
363
+ fi
364
+
365
+ printf '%s|%s|%s\n' "$repo_root" "$worktree" "$branch"
366
+ }
367
+
368
+ orchestrate_init() {
369
+ local goal="${args[*]:-}"
370
+ [ -z "$goal" ] && { eagle_err "Usage: eagle-mem orchestrate init <goal>"; exit 1; }
371
+
372
+ local baseline=""
373
+ baseline=$(git -C "$(pwd)" rev-parse --short HEAD 2>/dev/null || true)
374
+
375
+ local existing existing_id existing_status run_key goal_sql baseline_sql run_key_sql
376
+ existing=$(eagle_db "SELECT id || '|' || status FROM orchestrations
377
+ WHERE project = '$project_sql' AND name = '$name_sql'
378
+ LIMIT 1;")
379
+ IFS='|' read -r existing_id existing_status <<< "$existing"
380
+
381
+ if [ "$existing_status" = "completed" ] || [ "$existing_status" = "cancelled" ]; then
382
+ eagle_db_pipe <<SQL >/dev/null
383
+ DELETE FROM agent_tasks
384
+ WHERE project = '$project_sql'
385
+ AND source_session_id = 'orchestration'
386
+ AND file_path IN (
387
+ SELECT 'orchestration-lane://' || l.project || '/' || o.name || '/' || l.lane_key
388
+ FROM orchestration_lanes l
389
+ JOIN orchestrations o ON o.id = l.orchestration_id
390
+ WHERE l.orchestration_id = $existing_id
391
+ );
392
+
393
+ DELETE FROM orchestration_lanes
394
+ WHERE orchestration_id = $existing_id;
395
+ SQL
396
+ fi
397
+
398
+ run_key="r$(date -u +%Y%m%d%H%M%S)-$$"
399
+ goal_sql=$(eagle_sql_escape "$goal")
400
+ baseline_sql=$(eagle_sql_escape "$baseline")
401
+ run_key_sql=$(eagle_sql_escape "$run_key")
402
+
403
+ eagle_db_pipe <<SQL >/dev/null
404
+ INSERT INTO orchestrations (project, name, goal, status, baseline_ref, run_key)
405
+ VALUES ('$project_sql', '$name_sql', '$goal_sql', 'active', '$baseline_sql', '$run_key_sql')
406
+ ON CONFLICT(project, name) DO UPDATE SET
407
+ goal = excluded.goal,
408
+ status = 'active',
409
+ baseline_ref = COALESCE(NULLIF(excluded.baseline_ref, ''), orchestrations.baseline_ref),
410
+ run_key = CASE
411
+ WHEN orchestrations.status IN ('completed', 'cancelled')
412
+ OR orchestrations.run_key IS NULL
413
+ OR orchestrations.run_key = ''
414
+ THEN excluded.run_key
415
+ ELSE orchestrations.run_key
416
+ END,
417
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');
418
+ SQL
419
+
420
+ if [ "$json_output" = true ]; then
421
+ jq -nc --arg project "$project" --arg name "$name" --arg goal "$goal" --arg baseline "$baseline" \
422
+ '{project:$project,name:$name,goal:$goal,baseline_ref:$baseline,status:"active"}'
423
+ else
424
+ eagle_ok "Orchestration '$name' active"
425
+ [ -n "$baseline" ] && eagle_kv "Baseline:" "$baseline"
426
+ fi
427
+ }
428
+
429
+ parse_lane_options() {
430
+ lane_title=""
431
+ lane_desc=""
432
+ lane_worktree=""
433
+ lane_validation=""
434
+ lane_notes=""
435
+
436
+ local parsed=()
437
+ local i=0
438
+ while [ "$i" -lt "${#args[@]}" ]; do
439
+ case "${args[$i]}" in
440
+ --title)
441
+ i=$((i + 1)); lane_title="${args[$i]:-}" ;;
442
+ --desc|-d)
443
+ i=$((i + 1)); lane_desc="${args[$i]:-}" ;;
444
+ --worktree)
445
+ i=$((i + 1)); lane_worktree="${args[$i]:-}" ;;
446
+ --validate)
447
+ i=$((i + 1)); lane_validation="${args[$i]:-}" ;;
448
+ --notes)
449
+ i=$((i + 1)); lane_notes="${args[$i]:-}" ;;
450
+ *)
451
+ parsed+=("${args[$i]}") ;;
452
+ esac
453
+ i=$((i + 1))
454
+ done
455
+ args=("${parsed[@]}")
456
+ }
457
+
458
+ lane_add() {
459
+ parse_lane_options
460
+ local key="${args[0]:-}"
461
+ [ -z "$key" ] && { eagle_err "Usage: eagle-mem orchestrate lane add <key> [--desc <text>]"; exit 1; }
462
+ case "$key" in *[!A-Za-z0-9._-]*) eagle_err "Lane key may contain only letters, numbers, dot, underscore, or dash"; exit 1 ;; esac
463
+
464
+ local oid raw_agent
465
+ oid=$(require_active_orchestration_id)
466
+ [ -z "$lane_title" ] && lane_title="$key"
467
+
468
+ local key_slug existing_keys existing_key
469
+ key_slug=$(orchestrate_slug "$key")
470
+ existing_keys=$(eagle_db "SELECT lane_key FROM orchestration_lanes WHERE orchestration_id = $oid;")
471
+ while IFS= read -r existing_key; do
472
+ [ -z "$existing_key" ] && continue
473
+ [ "$existing_key" = "$key" ] && continue
474
+ if [ "$(orchestrate_slug "$existing_key")" = "$key_slug" ]; then
475
+ eagle_err "Lane key '$key' collides with existing lane '$existing_key' after normalization."
476
+ exit 1
477
+ fi
478
+ done <<< "$existing_keys"
479
+ if ! git check-ref-format --branch "$(orchestrate_branch_name "$key")" >/dev/null 2>&1; then
480
+ eagle_err "Lane key '$key' cannot be used as a git worker branch. Use letters, numbers, dashes, or underscores without leading/trailing dots."
481
+ exit 1
482
+ fi
483
+
484
+ if [ -z "$agent" ] || [ "$agent_explicit" = false ]; then
485
+ agent=$(orchestrate_default_worker_agent)
486
+ else
487
+ raw_agent="$agent"
488
+ if ! agent=$(orchestrate_normalize_agent "$agent"); then
489
+ eagle_err "Invalid worker agent: $raw_agent. Use codex or claude-code."
490
+ exit 1
491
+ fi
492
+ fi
493
+
494
+ local source_task_id file_path content_hash
495
+ source_task_id=$(lane_source_task_id "$key")
496
+ file_path=$(lane_file_path "$key")
497
+ content_hash=$(printf '%s|%s|%s|%s|%s' "$key" "$lane_title" "$lane_desc" "$agent" "$lane_validation" | eagle_sha256_stream)
498
+
499
+ local key_sql title_sql desc_sql agent_sql worktree_sql validation_sql task_sql fp_sql hash_sql
500
+ key_sql=$(eagle_sql_escape "$key")
501
+ title_sql=$(eagle_sql_escape "$lane_title")
502
+ desc_sql=$(eagle_sql_escape "$lane_desc")
503
+ agent_sql=$(eagle_sql_escape "$agent")
504
+ worktree_sql=$(eagle_sql_escape "$lane_worktree")
505
+ validation_sql=$(eagle_sql_escape "$lane_validation")
506
+ task_sql=$(eagle_sql_escape "$source_task_id")
507
+ fp_sql=$(eagle_sql_escape "$file_path")
508
+ hash_sql=$(eagle_sql_escape "$content_hash")
509
+
510
+ eagle_db_pipe <<SQL >/dev/null
511
+ INSERT INTO orchestration_lanes (orchestration_id, project, lane_key, title, description, agent, worktree_path, validation, status, source_task_id)
512
+ VALUES ($oid, '$project_sql', '$key_sql', '$title_sql', '$desc_sql', '$agent_sql', '$worktree_sql', '$validation_sql', 'pending', '$task_sql')
513
+ ON CONFLICT(orchestration_id, lane_key) DO UPDATE SET
514
+ orchestration_id = excluded.orchestration_id,
515
+ title = excluded.title,
516
+ description = excluded.description,
517
+ agent = excluded.agent,
518
+ worktree_path = excluded.worktree_path,
519
+ validation = excluded.validation,
520
+ source_task_id = excluded.source_task_id,
521
+ status = orchestration_lanes.status,
522
+ notes = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.notes END,
523
+ branch_name = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.branch_name END,
524
+ worker_agent = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_agent END,
525
+ worker_model = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_model END,
526
+ worker_effort = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_effort END,
527
+ worker_pid = CASE WHEN orchestration_lanes.status = 'in_progress' THEN orchestration_lanes.worker_pid ELSE NULL END,
528
+ worker_log_path = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_log_path END,
529
+ worker_exit_path = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_exit_path END,
530
+ worker_prompt_path = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_prompt_path END,
531
+ worker_command = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_command END,
532
+ worker_started_at = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_started_at END,
533
+ worker_finished_at = CASE WHEN orchestration_lanes.status = 'pending' THEN NULL ELSE orchestration_lanes.worker_finished_at END,
534
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');
535
+
536
+ INSERT INTO agent_tasks (project, source_session_id, source_task_id, file_path, subject, description, active_form, status, blocks, blocked_by, content_hash, origin_agent)
537
+ VALUES ('$project_sql', 'orchestration', '$task_sql', '$fp_sql', '$title_sql', '$desc_sql', '$validation_sql', 'pending', '[]', '[]', '$hash_sql', '$agent_sql')
538
+ ON CONFLICT(file_path) DO UPDATE SET
539
+ subject = excluded.subject,
540
+ description = excluded.description,
541
+ active_form = excluded.active_form,
542
+ status = CASE
543
+ WHEN (SELECT status FROM orchestration_lanes WHERE orchestration_id = $oid AND lane_key = '$key_sql') = 'in_progress' THEN 'in_progress'
544
+ WHEN (SELECT status FROM orchestration_lanes WHERE orchestration_id = $oid AND lane_key = '$key_sql') = 'completed' THEN 'completed'
545
+ WHEN (SELECT status FROM orchestration_lanes WHERE orchestration_id = $oid AND lane_key = '$key_sql') = 'cancelled' THEN 'cancelled'
546
+ ELSE 'pending'
547
+ END,
548
+ content_hash = excluded.content_hash,
549
+ origin_agent = excluded.origin_agent,
550
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');
551
+
552
+ UPDATE orchestrations
553
+ SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
554
+ WHERE id = $oid;
555
+ SQL
556
+
557
+ local db_status
558
+ db_status=$(eagle_db "SELECT status FROM orchestration_lanes
559
+ WHERE orchestration_id = $oid
560
+ AND lane_key = '$key_sql'
561
+ LIMIT 1;")
562
+ db_status=${db_status:-pending}
563
+
564
+ if [ "$json_output" = true ]; then
565
+ jq -nc --arg key "$key" --arg title "$lane_title" --arg agent "$agent" --arg status "$db_status" --arg task "$source_task_id" \
566
+ '{lane_key:$key,title:$title,agent:$agent,status:$status,source_task_id:$task}'
567
+ else
568
+ eagle_ok "Lane '$key' added for $(eagle_agent_label "$agent")"
569
+ fi
570
+ }
571
+
572
+ lane_set_status() {
573
+ parse_lane_options
574
+ local status="$1"
575
+ local key="${args[0]:-}"
576
+ [ -z "$key" ] && { eagle_err "Usage: eagle-mem orchestrate lane $action <key>"; exit 1; }
577
+
578
+ local key_sql status_sql notes_sql task_status
579
+ key_sql=$(eagle_sql_escape "$key")
580
+ status_sql=$(eagle_sql_escape "$status")
581
+ notes_sql=$(eagle_sql_escape "$lane_notes")
582
+ case "$status" in
583
+ in_progress) task_status="in_progress" ;;
584
+ completed) task_status="completed" ;;
585
+ cancelled) task_status="cancelled" ;;
586
+ blocked) task_status="pending" ;;
587
+ *) task_status="pending" ;;
588
+ esac
589
+
590
+ local task_file_path fp_sql changed current_status
591
+ local oid
592
+ oid=$(require_active_orchestration_id)
593
+ task_file_path=$(lane_file_path "$key")
594
+ fp_sql=$(eagle_sql_escape "$task_file_path")
595
+
596
+ current_status=$(eagle_db "SELECT status FROM orchestration_lanes
597
+ WHERE project = '$project_sql'
598
+ AND lane_key = '$key_sql'
599
+ AND orchestration_id = $oid
600
+ LIMIT 1;")
601
+ if [ "$current_status" = "cancelled" ] && [ "$status" != "cancelled" ]; then
602
+ eagle_dim "Lane '$key' is cancelled; leaving status unchanged."
603
+ return 0
604
+ fi
605
+ if [ "$current_status" = "blocked" ] && [ "$status" = "completed" ]; then
606
+ eagle_dim "Lane '$key' is blocked; leaving blocker unchanged. Run lane start before completing it."
607
+ return 0
608
+ fi
609
+
610
+ changed=$(eagle_db_pipe <<SQL
611
+ UPDATE orchestration_lanes
612
+ SET status = '$status_sql',
613
+ notes = CASE WHEN '$notes_sql' != '' THEN '$notes_sql' ELSE notes END,
614
+ worker_pid = CASE
615
+ WHEN '$status_sql' IN ('in_progress', 'blocked', 'completed', 'cancelled') THEN NULL
616
+ ELSE worker_pid
617
+ END,
618
+ worker_exit_path = CASE WHEN '$status_sql' = 'in_progress' THEN NULL ELSE worker_exit_path END,
619
+ worker_started_at = CASE WHEN '$status_sql' = 'in_progress' THEN NULL ELSE worker_started_at END,
620
+ worker_finished_at = CASE
621
+ WHEN '$status_sql' = 'in_progress' THEN NULL
622
+ WHEN '$status_sql' IN ('blocked', 'completed', 'cancelled') THEN strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
623
+ ELSE worker_finished_at
624
+ END,
625
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
626
+ WHERE project = '$project_sql'
627
+ AND lane_key = '$key_sql'
628
+ AND orchestration_id = $oid
629
+ AND (status != 'cancelled' OR '$status_sql' = 'cancelled')
630
+ AND NOT (status = 'blocked' AND '$status_sql' = 'completed');
631
+ SELECT changes();
632
+ SQL
633
+ )
634
+
635
+ if [ "${changed:-0}" -gt 0 ] 2>/dev/null; then
636
+ eagle_db_pipe <<SQL >/dev/null
637
+ UPDATE agent_tasks
638
+ SET status = '$task_status',
639
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
640
+ WHERE project = '$project_sql'
641
+ AND file_path = '$fp_sql';
642
+
643
+ UPDATE orchestrations
644
+ SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
645
+ WHERE project = '$project_sql' AND name = '$name_sql';
646
+ SQL
647
+ eagle_ok "Lane '$key' marked $status"
648
+ else
649
+ current_status=$(eagle_db "SELECT status FROM orchestration_lanes
650
+ WHERE project = '$project_sql'
651
+ AND lane_key = '$key_sql'
652
+ AND orchestration_id = $oid
653
+ LIMIT 1;")
654
+ if [ "$current_status" = "cancelled" ] && [ "$status" != "cancelled" ]; then
655
+ eagle_dim "Lane '$key' is cancelled; leaving status unchanged."
656
+ return 0
657
+ fi
658
+ if [ "$current_status" = "blocked" ] && [ "$status" = "completed" ]; then
659
+ eagle_dim "Lane '$key' is blocked; leaving blocker unchanged. Run lane start before completing it."
660
+ return 0
661
+ fi
662
+ eagle_err "Lane not found: $key"
663
+ exit 1
664
+ fi
665
+ }
666
+
667
+ orchestrate_set_status() {
668
+ local status="$1"
669
+ local status_sql changed oid active_count lane_terminal_where task_terminal_where
670
+ status_sql=$(eagle_sql_escape "$status")
671
+ oid=$(active_orchestration_id)
672
+ if [ -z "$oid" ]; then
673
+ eagle_err "No active orchestration found for '$project' ($name). Run: eagle-mem orchestrate init <goal>"
674
+ exit 1
675
+ fi
676
+
677
+ active_count=$(eagle_db "SELECT COUNT(*) FROM orchestration_lanes
678
+ WHERE orchestration_id = $oid
679
+ AND status IN ('pending', 'in_progress', 'blocked');")
680
+ active_count=${active_count:-0}
681
+
682
+ if [ "$status" = "completed" ] && [ "$active_count" -gt 0 ] 2>/dev/null; then
683
+ eagle_err "Cannot complete orchestration '$name' while $active_count lane(s) are still active."
684
+ eagle_dim "Complete or cancel active lanes first, then run: eagle-mem orchestrate complete --name \"$name\""
685
+ exit 1
686
+ fi
687
+
688
+ lane_terminal_where="0"
689
+ task_terminal_where="0"
690
+ if [ "$status" = "cancelled" ]; then
691
+ lane_terminal_where="orchestration_id = $oid AND status IN ('pending', 'in_progress', 'blocked')"
692
+ task_terminal_where="project = '$project_sql'
693
+ AND source_session_id = 'orchestration'
694
+ AND source_task_id IN (
695
+ SELECT source_task_id
696
+ FROM orchestration_lanes
697
+ WHERE orchestration_id = $oid
698
+ AND status = 'cancelled'
699
+ )"
700
+ fi
701
+
702
+ changed=$(eagle_db_pipe <<SQL
703
+ UPDATE orchestration_lanes
704
+ SET status = 'cancelled',
705
+ notes = CASE WHEN notes IS NULL OR notes = '' THEN 'Cancelled with parent orchestration' ELSE notes END,
706
+ worker_pid = NULL,
707
+ worker_finished_at = CASE
708
+ WHEN worker_started_at IS NOT NULL AND worker_finished_at IS NULL THEN strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
709
+ ELSE worker_finished_at
710
+ END,
711
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
712
+ WHERE $lane_terminal_where;
713
+
714
+ UPDATE agent_tasks
715
+ SET status = 'cancelled',
716
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
717
+ WHERE $task_terminal_where;
718
+
719
+ UPDATE orchestrations
720
+ SET status = '$status_sql',
721
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
722
+ WHERE id = $oid;
723
+ SELECT changes();
724
+ SQL
725
+ )
726
+
727
+ if [ "${changed:-0}" -gt 0 ] 2>/dev/null; then
728
+ eagle_ok "Orchestration '$name' marked $status"
729
+ else
730
+ eagle_err "No orchestration found for '$project' ($name). Run: eagle-mem orchestrate init <goal>"
731
+ exit 1
732
+ fi
733
+ }
734
+
735
+ parse_spawn_options() {
736
+ spawn_no_worktree=false
737
+ spawn_no_launch=false
738
+ spawn_foreground=false
739
+ spawn_dry_run=false
740
+ spawn_notes=""
741
+
742
+ local parsed=()
743
+ local i=0
744
+ while [ "$i" -lt "${#args[@]}" ]; do
745
+ case "${args[$i]}" in
746
+ --no-worktree)
747
+ spawn_no_worktree=true ;;
748
+ --no-launch)
749
+ spawn_no_launch=true ;;
750
+ --foreground)
751
+ spawn_foreground=true ;;
752
+ --dry-run)
753
+ spawn_dry_run=true ;;
754
+ --notes)
755
+ i=$((i + 1)); spawn_notes="${args[$i]:-}" ;;
756
+ *)
757
+ parsed+=("${args[$i]}") ;;
758
+ esac
759
+ i=$((i + 1))
760
+ done
761
+ args=("${parsed[@]}")
762
+ }
763
+
764
+ orchestrate_worker_run_script() {
765
+ local run_script="$1" worker_agent="$2" worker_model="$3" worker_effort="$4" worktree="$5" prompt_file="$6" exit_path="$7" last_message_path="$8" bin_path="$9" lane_key="${10}" log_path="${11}"
766
+ local effort_config="model_reasoning_effort=\"$worker_effort\""
767
+ local complete_note="Worker exited 0; log: $log_path"
768
+ local block_note="Worker exited non-zero; log: $log_path"
769
+
770
+ {
771
+ echo '#!/usr/bin/env bash'
772
+ echo 'set +e'
773
+ printf 'cd %q || exit 1\n' "$worktree"
774
+ printf 'export EAGLE_MEM_DIR=%q\n' "$EAGLE_MEM_DIR"
775
+ printf 'export EAGLE_MEM_PROJECT=%q\n' "$project"
776
+ printf 'export EAGLE_AGENT_SOURCE=%q\n' "$worker_agent"
777
+ printf 'export EAGLE_ORCHESTRATION_NAME=%q\n' "$name"
778
+ printf 'export EAGLE_ORCHESTRATION_LANE=%q\n' "$lane_key"
779
+ printf 'export EAGLE_ORCHESTRATION_WORKTREE=%q\n' "$worktree"
780
+ if [ "$worker_agent" = "codex" ]; then
781
+ printf 'codex exec --cd %q --model %q -c %q -c %q --sandbox danger-full-access --output-last-message %q - < %q\n' \
782
+ "$worktree" "$worker_model" "$effort_config" 'approval_policy="never"' "$last_message_path" "$prompt_file"
783
+ else
784
+ printf 'prompt=$(cat %q)\n' "$prompt_file"
785
+ printf 'claude -p --model %q --effort %q --permission-mode dontAsk --output-format text "$prompt"\n' \
786
+ "$worker_model" "$worker_effort"
787
+ fi
788
+ echo 'rc=$?'
789
+ printf 'printf "%%s\\n" "$rc" > %q\n' "$exit_path"
790
+ printf 'date -u "+%%Y-%%m-%%dT%%H:%%M:%%SZ" > %q.done\n' "$exit_path"
791
+ echo 'if [ "$rc" -eq 0 ]; then'
792
+ printf ' bash %q orchestrate lane --project %q --name %q complete %q --notes %q >/dev/null 2>&1\n' "$bin_path" "$project" "$name" "$lane_key" "$complete_note"
793
+ echo 'else'
794
+ printf ' bash %q orchestrate lane --project %q --name %q block %q --notes %q >/dev/null 2>&1\n' "$bin_path" "$project" "$name" "$lane_key" "$block_note"
795
+ echo 'fi'
796
+ echo 'exit "$rc"'
797
+ } > "$run_script"
798
+ chmod +x "$run_script"
799
+ }
800
+
801
+ orchestrate_spawn() {
802
+ parse_spawn_options
803
+ local lane_key="${args[0]:-}"
804
+ [ -z "$lane_key" ] && { eagle_err "Usage: eagle-mem orchestrate spawn <lane-key>"; exit 1; }
805
+
806
+ local oid lane_json lane_count
807
+ oid=$(require_active_orchestration_id)
808
+ lane_json=$(orchestrate_lane_json "$oid" "$lane_key")
809
+ lane_count=$(printf '%s' "$lane_json" | jq 'length' 2>/dev/null)
810
+ if [ "${lane_count:-0}" -eq 0 ] 2>/dev/null; then
811
+ eagle_err "Lane not found: $lane_key"
812
+ exit 1
813
+ fi
814
+
815
+ local lane_title lane_desc lane_agent lane_worktree lane_validation lane_status existing_pid goal
816
+ lane_title=$(printf '%s' "$lane_json" | jq -r '.[0].title // ""')
817
+ lane_desc=$(printf '%s' "$lane_json" | jq -r '.[0].description // ""')
818
+ lane_agent=$(printf '%s' "$lane_json" | jq -r '.[0].agent // ""')
819
+ lane_worktree=$(printf '%s' "$lane_json" | jq -r '.[0].worktree_path // ""')
820
+ lane_validation=$(printf '%s' "$lane_json" | jq -r '.[0].validation // ""')
821
+ lane_status=$(printf '%s' "$lane_json" | jq -r '.[0].status // ""')
822
+ existing_pid=$(printf '%s' "$lane_json" | jq -r '.[0].worker_pid // empty')
823
+ goal=$(eagle_db "SELECT goal FROM orchestrations WHERE id = $oid;")
824
+
825
+ if [ "$lane_status" = "completed" ] || [ "$lane_status" = "cancelled" ]; then
826
+ eagle_err "Lane '$lane_key' is already $lane_status"
827
+ exit 1
828
+ fi
829
+ if [ "$lane_status" = "in_progress" ]; then
830
+ if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
831
+ eagle_err "Lane '$lane_key' already has a running worker (pid $existing_pid). Run: eagle-mem orchestrate sync $lane_key"
832
+ exit 1
833
+ fi
834
+ eagle_err "Lane '$lane_key' is already in progress. Run sync first, or cancel/block it before spawning again."
835
+ exit 1
836
+ fi
837
+
838
+ local worker_agent worker_model worker_effort
839
+ if [ -n "$agent" ]; then
840
+ if ! worker_agent=$(orchestrate_normalize_agent "$agent"); then
841
+ eagle_err "Invalid worker agent: $agent. Use codex or claude-code."
842
+ exit 1
843
+ fi
844
+ elif [ -n "$lane_agent" ]; then
845
+ if ! worker_agent=$(orchestrate_normalize_agent "$lane_agent"); then
846
+ eagle_err "Invalid lane worker agent: $lane_agent. Use codex or claude-code."
847
+ exit 1
848
+ fi
849
+ else
850
+ worker_agent=$(orchestrate_default_worker_agent)
851
+ fi
852
+ worker_model=$(orchestrate_worker_model "$worker_agent")
853
+ worker_effort=$(orchestrate_worker_effort "$worker_agent")
854
+
855
+ if [ "$spawn_dry_run" != true ] && [ "$spawn_no_launch" != true ]; then
856
+ orchestrate_require_worker_cli "$worker_agent"
857
+ fi
858
+
859
+ local worktree_info repo_root worktree branch
860
+ worktree_info=$(orchestrate_prepare_worktree "$lane_key" "$lane_worktree" "$spawn_no_worktree" "$spawn_dry_run")
861
+ IFS='|' read -r repo_root worktree branch <<< "$worktree_info"
862
+
863
+ local run_dir prompt_file log_path exit_path last_message_path run_script bin_path command_display attempt_key
864
+ attempt_key="a$(date -u +%Y%m%d%H%M%S)-$$"
865
+ run_dir=$(orchestrate_run_dir "$lane_key" "$attempt_key")
866
+ prompt_file="$run_dir/prompt.md"
867
+ log_path="$run_dir/worker.log"
868
+ exit_path="$run_dir/exit_code"
869
+ last_message_path="$run_dir/last-message.txt"
870
+ run_script="$run_dir/run-worker.sh"
871
+ bin_path="$(cd "$SCRIPTS_DIR/.." && pwd)/bin/eagle-mem"
872
+
873
+ command_display=$(orchestrate_shell_join bash "$run_script")
874
+
875
+ if [ "$spawn_dry_run" = true ]; then
876
+ if [ "$json_output" = true ]; then
877
+ jq -nc --arg lane "$lane_key" --arg agent "$worker_agent" --arg model "$worker_model" --arg effort "$worker_effort" --arg worktree "$worktree" --arg branch "$branch" --arg command "$command_display" \
878
+ '{lane_key:$lane, worker_agent:$agent, model:$model, effort:$effort, worktree:$worktree, branch:$branch, command:$command, dry_run:true}'
879
+ else
880
+ eagle_ok "Dry-run worker plan"
881
+ eagle_kv "Lane:" "$lane_key"
882
+ eagle_kv "Worker:" "$(eagle_agent_label "$worker_agent") $worker_model / $worker_effort"
883
+ eagle_kv "Worktree:" "$worktree"
884
+ eagle_kv "Branch:" "$branch"
885
+ eagle_kv "Command:" "$command_display"
886
+ fi
887
+ return 0
888
+ fi
889
+
890
+ mkdir -p "$run_dir"
891
+ orchestrate_prepare_prompt "$prompt_file" "$lane_key" "$lane_title" "$lane_desc" "$lane_validation" "$worker_agent" "$worker_model" "$worker_effort" "$worktree" "$branch" "$goal"
892
+ orchestrate_worker_run_script "$run_script" "$worker_agent" "$worker_model" "$worker_effort" "$worktree" "$prompt_file" "$exit_path" "$last_message_path" "$bin_path" "$lane_key" "$log_path"
893
+
894
+ local key_sql wt_sql branch_sql worker_agent_sql model_sql effort_sql log_sql exit_sql prompt_sql cmd_sql notes_sql fp_sql
895
+ key_sql=$(eagle_sql_escape "$lane_key")
896
+ wt_sql=$(eagle_sql_escape "$worktree")
897
+ branch_sql=$(eagle_sql_escape "$branch")
898
+ worker_agent_sql=$(eagle_sql_escape "$worker_agent")
899
+ model_sql=$(eagle_sql_escape "$worker_model")
900
+ effort_sql=$(eagle_sql_escape "$worker_effort")
901
+ log_sql=$(eagle_sql_escape "$log_path")
902
+ exit_sql=$(eagle_sql_escape "$exit_path")
903
+ prompt_sql=$(eagle_sql_escape "$prompt_file")
904
+ cmd_sql=$(eagle_sql_escape "$command_display")
905
+ notes_sql=$(eagle_sql_escape "${spawn_notes:-Worker prepared}")
906
+ fp_sql=$(eagle_sql_escape "$(lane_file_path "$lane_key")")
907
+
908
+ if [ "$spawn_no_launch" = true ]; then
909
+ eagle_db_pipe <<SQL >/dev/null
910
+ UPDATE orchestration_lanes
911
+ SET worktree_path = '$wt_sql',
912
+ branch_name = '$branch_sql',
913
+ worker_agent = '$worker_agent_sql',
914
+ worker_model = '$model_sql',
915
+ worker_effort = '$effort_sql',
916
+ worker_log_path = '$log_sql',
917
+ worker_exit_path = '$exit_sql',
918
+ worker_prompt_path = '$prompt_sql',
919
+ worker_command = '$cmd_sql',
920
+ notes = '$notes_sql',
921
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
922
+ WHERE orchestration_id = $oid AND lane_key = '$key_sql';
923
+ SQL
924
+ if [ "$json_output" = true ]; then
925
+ jq -nc --arg lane "$lane_key" --arg agent "$worker_agent" --arg model "$worker_model" --arg effort "$worker_effort" --arg worktree "$worktree" --arg branch "$branch" --arg log "$log_path" --arg prompt "$prompt_file" --arg command "$command_display" \
926
+ '{lane_key:$lane, worker_agent:$agent, model:$model, effort:$effort, worktree:$worktree, branch:$branch, log:$log, prompt:$prompt, command:$command, launched:false}'
927
+ else
928
+ eagle_ok "Lane '$lane_key' prepared"
929
+ eagle_kv "Worker:" "$(eagle_agent_label "$worker_agent") $worker_model / $worker_effort"
930
+ eagle_kv "Worktree:" "$worktree"
931
+ eagle_kv "Branch:" "$branch"
932
+ fi
933
+ return 0
934
+ fi
935
+
936
+ rm -f "$exit_path" "$exit_path.done" "$last_message_path"
937
+
938
+ local claim_changed
939
+ claim_changed=$(eagle_db_pipe <<SQL
940
+ UPDATE orchestration_lanes
941
+ SET status = 'in_progress',
942
+ worktree_path = '$wt_sql',
943
+ branch_name = '$branch_sql',
944
+ worker_agent = '$worker_agent_sql',
945
+ worker_model = '$model_sql',
946
+ worker_effort = '$effort_sql',
947
+ worker_pid = NULL,
948
+ worker_log_path = '$log_sql',
949
+ worker_exit_path = '$exit_sql',
950
+ worker_prompt_path = '$prompt_sql',
951
+ worker_command = '$cmd_sql',
952
+ worker_started_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
953
+ worker_finished_at = NULL,
954
+ notes = CASE WHEN '$notes_sql' != '' THEN '$notes_sql' ELSE notes END,
955
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
956
+ WHERE orchestration_id = $oid
957
+ AND lane_key = '$key_sql'
958
+ AND status NOT IN ('in_progress', 'completed', 'cancelled');
959
+ SELECT changes();
960
+ SQL
961
+ )
962
+
963
+ if [ "${claim_changed:-0}" -le 0 ] 2>/dev/null; then
964
+ eagle_err "Lane '$lane_key' was not claimed. Run sync/status before spawning again."
965
+ exit 1
966
+ fi
967
+
968
+ eagle_db_pipe <<SQL >/dev/null
969
+ UPDATE agent_tasks
970
+ SET status = 'in_progress',
971
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
972
+ WHERE project = '$project_sql'
973
+ AND file_path = '$fp_sql';
974
+ SQL
975
+
976
+ local pid pid_sql
977
+ if [ "$spawn_foreground" = true ]; then
978
+ bash "$run_script" > "$log_path" 2>&1
979
+ pid=""
980
+ else
981
+ nohup bash "$run_script" > "$log_path" 2>&1 &
982
+ pid=$!
983
+ pid_sql=$(eagle_sql_int "$pid")
984
+ eagle_db "UPDATE orchestration_lanes
985
+ SET worker_pid = $pid_sql,
986
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
987
+ WHERE orchestration_id = $oid
988
+ AND lane_key = '$key_sql'
989
+ AND status = 'in_progress';" >/dev/null
990
+ fi
991
+
992
+ if [ "$json_output" = true ]; then
993
+ jq -nc --arg lane "$lane_key" --arg agent "$worker_agent" --arg model "$worker_model" --arg effort "$worker_effort" --arg worktree "$worktree" --arg branch "$branch" --arg log "$log_path" --arg pid "${pid:-}" \
994
+ '{lane_key:$lane, worker_agent:$agent, model:$model, effort:$effort, worktree:$worktree, branch:$branch, log:$log, pid:$pid}'
995
+ else
996
+ eagle_ok "Worker launched for lane '$lane_key'"
997
+ eagle_kv "Worker:" "$(eagle_agent_label "$worker_agent") $worker_model / $worker_effort"
998
+ [ -n "$pid" ] && eagle_kv "PID:" "$pid"
999
+ eagle_kv "Worktree:" "$worktree"
1000
+ eagle_kv "Branch:" "$branch"
1001
+ eagle_kv "Log:" "$log_path"
1002
+ fi
1003
+ }
1004
+
1005
+ orchestrate_sync_one() {
1006
+ local lane_key="$1"
1007
+ local oid lane_json lane_count lane_status pid exit_path log_path started_at rc key_sql fp_sql note_sql task_status
1008
+ oid=$(require_orchestration_id)
1009
+ lane_json=$(orchestrate_lane_json "$oid" "$lane_key")
1010
+ lane_count=$(printf '%s' "$lane_json" | jq 'length' 2>/dev/null)
1011
+ if [ "${lane_count:-0}" -eq 0 ] 2>/dev/null; then
1012
+ eagle_err "Lane not found: $lane_key"
1013
+ return 1
1014
+ fi
1015
+
1016
+ lane_status=$(printf '%s' "$lane_json" | jq -r '.[0].status // ""')
1017
+ pid=$(printf '%s' "$lane_json" | jq -r '.[0].worker_pid // empty')
1018
+ exit_path=$(printf '%s' "$lane_json" | jq -r '.[0].worker_exit_path // empty')
1019
+ log_path=$(printf '%s' "$lane_json" | jq -r '.[0].worker_log_path // empty')
1020
+ started_at=$(printf '%s' "$lane_json" | jq -r '.[0].worker_started_at // empty')
1021
+
1022
+ case "$lane_status" in
1023
+ completed|cancelled|blocked)
1024
+ eagle_ok "Lane '$lane_key' already $lane_status"
1025
+ return 0
1026
+ ;;
1027
+ esac
1028
+
1029
+ if [ -n "$exit_path" ] && [ -f "$exit_path" ]; then
1030
+ rc=$(tr -d '[:space:]' < "$exit_path")
1031
+ rc=${rc:-1}
1032
+ if [ "$rc" = "0" ]; then
1033
+ lane_status="completed"
1034
+ task_status="completed"
1035
+ note_sql=$(eagle_sql_escape "Worker completed; log: $log_path")
1036
+ else
1037
+ lane_status="blocked"
1038
+ task_status="pending"
1039
+ note_sql=$(eagle_sql_escape "Worker exited $rc; log: $log_path")
1040
+ fi
1041
+ elif [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
1042
+ eagle_ok "Lane '$lane_key' worker still running (pid $pid)"
1043
+ return 0
1044
+ elif [ -z "$started_at" ]; then
1045
+ eagle_dim "Lane '$lane_key' has not been launched; leaving status as $lane_status."
1046
+ return 0
1047
+ else
1048
+ lane_status="blocked"
1049
+ task_status="pending"
1050
+ note_sql=$(eagle_sql_escape "Worker process is not running and no exit code was recorded; log: $log_path")
1051
+ fi
1052
+
1053
+ key_sql=$(eagle_sql_escape "$lane_key")
1054
+ fp_sql=$(eagle_sql_escape "$(lane_file_path "$lane_key")")
1055
+ local sync_changed
1056
+ sync_changed=$(eagle_db_pipe <<SQL
1057
+ UPDATE orchestration_lanes
1058
+ SET status = '$lane_status',
1059
+ notes = '$note_sql',
1060
+ worker_pid = NULL,
1061
+ worker_finished_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
1062
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
1063
+ WHERE orchestration_id = $oid
1064
+ AND lane_key = '$key_sql'
1065
+ AND status = 'in_progress';
1066
+ SELECT changes();
1067
+ SQL
1068
+ )
1069
+
1070
+ if [ "${sync_changed:-0}" -le 0 ] 2>/dev/null; then
1071
+ lane_status=$(eagle_db "SELECT status FROM orchestration_lanes
1072
+ WHERE orchestration_id = $oid AND lane_key = '$key_sql'
1073
+ LIMIT 1;")
1074
+ if [ "$lane_status" = "cancelled" ] || [ "$lane_status" = "blocked" ] || [ "$lane_status" = "completed" ]; then
1075
+ eagle_dim "Lane '$lane_key' is $lane_status; leaving status unchanged."
1076
+ return 0
1077
+ fi
1078
+ eagle_err "Lane not found: $lane_key"
1079
+ return 1
1080
+ fi
1081
+
1082
+ eagle_db_pipe <<SQL >/dev/null
1083
+ UPDATE agent_tasks
1084
+ SET status = '$task_status',
1085
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
1086
+ WHERE project = '$project_sql'
1087
+ AND file_path = '$fp_sql';
1088
+ SQL
1089
+ eagle_ok "Lane '$lane_key' synced as $lane_status"
1090
+ }
1091
+
1092
+ orchestrate_sync() {
1093
+ local lane_key="${args[0]:-}"
1094
+ if [ -n "$lane_key" ]; then
1095
+ orchestrate_sync_one "$lane_key"
1096
+ return
1097
+ fi
1098
+
1099
+ local oid keys
1100
+ oid=$(require_orchestration_id)
1101
+ keys=$(eagle_db "SELECT lane_key FROM orchestration_lanes
1102
+ WHERE orchestration_id = $oid
1103
+ AND (worker_pid IS NOT NULL OR worker_started_at IS NOT NULL)
1104
+ AND status IN ('pending', 'in_progress', 'blocked')
1105
+ ORDER BY lane_key;")
1106
+ if [ -z "$keys" ]; then
1107
+ eagle_dim "No worker-backed lanes to sync."
1108
+ return
1109
+ fi
1110
+ while IFS= read -r lane_key; do
1111
+ [ -z "$lane_key" ] && continue
1112
+ orchestrate_sync_one "$lane_key"
1113
+ done <<< "$keys"
1114
+ }
1115
+
1116
+ orchestrate_status() {
1117
+ local oid
1118
+ oid=$(orchestration_id)
1119
+ if [ -z "$oid" ]; then
1120
+ if [ "$json_output" = true ]; then
1121
+ printf '[]\n'
1122
+ return
1123
+ fi
1124
+ eagle_dim "No orchestration for '$project'"
1125
+ eagle_dim "Run: eagle-mem orchestrate init <goal>"
1126
+ return
1127
+ fi
1128
+
1129
+ if [ "$json_output" = true ]; then
1130
+ eagle_db_json "SELECT lane_key, title, description, agent, worktree_path, branch_name, validation, status, notes,
1131
+ worker_agent, worker_model, worker_effort, worker_pid, worker_log_path,
1132
+ worker_started_at, worker_finished_at, updated_at
1133
+ FROM orchestration_lanes
1134
+ WHERE orchestration_id = $oid
1135
+ ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 WHEN 'pending' THEN 2 WHEN 'completed' THEN 3 ELSE 4 END, lane_key;"
1136
+ return
1137
+ fi
1138
+
1139
+ local meta
1140
+ meta=$(eagle_db "SELECT name, goal, status, baseline_ref, updated_at FROM orchestrations WHERE id = $oid;")
1141
+ IFS='|' read -r oname goal status baseline updated <<< "$meta"
1142
+
1143
+ echo ""
1144
+ echo -e " ${BOLD}Orchestration${RESET} ${DIM}($project)${RESET}"
1145
+ echo -e " ${DIM}─────────────────────────────────────${RESET}"
1146
+ eagle_kv "Name:" "$oname"
1147
+ eagle_kv "Status:" "$status"
1148
+ [ -n "$baseline" ] && eagle_kv "Baseline:" "$baseline"
1149
+ [ -n "$goal" ] && eagle_kv "Goal:" "$goal"
1150
+ eagle_kv "Updated:" "$updated"
1151
+ echo ""
1152
+
1153
+ local rows
1154
+ rows=$(eagle_db "SELECT lane_key, title, agent, status, validation, worktree_path, notes, branch_name, worker_agent, worker_model, worker_effort, worker_pid, worker_log_path
1155
+ FROM orchestration_lanes
1156
+ WHERE orchestration_id = $oid
1157
+ ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 WHEN 'pending' THEN 2 WHEN 'completed' THEN 3 ELSE 4 END, lane_key;")
1158
+ if [ -z "$rows" ]; then
1159
+ eagle_dim "No lanes yet. Add one with: eagle-mem orchestrate lane add <key>"
1160
+ return
1161
+ fi
1162
+
1163
+ while IFS='|' read -r key title lane_agent lane_status validation worktree notes branch worker_agent worker_model worker_effort worker_pid worker_log; do
1164
+ [ -z "$key" ] && continue
1165
+ echo -e " ${CYAN}${key}${RESET} ${BOLD}$title${RESET} ${DIM}[$lane_status, $(eagle_agent_label "$lane_agent")]${RESET}"
1166
+ [ -n "$validation" ] && echo -e " ${DIM}validate: $validation${RESET}"
1167
+ [ -n "$worktree" ] && echo -e " ${DIM}worktree: $worktree${RESET}"
1168
+ [ -n "$branch" ] && echo -e " ${DIM}branch: $branch${RESET}"
1169
+ [ -n "$worker_model" ] && echo -e " ${DIM}worker: $(eagle_agent_label "$worker_agent") $worker_model / $worker_effort${RESET}"
1170
+ [ -n "$worker_pid" ] && echo -e " ${DIM}pid: $worker_pid${RESET}"
1171
+ [ -n "$worker_log" ] && echo -e " ${DIM}log: $worker_log${RESET}"
1172
+ [ -n "$notes" ] && echo -e " ${DIM}notes: $notes${RESET}"
1173
+ done <<< "$rows"
1174
+ echo ""
1175
+ }
1176
+
1177
+ orchestrate_handoff() {
1178
+ parse_lane_options
1179
+ local write_path=""
1180
+ local i=0
1181
+ while [ "$i" -lt "${#args[@]}" ]; do
1182
+ case "${args[$i]}" in
1183
+ --write)
1184
+ i=$((i + 1)); write_path="${args[$i]:-}" ;;
1185
+ esac
1186
+ i=$((i + 1))
1187
+ done
1188
+
1189
+ local oid
1190
+ oid=$(require_orchestration_id)
1191
+ local out
1192
+ out=$(mktemp)
1193
+
1194
+ {
1195
+ local meta
1196
+ meta=$(eagle_db "SELECT name, goal, status, baseline_ref, updated_at FROM orchestrations WHERE id = $oid;")
1197
+ IFS='|' read -r oname goal status baseline updated <<< "$meta"
1198
+ echo "# Eagle Mem Orchestration"
1199
+ echo ""
1200
+ echo "- Project: $project"
1201
+ echo "- Name: $oname"
1202
+ echo "- Status: $status"
1203
+ [ -n "$baseline" ] && echo "- Baseline: $baseline"
1204
+ echo "- Updated: $updated"
1205
+ [ -n "$goal" ] && echo "- Goal: $goal"
1206
+ echo ""
1207
+ echo "## Worker Lanes"
1208
+ echo ""
1209
+ local rows
1210
+ rows=$(eagle_db "SELECT lane_key, title, description, agent, status, validation, worktree_path, notes, branch_name, worker_agent, worker_model, worker_effort, worker_log_path
1211
+ FROM orchestration_lanes
1212
+ WHERE orchestration_id = $oid
1213
+ ORDER BY lane_key;")
1214
+ if [ -z "$rows" ]; then
1215
+ echo "No lanes recorded."
1216
+ else
1217
+ while IFS='|' read -r key title desc lane_agent lane_status validation worktree notes branch worker_agent worker_model worker_effort worker_log; do
1218
+ [ -z "$key" ] && continue
1219
+ echo "### $key — $title"
1220
+ echo ""
1221
+ echo "- Agent: $(eagle_agent_label "$lane_agent")"
1222
+ echo "- Status: $lane_status"
1223
+ [ -n "$worktree" ] && echo "- Worktree: $worktree"
1224
+ [ -n "$branch" ] && echo "- Branch: $branch"
1225
+ [ -n "$worker_model" ] && echo "- Worker: $(eagle_agent_label "$worker_agent") $worker_model / $worker_effort"
1226
+ [ -n "$worker_log" ] && echo "- Log: $worker_log"
1227
+ [ -n "$validation" ] && echo "- Validation: \`$validation\`"
1228
+ [ -n "$desc" ] && echo "- Scope: $desc"
1229
+ [ -n "$notes" ] && echo "- Notes: $notes"
1230
+ echo ""
1231
+ done <<< "$rows"
1232
+ fi
1233
+ } > "$out"
1234
+
1235
+ if [ -n "$write_path" ]; then
1236
+ mkdir -p "$(dirname "$write_path")"
1237
+ cp "$out" "$write_path"
1238
+ rm -f "$out"
1239
+ eagle_ok "Handoff written: $write_path"
1240
+ else
1241
+ cat "$out"
1242
+ rm -f "$out"
1243
+ fi
1244
+ }
1245
+
1246
+ case "$action" in
1247
+ init) orchestrate_init ;;
1248
+ status|list|ls) orchestrate_status ;;
1249
+ spawn) orchestrate_spawn ;;
1250
+ sync) orchestrate_sync ;;
1251
+ complete) orchestrate_set_status "completed" ;;
1252
+ cancel) orchestrate_set_status "cancelled" ;;
1253
+ handoff) orchestrate_handoff ;;
1254
+ lane)
1255
+ lane_action="${args[0]:-}"
1256
+ if [ "${#args[@]}" -gt 0 ]; then args=("${args[@]:1}"); fi
1257
+ case "$lane_action" in
1258
+ add) lane_add ;;
1259
+ start) lane_set_status "in_progress" ;;
1260
+ block) lane_set_status "blocked" ;;
1261
+ complete) lane_set_status "completed" ;;
1262
+ cancel) lane_set_status "cancelled" ;;
1263
+ *) eagle_err "Usage: eagle-mem orchestrate lane [add|start|block|complete|cancel] <key>"; exit 1 ;;
1264
+ esac
1265
+ ;;
1266
+ --help|-h) show_help ;;
1267
+ *) eagle_err "Unknown action: $action"; eagle_dim " Run 'eagle-mem orchestrate --help' for options"; exit 1 ;;
1268
+ esac