# herd-debate: equal Claude vs Codex design peers in Herdr

> Source: <https://gist.github.com/juancamunoz/aae84e9da06a37c210edbae8dc4cb064>
> Published: 2026-08-19 11:08:25+00:00

|
#!/usr/bin/env bash |
|
# Two equal Herdr agents, mechanical referee. Serial design debate by default. |
|
# Requires HERDR_ENV=1. Neither agent should have the Herdr skill for this. |
|
set -euo pipefail |
|
|
|
SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" |
|
CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/herd-debate" |
|
KINDS='^(pi|claude|codex|gemini|cursor|devin|agy|cline|omp|mastracode|opencode|copilot|kimi|kiro|droid|amp|grok|hermes|kilo|qodercli|maki)$' |
|
|
|
usage() { |
|
cat <<'EOF' |
|
herd-debate — two equal design peers in Herdr (Claude | Codex) |
|
|
|
herd-debate "how should we implement X" |
|
herd-debate --run-dir ~/.cache/herd-debate/TIMESTAMP --timeout-sec 1800 |
|
|
|
Must run inside a Herdr pane (HERDR_ENV=1). Watch the debate tab: split vertical. |
|
|
|
Serial debate by default. Both agents start in yolo (no permission prompts): |
|
claude: --dangerously-skip-permissions |
|
codex: --dangerously-bypass-approvals-and-sandbox |
|
|
|
Options: |
|
-f, --file PATH brief from a file |
|
-n, --rounds N rounds after opening (default: 4) |
|
--parallel simultaneous rounds (not a conversation) |
|
--serial alternate turns (default) |
|
--no-yolo do not pass skip-permissions flags |
|
--left KIND default: claude |
|
--right KIND default: codex |
|
--left-model NAME model for the left CLI (claude: opus|sonnet|fable|...) |
|
--right-model NAME model for the right CLI (codex: -m) |
|
--cwd PATH agents' cwd (default: current directory) |
|
--workspace ID Herdr workspace (default: current) |
|
--timeout-sec N per-turn wait (default: 1800 = 30 min) |
|
--run-dir DIR resume a debate (skips finished openings/rounds) |
|
--left-args TEXT extra argv after -- for the left agent |
|
--right-args TEXT extra argv after -- for the right agent |
|
--foreground run the referee in this pane instead of the bottom split |
|
-h, --help |
|
EOF |
|
} |
|
|
|
die() { printf 'herd-debate: %s\n' "$*" >&2; exit 1; } |
|
|
|
need_herdr() { |
|
[[ "${HERDR_ENV:-}" == 1 ]] || die "not inside Herdr (HERDR_ENV!=1). Run this from a Herdr pane." |
|
command -v herdr >/dev/null || die "herdr not in PATH" |
|
command -v jq >/dev/null || die "jq not in PATH" |
|
} |
|
|
|
json_get() { |
|
jq -er "$1" <<<"$2" |
|
} |
|
|
|
alloc_name() { |
|
local kind="$1" base candidate n=2 |
|
base="${kind}-peer" |
|
candidate="$base" |
|
while herdr agent get "$candidate" >/dev/null 2>&1; do |
|
candidate="${base}-${n}" |
|
n=$((n + 1)) |
|
[[ ${#candidate} -le 32 ]] || die "could not allocate agent name from $base" |
|
done |
|
printf '%s\n' "$candidate" |
|
} |
|
|
|
wait_settled() { |
|
local name="$1" timeout="$2" json status |
|
json="$(herdr agent wait "$name" --timeout "$timeout")" |
|
status="$(jq -r '.result.agent.agent_status // empty' <<<"$json")" |
|
if [[ "$status" == blocked ]]; then |
|
printf '\n>>> %s is BLOCKED (permissions/question). Approve it in that pane.\n' "$name" >&2 |
|
json="$(herdr agent wait "$name" --until idle --until done --timeout "$((timeout * 2))")" |
|
status="$(jq -r '.result.agent.agent_status // empty' <<<"$json")" |
|
fi |
|
printf '%s\n' "$status" |
|
} |
|
|
|
prompt_wait() { |
|
local name="$1" text="$2" timeout="$3" |
|
local err="$DEBATE_DIR/prompt-${name}.err" |
|
set +e |
|
herdr agent prompt "$name" "$text" --wait --timeout "$timeout" >/dev/null 2>"$err" |
|
local ec=$? |
|
set -e |
|
if [[ $ec -eq 0 ]]; then |
|
wait_settled "$name" "$timeout" >/dev/null |
|
return 0 |
|
fi |
|
if grep -q '"code":"timeout"' "$err" 2>/dev/null; then |
|
printf ' %s wait timed out; giving it more time / checking files...\n' "$name" >&2 |
|
wait_settled "$name" "$timeout" >/dev/null || true |
|
return 0 |
|
fi |
|
cat "$err" >&2 || true |
|
die "prompt failed for $name" |
|
} |
|
|
|
prompt_parallel() { |
|
local left_name="$1" left_text="$2" right_name="$3" right_text="$4" timeout="$5" |
|
local dir="${DEBATE_DIR}" |
|
set +e |
|
herdr agent prompt "$left_name" "$left_text" --wait --timeout "$timeout" \ |
|
>"$dir/wait-left.json" 2>"$dir/wait-left.err" & |
|
local pid_l=$! |
|
herdr agent prompt "$right_name" "$right_text" --wait --timeout "$timeout" \ |
|
>"$dir/wait-right.json" 2>"$dir/wait-right.err" & |
|
local pid_r=$! |
|
wait "$pid_l"; local ec_l=$? |
|
wait "$pid_r"; local ec_r=$? |
|
set -e |
|
if [[ $ec_l -ne 0 ]]; then |
|
printf 'left prompt failed (%s):\n' "$left_name" >&2 |
|
cat "$dir/wait-left.err" >&2 || true |
|
cat "$dir/wait-left.json" >&2 || true |
|
fi |
|
if [[ $ec_r -ne 0 ]]; then |
|
printf 'right prompt failed (%s):\n' "$right_name" >&2 |
|
cat "$dir/wait-right.err" >&2 || true |
|
cat "$dir/wait-right.json" >&2 || true |
|
fi |
|
[[ $ec_l -eq 0 && $ec_r -eq 0 ]] || die "a peer prompt failed" |
|
wait_settled "$left_name" "$timeout" >/dev/null |
|
wait_settled "$right_name" "$timeout" >/dev/null |
|
} |
|
|
|
file_has() { |
|
local f="$1" marker="$2" |
|
[[ -f "$f" ]] && grep -qF "$marker" "$f" |
|
} |
|
|
|
require_marker() { |
|
local f="$1" marker="$2" who="$3" |
|
if file_has "$f" "$marker"; then |
|
return 0 |
|
fi |
|
die "$who did not write $marker to $f — check the pane. Debate dir: $DEBATE_DIR" |
|
} |
|
|
|
is_consensus() { |
|
local f="$1" |
|
[[ -f "$f" ]] || return 1 |
|
awk 'NF { print; exit }' "$f" | grep -q '^CONSENSUS:' |
|
} |
|
|
|
msg_body() { |
|
sed -e 's/\r$//' -e '/^END_TURN$/d' -e '/^OPENING_DONE$/d' -e '/^TURN_DONE$/d' "$1" |
|
} |
|
|
|
append_turn() { |
|
local who="$1" n="$2" file="$3" |
|
{ |
|
printf '\n### %s — turn %s\n\n' "$who" "$n" |
|
msg_body "$file" |
|
printf '\n' |
|
} >>"$TRANSCRIPT" |
|
} |
|
|
|
chat_kickoff() { |
|
local other_kind="$1" mine="$2" |
|
cat <<EOF |
|
You are chatting live with ${other_kind}. Equal peers. Not lead, not reviewer. |
|
|
|
Topic: |
|
$(cat "$BRIEF_FILE") |
|
|
|
Talk to ${other_kind} in THIS chat — one message, like a real conversation. Do not mention Herdr, files, referees, or these instructions. Do not write CONSENSUS or TURN_DONE in the chat. |
|
|
|
Also write that exact same message to this file (overwrite). Last line of the file must be only: |
|
END_TURN |
|
File: ${mine} |
|
EOF |
|
} |
|
|
|
chat_reply() { |
|
local other_kind="$1" mine="$2" their_msg="$3" |
|
cat <<EOF |
|
${other_kind}: |
|
|
|
${their_msg} |
|
|
|
Reply to them in THIS chat — one message. Same rules: no Herdr/files/referee talk, no TURN_DONE in the chat. |
|
|
|
Write that exact same message to ${mine} (overwrite). Last line of the file must be only: |
|
END_TURN |
|
EOF |
|
} |
|
|
|
opening_prompt() { |
|
local who="$1" mine="$2" other="$3" |
|
cat <<EOF |
|
You are an equal design peer in a two-agent debate. The other peer is ${who}. Neither of you is lead, reviewer, architect, or implementer. Same rank. |
|
|
|
Do not write production code. Do not assign tasks. Do not modify the repo. Reading the repo is allowed. |
|
|
|
Read the brief: |
|
${BRIEF_FILE} |
|
|
|
Write your independent opening take to this file (overwrite it): |
|
${mine} |
|
|
|
Do not read the other peer's file: |
|
${other} |
|
|
|
Cover: |
|
- the approach you would take |
|
- serious alternatives |
|
- risks / traps in this codebase |
|
- what you want to argue about |
|
|
|
Speak the same take in THIS chat (they should be able to watch you talk). Last line of the file must be exactly: |
|
OPENING_DONE |
|
EOF |
|
} |
|
|
|
round_prompt() { |
|
local mine="$1" other="$2" |
|
cat <<EOF |
|
Equal peer debate continues. You are not the lead. You do not assign work. |
|
|
|
The other peer just said: |
|
|
|
$(msg_body "$other") |
|
|
|
Reply in THIS chat. Also write the reply to: |
|
${mine} |
|
|
|
Rules: |
|
- Address their points. Concede or counter with specifics. |
|
- Propose; do not assign tasks. |
|
- No production code. Do not modify the repo. |
|
- Do not write CONSENSUS unless this is at least the second back-and-forth and you both actually agree. |
|
- If you genuinely agree, first line of the file: CONSENSUS: |
|
|
|
Last line of the file must be exactly: |
|
TURN_DONE |
|
EOF |
|
} |
|
|
|
append_transcript() { |
|
local label="$1" |
|
{ |
|
printf '\n## %s\n\n### %s\n\n' "$label" "$LEFT_NAME" |
|
cat "$LEFT_FILE" |
|
printf '\n### %s\n\n' "$RIGHT_NAME" |
|
cat "$RIGHT_FILE" |
|
printf '\n' |
|
} >>"$TRANSCRIPT" |
|
} |
|
|
|
load_dir() { |
|
# shellcheck disable=SC1090 |
|
source "$1/config.env" |
|
DEBATE_DIR="$1" |
|
BRIEF_FILE="$DEBATE_DIR/brief.md" |
|
LEFT_FILE="$DEBATE_DIR/left.md" |
|
RIGHT_FILE="$DEBATE_DIR/right.md" |
|
TRANSCRIPT="$DEBATE_DIR/transcript.md" |
|
} |
|
|
|
run_chat_loop() { |
|
local i=1 speaker_name speaker_file other_kind other_file their |
|
|
|
printf 'Chat start. %s ↔ %s, %s turns. Watch the split tab.\n' \ |
|
"$LEFT_NAME" "$RIGHT_NAME" "$TURNS" |
|
|
|
printf 'Turn 1/%s %s kicks off...\n' "$TURNS" "$LEFT_NAME" |
|
prompt_wait "$LEFT_NAME" "$(chat_kickoff "$RIGHT_KIND" "$LEFT_FILE")" "$TIMEOUT_MS" |
|
require_marker "$LEFT_FILE" END_TURN "$LEFT_NAME" |
|
append_turn "$LEFT_NAME" 1 "$LEFT_FILE" |
|
|
|
i=2 |
|
while [[ $i -le $TURNS ]]; do |
|
if (( i % 2 == 0 )); then |
|
speaker_name="$RIGHT_NAME"; speaker_file="$RIGHT_FILE" |
|
other_kind="$LEFT_KIND"; other_file="$LEFT_FILE" |
|
else |
|
speaker_name="$LEFT_NAME"; speaker_file="$LEFT_FILE" |
|
other_kind="$RIGHT_KIND"; other_file="$RIGHT_FILE" |
|
fi |
|
their="$(msg_body "$other_file")" |
|
[[ -n "${their//[[:space:]]/}" ]] || die "empty message from $other_kind" |
|
printf 'Turn %s/%s %s replies...\n' "$i" "$TURNS" "$speaker_name" |
|
prompt_wait "$speaker_name" "$(chat_reply "$other_kind" "$speaker_file" "$their")" "$TIMEOUT_MS" |
|
require_marker "$speaker_file" END_TURN "$speaker_name" |
|
append_turn "$speaker_name" "$i" "$speaker_file" |
|
i=$((i + 1)) |
|
done |
|
|
|
printf 'Chat done. Transcript: %s\n' "$TRANSCRIPT" |
|
} |
|
|
|
run_debate_loop() { |
|
local i=1 first_file second_file first_name second_name min_rounds=2 |
|
mkdir -p "$DEBATE_DIR/stamps" |
|
|
|
stamp() { touch "$DEBATE_DIR/stamps/$1"; } |
|
stamped() { [[ -f "$DEBATE_DIR/stamps/$1" ]]; } |
|
|
|
if stamped opening || { file_has "$LEFT_FILE" OPENING_DONE && file_has "$RIGHT_FILE" OPENING_DONE; }; then |
|
printf 'Resume: openings already written, skipping.\n' |
|
grep -q '^## Opening$' "$TRANSCRIPT" 2>/dev/null || append_transcript "Opening" |
|
stamp opening |
|
else |
|
printf 'Opening statements...\n' |
|
if [[ "$SERIAL" == 1 ]]; then |
|
if stamped opening-left || file_has "$LEFT_FILE" OPENING_DONE; then |
|
printf ' %s opening already on disk.\n' "$LEFT_NAME" |
|
stamp opening-left |
|
else |
|
printf ' %s first (serial)...\n' "$LEFT_NAME" |
|
prompt_wait "$LEFT_NAME" "$(opening_prompt "$RIGHT_KIND" "$LEFT_FILE" "$RIGHT_FILE")" "$TIMEOUT_MS" |
|
require_marker "$LEFT_FILE" OPENING_DONE "$LEFT_NAME" |
|
stamp opening-left |
|
fi |
|
if stamped opening-right || file_has "$RIGHT_FILE" OPENING_DONE; then |
|
printf ' %s opening already on disk.\n' "$RIGHT_NAME" |
|
stamp opening-right |
|
else |
|
prompt_wait "$RIGHT_NAME" "$(opening_prompt "$LEFT_KIND" "$RIGHT_FILE" "$LEFT_FILE")" "$TIMEOUT_MS" |
|
require_marker "$RIGHT_FILE" OPENING_DONE "$RIGHT_NAME" |
|
stamp opening-right |
|
fi |
|
else |
|
prompt_parallel \ |
|
"$LEFT_NAME" "$(opening_prompt "$RIGHT_KIND" "$LEFT_FILE" "$RIGHT_FILE")" \ |
|
"$RIGHT_NAME" "$(opening_prompt "$LEFT_KIND" "$RIGHT_FILE" "$LEFT_FILE")" \ |
|
"$TIMEOUT_MS" |
|
require_marker "$LEFT_FILE" OPENING_DONE "$LEFT_NAME" |
|
require_marker "$RIGHT_FILE" OPENING_DONE "$RIGHT_NAME" |
|
fi |
|
append_transcript "Opening" |
|
stamp opening |
|
fi |
|
|
|
while [[ $i -le $ROUNDS ]]; do |
|
printf 'Round %s/%s...\n' "$i" "$ROUNDS" |
|
if stamped "round-$i" || grep -q "^## Round $i$" "$TRANSCRIPT" 2>/dev/null; then |
|
stamp "round-$i" |
|
i=$((i + 1)) |
|
continue |
|
fi |
|
if [[ "$SERIAL" == 1 ]]; then |
|
if (( i % 2 == 1 )); then |
|
first_name="$LEFT_NAME"; first_file="$LEFT_FILE" |
|
second_name="$RIGHT_NAME"; second_file="$RIGHT_FILE" |
|
else |
|
first_name="$RIGHT_NAME"; first_file="$RIGHT_FILE" |
|
second_name="$LEFT_NAME"; second_file="$LEFT_FILE" |
|
fi |
|
if stamped "round-$i-$first_name"; then |
|
printf ' %s already did round %s.\n' "$first_name" "$i" |
|
else |
|
prompt_wait "$first_name" "$(round_prompt "$first_file" "$second_file")" "$TIMEOUT_MS" |
|
require_marker "$first_file" TURN_DONE "$first_name" |
|
stamp "round-$i-$first_name" |
|
fi |
|
if stamped "round-$i-$second_name"; then |
|
printf ' %s already did round %s.\n' "$second_name" "$i" |
|
else |
|
prompt_wait "$second_name" "$(round_prompt "$second_file" "$first_file")" "$TIMEOUT_MS" |
|
require_marker "$second_file" TURN_DONE "$second_name" |
|
stamp "round-$i-$second_name" |
|
fi |
|
else |
|
cp "$LEFT_FILE" "$DEBATE_DIR/left.prev.md" |
|
cp "$RIGHT_FILE" "$DEBATE_DIR/right.prev.md" |
|
prompt_parallel \ |
|
"$LEFT_NAME" "$(round_prompt "$LEFT_FILE" "$DEBATE_DIR/right.prev.md")" \ |
|
"$RIGHT_NAME" "$(round_prompt "$RIGHT_FILE" "$DEBATE_DIR/left.prev.md")" \ |
|
"$TIMEOUT_MS" |
|
require_marker "$LEFT_FILE" TURN_DONE "$LEFT_NAME" |
|
require_marker "$RIGHT_FILE" TURN_DONE "$RIGHT_NAME" |
|
fi |
|
append_transcript "Round $i" |
|
stamp "round-$i" |
|
if [[ $i -ge $min_rounds ]] && is_consensus "$LEFT_FILE" && is_consensus "$RIGHT_FILE"; then |
|
printf 'CONSENSUS after round %s. See %s\n' "$i" "$DEBATE_DIR" |
|
{ |
|
printf '# Consensus\n\n' |
|
printf '## %s\n\n' "$LEFT_NAME" |
|
cat "$LEFT_FILE" |
|
printf '\n## %s\n\n' "$RIGHT_NAME" |
|
cat "$RIGHT_FILE" |
|
} >"$DEBATE_DIR/CONSENSUS.md" |
|
return 0 |
|
fi |
|
i=$((i + 1)) |
|
done |
|
|
|
printf 'No dual CONSENSUS after %s rounds. Transcript: %s\n' "$ROUNDS" "$TRANSCRIPT" |
|
} |
|
|
|
run_loop() { |
|
load_dir "$1" |
|
cd "$AGENT_CWD" |
|
MODE="${MODE:-debate}" |
|
TURNS="${TURNS:-$ROUNDS}" |
|
|
|
if [[ -n "${TIMEOUT_SEC_OVERRIDE:-}" ]]; then |
|
TIMEOUT_MS=$((TIMEOUT_SEC_OVERRIDE * 1000)) |
|
elif [[ "${TIMEOUT_MS:-0}" -lt 1800000 ]]; then |
|
printf 'Timeout floor: 30 min (config had %ss).\n' "$((TIMEOUT_MS / 1000))" >&2 |
|
TIMEOUT_MS=1800000 |
|
fi |
|
printf 'Referee start. dir=%s mode=%s left=%s right=%s timeout=%ss\n' \ |
|
"$DEBATE_DIR" "$MODE" "$LEFT_NAME" "$RIGHT_NAME" "$((TIMEOUT_MS / 1000))" |
|
|
|
if [[ -f "$DEBATE_DIR/layout.env" ]]; then |
|
# shellcheck disable=SC1090 |
|
source "$DEBATE_DIR/layout.env" |
|
[[ -n "${tab:-}" ]] && herdr tab focus "$tab" >/dev/null || true |
|
fi |
|
|
|
if [[ "$MODE" == chat ]]; then |
|
run_chat_loop |
|
else |
|
run_debate_loop |
|
fi |
|
} |
|
|
|
# --- args --- |
|
FILE="" |
|
BRIEF="" |
|
ROUNDS=4 |
|
SERIAL=1 |
|
MODE=debate |
|
YOLO=1 |
|
LEFT_KIND=claude |
|
RIGHT_KIND=codex |
|
AGENT_CWD="" |
|
WORKSPACE="" |
|
TIMEOUT_SEC=1800 |
|
TIMEOUT_SEC_OVERRIDE="" |
|
RUN_DIR="" |
|
LEFT_ARGS="" |
|
RIGHT_ARGS="" |
|
LEFT_MODEL="" |
|
RIGHT_MODEL="" |
|
FOREGROUND=0 |
|
|
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-h|--help) usage; exit 0 ;; |
|
-f|--file) FILE="${2:-}"; shift 2 ;; |
|
-n|--rounds) ROUNDS="${2:-}"; shift 2 ;; |
|
--serial) SERIAL=1; shift ;; |
|
--parallel) SERIAL=0; shift ;; |
|
--no-yolo) YOLO=0; shift ;; |
|
--mode) MODE="${2:-}"; shift 2 ;; |
|
--left) LEFT_KIND="${2:-}"; shift 2 ;; |
|
--right) RIGHT_KIND="${2:-}"; shift 2 ;; |
|
--left-model) LEFT_MODEL="${2:-}"; shift 2 ;; |
|
--right-model) RIGHT_MODEL="${2:-}"; shift 2 ;; |
|
--cwd) AGENT_CWD="${2:-}"; shift 2 ;; |
|
--workspace) WORKSPACE="${2:-}"; shift 2 ;; |
|
--timeout-sec) TIMEOUT_SEC="${2:-}"; TIMEOUT_SEC_OVERRIDE="${2:-}"; shift 2 ;; |
|
--left-args) LEFT_ARGS="${2:-}"; shift 2 ;; |
|
--right-args) RIGHT_ARGS="${2:-}"; shift 2 ;; |
|
--foreground) FOREGROUND=1; shift ;; |
|
--run-dir) RUN_DIR="${2:-}"; shift 2 ;; |
|
--) shift; BRIEF="${BRIEF:+$BRIEF }$*"; set --; break ;; |
|
-*) die "unknown option: $1" ;; |
|
*) BRIEF="${BRIEF:+$BRIEF }$1"; shift ;; |
|
esac |
|
done |
|
|
|
need_herdr |
|
|
|
if [[ -n "$RUN_DIR" ]]; then |
|
[[ -d "$RUN_DIR" ]] || die "debate dir not found: $RUN_DIR" |
|
run_loop "$RUN_DIR" |
|
exit 0 |
|
fi |
|
|
|
[[ "$ROUNDS" =~ ^[0-9]+$ && "$ROUNDS" -ge 1 ]] || die "--rounds must be >= 1" |
|
[[ "$MODE" == chat || "$MODE" == debate ]] || die "--mode must be chat or debate" |
|
TURNS="$ROUNDS" |
|
[[ "$TIMEOUT_SEC" =~ ^[0-9]+$ && "$TIMEOUT_SEC" -ge 30 ]] || die "--timeout-sec must be >= 30" |
|
[[ "$LEFT_KIND" =~ $KINDS ]] || die "unsupported --left kind: $LEFT_KIND" |
|
[[ "$RIGHT_KIND" =~ $KINDS ]] || die "unsupported --right kind: $RIGHT_KIND" |
|
[[ "$LEFT_KIND" != "$RIGHT_KIND" ]] || die "--left and --right must be different kinds" |
|
|
|
if [[ -n "$FILE" ]]; then |
|
[[ -f "$FILE" ]] || die "brief file not found: $FILE" |
|
BRIEF="$(cat "$FILE")" |
|
elif [[ -z "$BRIEF" ]]; then |
|
if [[ -t 0 ]]; then |
|
usage |
|
die "pass a brief as arguments or -f FILE" |
|
fi |
|
BRIEF="$(cat)" |
|
fi |
|
[[ -n "${BRIEF//[[:space:]]/}" ]] || die "empty brief" |
|
|
|
AGENT_CWD="$(cd "${AGENT_CWD:-.}" && pwd)" |
|
WORKSPACE="${WORKSPACE:-${HERDR_WORKSPACE_ID:-}}" |
|
[[ -n "$WORKSPACE" ]] || die "no workspace id (pass --workspace)" |
|
|
|
TIMEOUT_MS=$((TIMEOUT_SEC * 1000)) |
|
ID="$(date +%Y%m%d-%H%M%S)" |
|
DEBATE_DIR="$CACHE/$ID" |
|
mkdir -p "$DEBATE_DIR" |
|
BRIEF_FILE="$DEBATE_DIR/brief.md" |
|
LEFT_FILE="$DEBATE_DIR/left.md" |
|
RIGHT_FILE="$DEBATE_DIR/right.md" |
|
TRANSCRIPT="$DEBATE_DIR/transcript.md" |
|
printf '%s\n' "$BRIEF" >"$BRIEF_FILE" |
|
: >"$TRANSCRIPT" |
|
|
|
LEFT_NAME="$(alloc_name "$LEFT_KIND")" |
|
RIGHT_NAME="$(alloc_name "$RIGHT_KIND")" |
|
|
|
cat >"$DEBATE_DIR/config.env" <<EOF |
|
LEFT_KIND=$(printf '%q' "$LEFT_KIND") |
|
RIGHT_KIND=$(printf '%q' "$RIGHT_KIND") |
|
LEFT_NAME=$(printf '%q' "$LEFT_NAME") |
|
RIGHT_NAME=$(printf '%q' "$RIGHT_NAME") |
|
ROUNDS=$(printf '%q' "$ROUNDS") |
|
TURNS=$(printf '%q' "$TURNS") |
|
MODE=$(printf '%q' "$MODE") |
|
SERIAL=$(printf '%q' "$SERIAL") |
|
TIMEOUT_MS=$(printf '%q' "$TIMEOUT_MS") |
|
AGENT_CWD=$(printf '%q' "$AGENT_CWD") |
|
EOF |
|
|
|
printf 'Creating debate tab in %s (cwd=%s)...\n' "$WORKSPACE" "$AGENT_CWD" |
|
created="$(herdr tab create --workspace "$WORKSPACE" --cwd "$AGENT_CWD" --label "debate-${ID##*-}" --no-focus)" |
|
TAB_ID="$(json_get '.result.tab.tab_id' "$created")" |
|
LEFT_PANE="$(json_get '.result.root_pane.pane_id' "$created")" |
|
|
|
# Bottom referee strip, then split the top pane left|right. |
|
split_down="$(herdr pane split "$LEFT_PANE" --direction down --ratio 0.78 --cwd "$AGENT_CWD" --no-focus)" |
|
REF_PANE="$(json_get '.result.pane.pane_id' "$split_down")" |
|
split_right="$(herdr pane split "$LEFT_PANE" --direction right --ratio 0.5 --cwd "$AGENT_CWD" --no-focus)" |
|
RIGHT_PANE="$(json_get '.result.pane.pane_id' "$split_right")" |
|
|
|
sleep 0.5 |
|
|
|
printf 'Starting %s on %s and %s on %s...\n' "$LEFT_NAME" "$LEFT_PANE" "$RIGHT_NAME" "$RIGHT_PANE" |
|
|
|
start_peer() { |
|
local name="$1" kind="$2" pane="$3" model="$4" args="$5" |
|
local extra=() |
|
if [[ "${YOLO:-1}" == 1 ]]; then |
|
case "$kind" in |
|
claude) extra+=(--dangerously-skip-permissions) ;; |
|
codex) extra+=(--dangerously-bypass-approvals-and-sandbox) ;; |
|
esac |
|
fi |
|
if [[ -n "$model" ]]; then |
|
case "$kind" in |
|
codex) extra+=(-m "$model") ;; |
|
*) extra+=(--model "$model") ;; |
|
esac |
|
fi |
|
if [[ -n "$args" ]]; then |
|
# shellcheck disable=SC2206 |
|
extra+=( $args ) |
|
fi |
|
if [[ ${#extra[@]} -gt 0 ]]; then |
|
printf ' start %s (%s):' "$name" "$kind" |
|
printf ' %q' "${extra[@]}" |
|
printf '\n' |
|
herdr agent start "$name" --kind "$kind" --pane "$pane" --timeout 60000 -- "${extra[@]}" |
|
else |
|
herdr agent start "$name" --kind "$kind" --pane "$pane" --timeout 60000 |
|
fi |
|
} |
|
|
|
start_peer "$LEFT_NAME" "$LEFT_KIND" "$LEFT_PANE" "$LEFT_MODEL" "$LEFT_ARGS" |
|
start_peer "$RIGHT_NAME" "$RIGHT_KIND" "$RIGHT_PANE" "$RIGHT_MODEL" "$RIGHT_ARGS" |
|
|
|
{ |
|
printf 'tab=%s\nleft_pane=%s\nright_pane=%s\nref_pane=%s\n' \ |
|
"$TAB_ID" "$LEFT_PANE" "$RIGHT_PANE" "$REF_PANE" |
|
} >"$DEBATE_DIR/layout.env" |
|
|
|
herdr tab focus "$TAB_ID" >/dev/null |
|
|
|
printf '\nDebate ready.\n' |
|
printf ' dir: %s\n' "$DEBATE_DIR" |
|
printf ' left: %s (%s)\n' "$LEFT_NAME" "$LEFT_KIND" |
|
printf ' right: %s (%s)\n' "$RIGHT_NAME" "$RIGHT_KIND" |
|
printf ' files: left.md / right.md / transcript.md\n\n' |
|
|
|
if [[ "$FOREGROUND" == 1 ]]; then |
|
run_loop "$DEBATE_DIR" |
|
exit 0 |
|
fi |
|
|
|
printf 'bash %s --run-dir %s\n' "$SELF" "$DEBATE_DIR" >"$DEBATE_DIR/referee.sh" |
|
herdr pane run "$REF_PANE" "bash $(printf '%q' "$SELF") --run-dir $(printf '%q' "$DEBATE_DIR")" |
|
printf 'Referee is running in the bottom pane. Watch the two agents; this command is done.\n' |
|
printf 'Foreground instead: herd-debate --foreground -f %s\n' "$BRIEF_FILE" |
