Add orchestrate skill: main-actor pattern + mav-agent/mav-tmux wrappers

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>
This commit is contained in:
mav
2026-05-29 11:31:15 +02:00
commit f509792872
5 changed files with 348 additions and 0 deletions

126
bin/mav-agent Executable file
View File

@@ -0,0 +1,126 @@
#!/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

35
bin/mav-tmux Executable file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# mav-tmux — run a command in the persistent mav tmux session and capture output
# usage: mav-tmux run "command"
# mav-tmux read (capture current pane output)
# mav-tmux clear (clear the pane)
SESSION="mav"
# ensure session exists
TMUX= tmux has-session -t "$SESSION" 2>/dev/null || TMUX= tmux new-session -d -s "$SESSION"
case "${1:-read}" in
run)
CMD="${2:?usage: mav-tmux run <command>}"
MARKER="__DONE_$(date +%s%N)__"
TMUX= tmux send-keys -t "$SESSION" "$CMD; echo $MARKER" Enter
# wait for marker
for i in $(seq 1 60); do
sleep 0.5
OUT=$(TMUX= tmux capture-pane -t "$SESSION" -p 2>/dev/null)
if echo "$OUT" | grep -q "$MARKER"; then
echo "$OUT" | sed "/$MARKER/d" | sed '/^$/d' | tail -30
break
fi
done
;;
read)
TMUX= tmux capture-pane -t "$SESSION" -p 2>/dev/null
;;
clear)
TMUX= tmux send-keys -t "$SESSION" "clear" Enter
;;
*)
echo "usage: mav-tmux run <cmd> | read | clear"
;;
esac