One bot owns the chat and delegates coding to persistent tmux sub-agents, using a .done file marker for a reliable handoff (status/wait/result). Includes README + install.sh for one-command setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
5.2 KiB
Bash
Executable File
127 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# mav-agent — spawn a persistent sub-agent (claude or codex) in a named tmux window.
|
|
# each agent lives in its own tmux window inside the "mav" session.
|
|
#
|
|
# usage:
|
|
# mav-agent run <name> <task> spawn a claude sub-agent with task
|
|
# mav-agent run <name> --codex <task> spawn a codex sub-agent with task
|
|
# mav-agent read <name> capture current output from agent window
|
|
# mav-agent status <name> running | done | none (reliable, marker-based)
|
|
# mav-agent wait <name> [timeout_s] block until agent done (marker-based)
|
|
# mav-agent result <name> print the agent's final stdout (after done)
|
|
# mav-agent list list all agent windows + status
|
|
# mav-agent kill <name> kill an agent window
|
|
#
|
|
# agents run in mav tmux session, window named <name>.
|
|
# clean handoff: each run writes /tmp/mav-agent-<name>.done when the CLI exits,
|
|
# and streams full stdout to /tmp/mav-agent-<name>.out. status/wait poll the marker
|
|
# (no flaky prompt-sniffing), so the main actor always knows when work is finished.
|
|
|
|
SESSION="mav"
|
|
RUNDIR="/tmp/mav-agent"
|
|
mkdir -p "$RUNDIR" 2>/dev/null
|
|
CLAUDE=/Users/m1/.local/bin/claude
|
|
OR_KEY=$(python3 -c "import json; print(json.load(open('/Users/m1/.config/mav/bridge/config.json')).get('openrouterKey',''))" 2>/dev/null)
|
|
|
|
# ensure session exists
|
|
TMUX= tmux has-session -t "$SESSION" 2>/dev/null || TMUX= tmux new-session -d -s "$SESSION"
|
|
|
|
window_exists() { TMUX= tmux list-windows -t "$SESSION" -F "#{window_name}" 2>/dev/null | grep -q "^${1}$"; }
|
|
|
|
case "${1:-list}" in
|
|
run)
|
|
NAME="${2:?usage: mav-agent run <name> [--codex] <task>}"
|
|
shift 2
|
|
|
|
# detect --codex flag
|
|
CLI="claude"
|
|
if [[ "${1:-}" == "--codex" ]]; then CLI="codex"; shift; fi
|
|
TASK="${*:?task required}"
|
|
|
|
# kill existing window with same name if present
|
|
window_exists "$NAME" && TMUX= tmux kill-window -t "${SESSION}:${NAME}" 2>/dev/null
|
|
|
|
OUT="$RUNDIR/${NAME}.out"
|
|
DONE="$RUNDIR/${NAME}.done"
|
|
rm -f "$OUT" "$DONE" 2>/dev/null
|
|
|
|
# create new window at the next free index (avoids "index N in use" on gaps)
|
|
NEXT=$(( $(TMUX= tmux list-windows -t "$SESSION" -F "#{window_index}" 2>/dev/null | sort -n | tail -1) + 1 ))
|
|
TMUX= tmux new-window -d -t "${SESSION}:${NEXT}" -n "$NAME"
|
|
|
|
if [[ "$CLI" == "codex" ]]; then
|
|
CLI_CMD="codex --approval-mode auto-edit -q $(printf '%q' "$TASK")"
|
|
else
|
|
CLI_CMD="$CLAUDE -p $(printf '%q' "$TASK") --output-format text --permission-mode bypassPermissions --model opus"
|
|
fi
|
|
|
|
# stream stdout to a file, then drop a done-marker with the exit code.
|
|
# this is the clean handoff: marker => work finished, no prompt-sniffing.
|
|
# run via bash -c so PIPESTATUS works regardless of the pane's login shell.
|
|
INNER="{ $CLI_CMD ; } 2>&1 | tee $(printf '%q' "$OUT"); echo \${PIPESTATUS[0]} > $(printf '%q' "$DONE")"
|
|
CMD="bash -c $(printf '%q' "$INNER")"
|
|
|
|
TMUX= tmux send-keys -t "${SESSION}:${NAME}" "$CMD" Enter
|
|
echo "[mav-agent] started: $CLI agent in window '${SESSION}:${NAME}'"
|
|
echo "task: $TASK"
|
|
echo "marker: $DONE | output: $OUT"
|
|
;;
|
|
|
|
status)
|
|
NAME="${2:?usage: mav-agent status <name>}"
|
|
if [[ -f "$RUNDIR/${NAME}.done" ]]; then echo "done (exit $(cat "$RUNDIR/${NAME}.done" 2>/dev/null | tr -d '[:space:]'))";
|
|
elif window_exists "$NAME"; then echo "running";
|
|
else echo "none"; fi
|
|
;;
|
|
|
|
result)
|
|
NAME="${2:?usage: mav-agent result <name>}"
|
|
if [[ -f "$RUNDIR/${NAME}.out" ]]; then cat "$RUNDIR/${NAME}.out"; else echo "no output for: $NAME"; exit 1; fi
|
|
;;
|
|
|
|
read)
|
|
NAME="${2:?usage: mav-agent read <name>}"
|
|
if ! window_exists "$NAME"; then echo "no agent window: $NAME"; exit 1; fi
|
|
TMUX= tmux capture-pane -t "${SESSION}:${NAME}" -p 2>/dev/null
|
|
;;
|
|
|
|
wait)
|
|
NAME="${2:?usage: mav-agent wait <name> [timeout_s]}"
|
|
TIMEOUT="${3:-120}"
|
|
DONE="$RUNDIR/${NAME}.done"
|
|
if [[ ! -f "$DONE" ]] && ! window_exists "$NAME"; then echo "no agent: $NAME"; exit 1; fi
|
|
echo "waiting for $NAME to finish (${TIMEOUT}s max)..."
|
|
for i in $(seq 1 $((TIMEOUT * 2))); do
|
|
if [[ -f "$DONE" ]]; then
|
|
echo "[done] exit $(cat "$DONE" 2>/dev/null | tr -d '[:space:]')"
|
|
tail -20 "$RUNDIR/${NAME}.out" 2>/dev/null
|
|
exit 0
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
echo "[timeout] agent still running after ${TIMEOUT}s"
|
|
tail -10 "$RUNDIR/${NAME}.out" 2>/dev/null
|
|
exit 2
|
|
;;
|
|
|
|
list)
|
|
echo "=== agent windows in session '$SESSION' ==="
|
|
TMUX= tmux list-windows -t "$SESSION" -F "#{window_index}: #{window_name}" 2>/dev/null | while read -r line; do
|
|
wn="${line#*: }"
|
|
if [[ -f "$RUNDIR/${wn}.done" ]]; then st="done(exit $(cat "$RUNDIR/${wn}.done" 2>/dev/null | tr -d '[:space:]'))"; else st="running"; fi
|
|
echo " $line [$st]"
|
|
done
|
|
[[ -z "$(TMUX= tmux list-windows -t "$SESSION" 2>/dev/null)" ]] && echo "(no session)"
|
|
;;
|
|
|
|
kill)
|
|
NAME="${2:?usage: mav-agent kill <name>}"
|
|
TMUX= tmux kill-window -t "${SESSION}:${NAME}" 2>/dev/null && echo "killed: $NAME" || echo "not found: $NAME"
|
|
;;
|
|
|
|
*)
|
|
echo "usage: mav-agent run <name> [--codex] <task> | read <name> | status <name> | wait <name> [s] | result <name> | list | kill <name>"
|
|
exit 1
|
|
;;
|
|
esac
|