eagle-mem 4.7.0 → 4.8.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/README.md +56 -12
- package/bin/eagle-mem +1 -0
- package/db/028_agent_artifact_tables.sql +124 -0
- package/db/029_orchestration_lanes.sql +45 -0
- package/db/030_orchestration_lane_scope.sql +88 -0
- package/db/031_orchestration_workers.sql +20 -0
- package/db/032_orchestration_run_keys.sql +9 -0
- package/hooks/post-tool-use.sh +2 -1
- package/hooks/pre-tool-use.sh +25 -1
- package/hooks/session-end.sh +2 -1
- package/hooks/session-start.sh +103 -13
- package/hooks/stop.sh +15 -13
- package/hooks/user-prompt-submit.sh +71 -12
- package/lib/common.sh +173 -2
- package/lib/db-backfill.sh +3 -3
- package/lib/db-mirrors.sh +59 -32
- package/lib/db-observations.sh +7 -0
- package/lib/db-sessions.sh +12 -6
- package/lib/db-summaries.sh +9 -5
- package/lib/hooks-posttool.sh +4 -4
- package/lib/provider.sh +224 -4
- package/package.json +3 -1
- package/scripts/config.sh +32 -0
- package/scripts/health.sh +71 -1
- package/scripts/help.sh +18 -7
- package/scripts/install.sh +12 -0
- package/scripts/memories.sh +50 -27
- package/scripts/orchestrate.sh +1268 -0
- package/scripts/refresh.sh +3 -3
- package/scripts/search.sh +21 -19
- package/scripts/statusline-em.sh +1 -1
- package/scripts/tasks.sh +186 -15
- package/scripts/update.sh +20 -1
- package/skills/eagle-mem-memories/SKILL.md +13 -13
- package/skills/eagle-mem-orchestrate/SKILL.md +149 -0
- package/skills/eagle-mem-tasks/SKILL.md +23 -15
|
@@ -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
|