249 lines
7.5 KiB
Bash
249 lines
7.5 KiB
Bash
|
|
#!/bin/bash
|
|||
|
|
# spawn.sh — launch and manage autonomous research groups
|
|||
|
|
#
|
|||
|
|
# Usage:
|
|||
|
|
# bash spawn.sh launch <tag> <agent:gpu> [agent:gpu ...]
|
|||
|
|
# bash spawn.sh stop <tag>
|
|||
|
|
# bash spawn.sh status
|
|||
|
|
#
|
|||
|
|
# Examples:
|
|||
|
|
# bash spawn.sh launch mar5 claude:0 claude:1 claude:2 claude:3
|
|||
|
|
# bash spawn.sh launch mar5 opus:0 sonnet:1 codex:2 codex:3
|
|||
|
|
# bash spawn.sh stop mar5
|
|||
|
|
# bash spawn.sh status
|
|||
|
|
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|||
|
|
WORKTREE_DIR="${REPO_ROOT}/worktrees"
|
|||
|
|
INITIAL_PROMPT="Read program.md and follow the instructions."
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Helpers
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
usage() {
|
|||
|
|
echo "Usage:"
|
|||
|
|
echo " $0 launch <tag> <agent:gpu> [agent:gpu ...]"
|
|||
|
|
echo " $0 stop <tag>"
|
|||
|
|
echo " $0 status"
|
|||
|
|
echo ""
|
|||
|
|
echo "Examples:"
|
|||
|
|
echo " $0 launch mar5 claude:0 claude:1 claude:2 claude:3"
|
|||
|
|
echo " $0 launch mar5 opus:0 sonnet:1 codex:2 codex:3"
|
|||
|
|
echo " $0 stop mar5"
|
|||
|
|
exit 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setup_worker() {
|
|||
|
|
local tag="$1" gpu="$2" agent="$3"
|
|||
|
|
local branch="autoresearch/${tag}-gpu${gpu}"
|
|||
|
|
local worktree="${WORKTREE_DIR}/gpu${gpu}"
|
|||
|
|
local claude_model="${CLAUDE_MODEL:-sonnet}"
|
|||
|
|
|
|||
|
|
# Create branch (must be fresh)
|
|||
|
|
cd "$REPO_ROOT"
|
|||
|
|
if git show-ref --verify --quiet "refs/heads/${branch}"; then
|
|||
|
|
echo "ERROR: Branch '${branch}' already exists. Use a different tag or clean up first." >&2
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
git checkout -q master
|
|||
|
|
git checkout -q -b "$branch"
|
|||
|
|
git checkout -q master
|
|||
|
|
|
|||
|
|
# Set up worktree
|
|||
|
|
if [ -d "$worktree" ]; then
|
|||
|
|
git worktree remove "$worktree" --force 2>/dev/null || rm -rf "$worktree"
|
|||
|
|
fi
|
|||
|
|
mkdir -p "$(dirname "$worktree")"
|
|||
|
|
git worktree add "$worktree" "$branch"
|
|||
|
|
|
|||
|
|
# Symlink .venv so uv/python work
|
|||
|
|
ln -sf "${REPO_ROOT}/.venv" "${worktree}/.venv"
|
|||
|
|
|
|||
|
|
# Build agent command
|
|||
|
|
case "$agent" in
|
|||
|
|
claude|sonnet|opus|haiku)
|
|||
|
|
case "$agent" in
|
|||
|
|
sonnet|opus|haiku) claude_model="$agent" ;;
|
|||
|
|
esac
|
|||
|
|
echo "cd ${worktree} && CUDA_VISIBLE_DEVICES=${gpu} claude --dangerously-skip-permissions --model ${claude_model} \"${INITIAL_PROMPT}\""
|
|||
|
|
;;
|
|||
|
|
codex)
|
|||
|
|
echo "cd ${worktree} && CUDA_VISIBLE_DEVICES=${gpu} codex --dangerously-bypass-approvals-and-sandbox --model gpt-5.3-codex-spark \"${INITIAL_PROMPT}\""
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
echo "ERROR: Unknown agent '${agent}'. Supported: claude, sonnet, opus, haiku, codex" >&2
|
|||
|
|
git worktree remove "$worktree" --force 2>/dev/null || true
|
|||
|
|
git branch -D "$branch" 2>/dev/null || true
|
|||
|
|
exit 1
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Commands
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
cmd_launch() {
|
|||
|
|
if [ $# -lt 2 ]; then
|
|||
|
|
usage
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local tag="$1"
|
|||
|
|
shift
|
|||
|
|
local specs=("$@")
|
|||
|
|
local tmux_session="autoresearch-${tag}"
|
|||
|
|
local num_workers=${#specs[@]}
|
|||
|
|
|
|||
|
|
# Kill existing session
|
|||
|
|
tmux kill-session -t "$tmux_session" 2>/dev/null || true
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
echo "============================================"
|
|||
|
|
echo " RESEARCH GROUP: ${tag}"
|
|||
|
|
echo "============================================"
|
|||
|
|
echo " Workers: ${num_workers}"
|
|||
|
|
|
|||
|
|
local pane_cmds=()
|
|||
|
|
local labels=()
|
|||
|
|
local agents=()
|
|||
|
|
|
|||
|
|
for spec in "${specs[@]}"; do
|
|||
|
|
local agent="${spec%%:*}"
|
|||
|
|
local gpu="${spec#*:}"
|
|||
|
|
|
|||
|
|
echo " - GPU ${gpu}: ${agent}"
|
|||
|
|
|
|||
|
|
local cmd
|
|||
|
|
cmd=$(setup_worker "$tag" "$gpu" "$agent")
|
|||
|
|
pane_cmds+=("$cmd")
|
|||
|
|
labels+=("$spec")
|
|||
|
|
case "$agent" in
|
|||
|
|
sonnet|opus|haiku|claude) agents+=("claude") ;;
|
|||
|
|
*) agents+=("$agent") ;;
|
|||
|
|
esac
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
echo " tmux: ${tmux_session}"
|
|||
|
|
echo "============================================"
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# Create tmux session with tiled grid
|
|||
|
|
tmux new-session -d -s "$tmux_session" -n workers \
|
|||
|
|
"${pane_cmds[0]}; echo ''; echo 'Session ended. Press any key to exit.'; read"
|
|||
|
|
|
|||
|
|
for ((i=1; i<num_workers; i++)); do
|
|||
|
|
tmux split-window -t "$tmux_session:workers" \
|
|||
|
|
"${pane_cmds[$i]}; echo ''; echo 'Session ended. Press any key to exit.'; read"
|
|||
|
|
tmux select-layout -t "$tmux_session:workers" tiled
|
|||
|
|
done
|
|||
|
|
tmux select-layout -t "$tmux_session:workers" tiled
|
|||
|
|
|
|||
|
|
echo " Attach: tmux attach -t ${tmux_session}"
|
|||
|
|
echo " Detach: Ctrl-b d"
|
|||
|
|
echo " Zoom: Ctrl-b z (toggle pane fullscreen)"
|
|||
|
|
echo " Navigate: Ctrl-b arrow keys"
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# Background watcher — nudge idle agents
|
|||
|
|
local nudge_interval=120
|
|||
|
|
echo "Watcher running (nudge interval: ${nudge_interval}s). Ctrl-C to stop watcher only."
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
declare -A prev_hash
|
|||
|
|
|
|||
|
|
while tmux has-session -t "$tmux_session" 2>/dev/null; do
|
|||
|
|
sleep "$nudge_interval"
|
|||
|
|
|
|||
|
|
for ((i=0; i<num_workers; i++)); do
|
|||
|
|
tmux has-session -t "$tmux_session" 2>/dev/null || break
|
|||
|
|
|
|||
|
|
local pane="${tmux_session}:workers.${i}"
|
|||
|
|
local content curr_hash
|
|||
|
|
|
|||
|
|
content=$(tmux capture-pane -t "$pane" -p 2>/dev/null) || continue
|
|||
|
|
curr_hash=$(echo "$content" | md5sum | cut -d' ' -f1)
|
|||
|
|
|
|||
|
|
if [ "${prev_hash[$i]:-}" != "$curr_hash" ]; then
|
|||
|
|
prev_hash[$i]="$curr_hash"
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local is_idle=false
|
|||
|
|
if echo "$content" | grep -qP '^❯\s*$'; then
|
|||
|
|
is_idle=true
|
|||
|
|
elif echo "$content" | grep -qP '^›\s*$'; then
|
|||
|
|
is_idle=true
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [ "$is_idle" = true ]; then
|
|||
|
|
local nudge_msg="Keep going. Do not stop — continue your research loop."
|
|||
|
|
if [ "${agents[$i]}" = "codex" ]; then
|
|||
|
|
tmux send-keys -t "$pane" "$nudge_msg" Enter
|
|||
|
|
else
|
|||
|
|
tmux send-keys -t "$pane" "$nudge_msg" C-m
|
|||
|
|
fi
|
|||
|
|
echo "[$(date +%H:%M:%S)] Nudged pane ${i} (${labels[$i]})"
|
|||
|
|
prev_hash[$i]=""
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
echo "tmux session ended. Watcher exiting."
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cmd_stop() {
|
|||
|
|
if [ $# -lt 1 ]; then
|
|||
|
|
usage
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local tag="$1"
|
|||
|
|
local tmux_session="autoresearch-${tag}"
|
|||
|
|
|
|||
|
|
# Kill tmux session
|
|||
|
|
if tmux kill-session -t "$tmux_session" 2>/dev/null; then
|
|||
|
|
echo "Killed tmux session '${tmux_session}'"
|
|||
|
|
else
|
|||
|
|
echo "No tmux session '${tmux_session}' found"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Remove worktrees
|
|||
|
|
local removed=0
|
|||
|
|
for wt in "${WORKTREE_DIR}"/gpu*; do
|
|||
|
|
[ -d "$wt" ] || continue
|
|||
|
|
git -C "$REPO_ROOT" worktree remove "$wt" --force 2>/dev/null && removed=$((removed + 1))
|
|||
|
|
done
|
|||
|
|
echo "Removed ${removed} worktree(s)"
|
|||
|
|
echo "Branches kept for review (git branch -l 'autoresearch/${tag}-*')"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cmd_status() {
|
|||
|
|
echo "Active tmux sessions:"
|
|||
|
|
tmux list-sessions 2>/dev/null | grep "^autoresearch-" || echo " (none)"
|
|||
|
|
echo ""
|
|||
|
|
echo "Active worktrees:"
|
|||
|
|
git -C "$REPO_ROOT" worktree list 2>/dev/null | grep "worktrees/" || echo " (none)"
|
|||
|
|
echo ""
|
|||
|
|
echo "Research branches:"
|
|||
|
|
git -C "$REPO_ROOT" branch -l 'autoresearch/*' 2>/dev/null || echo " (none)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Main
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
if [ $# -lt 1 ]; then
|
|||
|
|
usage
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
COMMAND="$1"
|
|||
|
|
shift
|
|||
|
|
|
|||
|
|
case "$COMMAND" in
|
|||
|
|
launch) cmd_launch "$@" ;;
|
|||
|
|
stop) cmd_stop "$@" ;;
|
|||
|
|
status) cmd_status "$@" ;;
|
|||
|
|
*) usage ;;
|
|||
|
|
esac
|