Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .agent/scripts/start-parallel-agents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
# Options:
# --mux tmux|cmux Terminal multiplexer to use (default: auto-detect)
# --agent claude|gemini|codex|droid Agent framework to use (default: auto-detect)
# --issue-source file|github|jira|ado Issue backend for this swarm run
# --issue-dir PATH File issue directory when --issue-source file
# --no-worktrees Run all agents in the same directory
#
# Examples:
# start-parallel-agents.sh 3 --mux tmux --agent claude
# start-parallel-agents.sh 4 --mux cmux --agent gemini
# start-parallel-agents.sh 3 --mux tmux --agent codex
# start-parallel-agents.sh 3 --mux tmux --agent droid
# start-parallel-agents.sh 5 --mux tmux --agent codex --issue-source github
# start-parallel-agents.sh 2 --no-worktrees

set -e
Expand All @@ -30,12 +33,16 @@ AGENTS_REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"

# shellcheck source=mux-send-lib.sh
source "$SCRIPT_DIR/mux-send-lib.sh"
# shellcheck source=issue-source-lib.sh
source "$SCRIPT_DIR/issue-source-lib.sh"

# Defaults
NUM_AGENTS=3
USE_WORKTREES=true
MUX=""
AGENT=""
CLI_ISSUE_SOURCE=""
CLI_ISSUE_DIR=""

# Parse arguments
while [[ $# -gt 0 ]]; do
Expand All @@ -52,6 +59,14 @@ while [[ $# -gt 0 ]]; do
USE_WORKTREES=false
shift
;;
--issue-source)
CLI_ISSUE_SOURCE="$2"
shift 2
;;
--issue-dir)
CLI_ISSUE_DIR="$2"
shift 2
;;
[0-9]*)
NUM_AGENTS="$1"
shift
Expand All @@ -62,13 +77,16 @@ while [[ $# -gt 0 ]]; do
echo "Options:"
echo " --mux tmux|cmux Terminal multiplexer (default: auto-detect)"
echo " --agent claude|gemini|codex|droid Agent framework (default: auto-detect)"
echo " --issue-source file|github|jira|ado Issue backend for this swarm run"
echo " --issue-dir PATH File issue directory when --issue-source file"
echo " --no-worktrees Run all agents in the same directory"
echo ""
echo "Examples:"
echo " start-parallel-agents.sh 3 --mux tmux --agent claude"
echo " start-parallel-agents.sh 4 --mux cmux --agent gemini"
echo " start-parallel-agents.sh 3 --mux tmux --agent codex"
echo " start-parallel-agents.sh 3 --mux tmux --agent droid"
echo " start-parallel-agents.sh 5 --mux tmux --agent codex --issue-source github"
exit 0
;;
*)
Expand Down Expand Up @@ -199,6 +217,14 @@ PROJECT_NAME=$(basename "$PROJECT_ROOT")
SESSION_NAME="${AGENT}-${PROJECT_NAME}"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")

# Resolve the backend once, in the dispatcher, and propagate it to every worker
# below via issue_env_exports. Workers also source issue-config.sh, which reads
# .autocoder.json per worktree — but that only agrees with the dispatcher by
# coincidence. With `--issue-dir` the dispatcher's directory exists nowhere in
# config, so without this propagation workers resolve a different issue store
# than the one the swarm was pointed at.
resolve_effective_issue_source "$CLI_ISSUE_SOURCE" "$CLI_ISSUE_DIR" "$(resolve_main_worktree)" || exit 1

# Generate shared task list ID for coordination
TASK_LIST_ID="parallel-$(date +%Y%m%d-%H%M%S)"

Expand Down Expand Up @@ -263,6 +289,13 @@ else
echo ""
fi

send_issue_env_tmux() {
local target="$1"
while IFS= read -r line; do
[ -n "$line" ] && tmux send-keys -t "$target" "$line" C-m
done < <(issue_env_exports)
}

# ============================================================================
# TMUX MODE
# ============================================================================
Expand All @@ -287,6 +320,9 @@ if [ "$MUX" = "tmux" ]; then
else
tmux send-keys -t "$SESSION_NAME:0.0" "cd '$PROJECT_ROOT'" C-m
fi
# Unconditional: every agent framework reads ISSUE_SOURCE, so this must not
# sit behind the claude-only coordination guard below.
send_issue_env_tmux "$SESSION_NAME:0.0"

# Set environment variables for Antigravity coordination
if [ "$AGENT" = "claude" ]; then
Expand All @@ -304,6 +340,7 @@ if [ "$MUX" = "tmux" ]; then
else
tmux send-keys -t "$SESSION_NAME:0.$((i-1))" "cd '$PROJECT_ROOT'" C-m
fi
send_issue_env_tmux "$SESSION_NAME:0.$((i-1))"

if [ "$AGENT" = "claude" ]; then
tmux send-keys -t "$SESSION_NAME:0.$((i-1))" "export ANTIGRAVITY_TASK_LIST_ID='$TASK_LIST_ID'" C-m
Expand Down Expand Up @@ -352,6 +389,7 @@ if [ "$MUX" = "tmux" ]; then
# Set up review window
echo " Starting coordinator..."
tmux send-keys -t "$SESSION_NAME:1.0" "cd '$PROJECT_ROOT'" C-m
send_issue_env_tmux "$SESSION_NAME:1.0"

if [ "$AGENT" = "claude" ]; then
tmux send-keys -t "$SESSION_NAME:1.0" "export ANTIGRAVITY_TASK_LIST_ID='$TASK_LIST_ID'" C-m
Expand Down Expand Up @@ -413,6 +451,15 @@ elif [ "$MUX" = "cmux" ]; then
cmux send-key --workspace "$ws_ref" enter >/dev/null
}

# Defined here rather than at file scope because it depends on cmux_send_cmd,
# which is itself local to this branch.
send_issue_env_cmux() {
local workspace="$1"
while IFS= read -r line; do
[ -n "$line" ] && cmux_send_cmd "$workspace" "$line"
done < <(issue_env_exports)
}

# Extract workspace ref (e.g. "workspace:3") from "OK workspace:3" output
parse_ws_ref() {
echo "$1" | grep -o 'workspace:[0-9]*'
Expand Down Expand Up @@ -447,6 +494,8 @@ elif [ "$MUX" = "cmux" ]; then
# Rename workspace: wt<N>-<project>
cmux rename-workspace --workspace "$WS_REF" "wt${i}-${PROJECT_NAME}" >/dev/null || true

send_issue_env_cmux "$WS_REF"

# Set environment variables for Antigravity coordination
if [ "$AGENT" = "claude" ]; then
cmux_send_cmd "$WS_REF" "export ANTIGRAVITY_TASK_LIST_ID='$TASK_LIST_ID'"
Expand Down Expand Up @@ -483,6 +532,8 @@ elif [ "$MUX" = "cmux" ]; then
# Manager workspace: manager-<project> (same pattern as wt<N>-<project> workers)
cmux rename-workspace --workspace "$REVIEW_WS_REF" "manager-${PROJECT_NAME}" >/dev/null || true

send_issue_env_cmux "$REVIEW_WS_REF"

if [ "$AGENT" = "claude" ]; then
cmux_send_cmd "$REVIEW_WS_REF" "export ANTIGRAVITY_TASK_LIST_ID='$TASK_LIST_ID'"
sleep 0.5
Expand Down
48 changes: 48 additions & 0 deletions tests/test_agent_mirror_parity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,54 @@ for side in "plugins:$PLUGIN_DIR" "mirror:$AGENT_DIR"; do
fi
done

# ── start-parallel-agents.sh: behavioural parity, not byte parity ─────────
# This pair CANNOT join SHARED above: the two legitimately differ (tmux/cmux
# launch mechanics, ANTIGRAVITY_* vs CLAUDE_CODE_* env names), so a diff check
# would fail permanently and teach everyone to ignore it. What must match is
# the contract: same issue CLI surface, and the dispatcher's resolved backend
# actually reaching the workers.
#
# The mirror shipped issue-source-lib.sh (via the fix for #71) while its only
# consumer still never called it — 233 lines behind, no --issue-source flag,
# and workers launched without ISSUE_SOURCE/ISSUE_DIR_PATH. See #88.
SPA="start-parallel-agents.sh"

# `--help` exits during argument parsing, before any multiplexer is touched, so
# running it is hermetic. Asserting on real output beats grepping the source:
# a flag can be documented in a comment and still not be parsed.
for side in "plugins:$PLUGIN_DIR" "mirror:$AGENT_DIR"; do
label="${side%%:*}"; dir="${side#*:}"
if [ ! -f "$dir/$SPA" ]; then
bad "$label: $SPA missing"
continue
fi
help_out=$(bash "$dir/$SPA" --help 2>&1) || true
for flag in --issue-source --issue-dir; do
if echo "$help_out" | grep -qF -- "$flag"; then
ok "$label $SPA documents $flag"
else
bad "$label $SPA does not document $flag"
fi
done
done

# The flags must be *parsed*, the backend *resolved*, and the result *sent* to
# workers. Each of the three is a separate way to be half-wired, so assert all
# three rather than only that the library is sourced.
for side in "plugins:$PLUGIN_DIR" "mirror:$AGENT_DIR"; do
label="${side%%:*}"; dir="${side#*:}"
[ -f "$dir/$SPA" ] || continue
src=$(cat "$dir/$SPA")
for needle in 'issue-source-lib.sh' '--issue-source)' '--issue-dir)' \
'resolve_effective_issue_source' 'issue_env_exports'; do
if echo "$src" | grep -qF -- "$needle"; then
ok "$label $SPA has $needle"
else
bad "$label $SPA is missing $needle"
fi
done
done

# ── The real gh must never have been reached ──────────────────────────────
if [ -f "$GH_TRIPWIRE" ]; then
bad "test shelled out to real gh: $(head -1 "$GH_TRIPWIRE")"
Expand Down
Loading