refactor: remove headless mode

Headless mode was half-built and untested. Agent-loop is a plugin
that runs interactively via tmux — there's no CI use case yet.
Removes --headless flag, timeout compatibility shim, output capture
logic, and LOOP_AGENT_TMPFILE handling. Cuts 82 lines from loop.sh.
This commit is contained in:
2026-03-28 12:17:30 -04:00
parent b4d4e1952a
commit c46de6815c
3 changed files with 33 additions and 129 deletions

146
loop.sh
View File

@@ -13,7 +13,6 @@
# --no-hooks Don't install stop hooks
# --dry-run Print assembled prompts without running agents
# --resume Skip already-passed stories (explicit mode)
# --replan (reserved — not yet implemented)
#
# Each iteration:
# 1. Generator: picks highest-priority incomplete story, does the work
@@ -81,23 +80,6 @@ if ! command -v jq &>/dev/null && ! command -v python3 &>/dev/null; then
exit 1
fi
# --- macOS timeout compatibility ---
# macOS doesn't have GNU timeout. Use gtimeout (from coreutils) or a perl fallback.
if ! command -v timeout &>/dev/null; then
if command -v gtimeout &>/dev/null; then
timeout() { gtimeout "$@"; }
else
# Perl-based fallback: runs command with alarm signal
timeout() {
local duration="$1"; shift
perl -e '
alarm shift @ARGV;
exec @ARGV;
' "$duration" "$@"
}
fi
fi
# --- Load config defaults ---
CONFIG_FILE="$LOOP_DIR/config.json"
config_default() { get_config_value "$1" "$2"; }
@@ -122,9 +104,7 @@ while [[ $# -gt 0 ]]; do
--tool=*) TOOL="${1#*=}"; shift ;;
--no-hooks) AUTO_HOOKS=false; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--headless) export LOOP_HEADLESS=true; shift ;;
--resume) RESUME=true; shift ;;
--replan) log "ERROR: --replan is not yet implemented. Use /agent-loop:stories interactively."; exit 1 ;;
[0-9]*) MAX_ITERATIONS="$1"; shift ;;
*) log "Unknown option: $1"; exit 1 ;;
esac
@@ -147,7 +127,6 @@ fi
cd "$PROJECT_ROOT"
cleanup() {
[ -n "${LOOP_AGENT_TMPFILE:-}" ] && rm -f "$LOOP_AGENT_TMPFILE"
# Remove hooks in case we exit mid-agent (Ctrl+C during a claude session)
[ "$AUTO_HOOKS" = true ] && remove_hooks 2>/dev/null
release_lock
@@ -178,8 +157,6 @@ finish() {
read -r -t 30 2>/dev/null || true
exit "$exit_code"
}
LOOP_AGENT_TMPFILE=""
# NOTE: Stop hook is installed/removed per-agent in run_agent(), not globally.
# This prevents the hook from killing the orchestrating CC session.
trap cleanup EXIT INT TERM
@@ -215,14 +192,10 @@ fi
# --- Agent runner ---
# Runs a prompt through the selected AI tool.
#
# Interactive (default): Pipes prompt to claude WITHOUT --print.
# This gives the full interactive CC UI — tool calls, file edits, etc.
# A Stop hook (installed at startup) sends SIGINT to the loop when claude
# finishes, which returns control to the while loop for the next iteration.
# State is tracked via files (prd.json, .verdict), not stdout.
#
# Headless (LOOP_HEADLESS=true): Uses claude --print for CI/background.
# Output captured to file for verdict parsing.
# Pipes prompt to claude WITHOUT --print. This gives the full interactive
# CC UI — tool calls, file edits, etc. A Stop hook sends SIGINT to the loop
# when claude finishes, returning control to the while loop for the next
# iteration. State is tracked via files (prd.json, .verdict), not stdout.
run_agent() {
local prompt="$1"
local role="${2:-}"
@@ -230,65 +203,31 @@ run_agent() {
rm -f "$LOOP_DIR/.verdict"
local agent_exit=0
if [ "${LOOP_HEADLESS:-false}" != "true" ]; then
# --- Interactive mode (Ralph pattern) ---
# Install Stop hook just before claude starts, remove after it exits.
# This scopes the hook to only affect the loop's claude sessions.
[ "$AUTO_HOOKS" = true ] && install_hooks
# Install Stop hook just before claude starts, remove after it exits.
# This scopes the hook to only affect the loop's claude sessions.
[ "$AUTO_HOOKS" = true ] && install_hooks
(
case "$TOOL" in
claude)
printf '%s\n' "$prompt" | claude --dangerously-skip-permissions
;;
amp)
printf '%s\n' "$prompt" | amp --dangerously-allow-all
;;
*)
log "ERROR: Unknown tool '$TOOL'"
exit 1
;;
esac
) || agent_exit=$?
(
case "$TOOL" in
claude)
printf '%s\n' "$prompt" | claude --dangerously-skip-permissions
;;
amp)
printf '%s\n' "$prompt" | amp --dangerously-allow-all
;;
*)
log "ERROR: Unknown tool '$TOOL'"
exit 1
;;
esac
) || agent_exit=$?
[ "$AUTO_HOOKS" = true ] && remove_hooks
sleep 2 # Brief pause between sessions
[ "$AUTO_HOOKS" = true ] && remove_hooks
sleep 2 # Brief pause between sessions
# Read verdict from file if evaluator wrote one
if [ "$role" = "evaluator" ] && [ -f "$LOOP_DIR/.verdict" ]; then
cat "$LOOP_DIR/.verdict"
fi
else
# --- Headless mode ---
local output_file
output_file=$(mktemp)
LOOP_AGENT_TMPFILE="$output_file"
(
case "$TOOL" in
claude)
printf '%s\n' "$prompt" | timeout "${LOOP_AGENT_TIMEOUT:-600}" \
claude --dangerously-skip-permissions --output-format text \
--print > "$output_file" 2>&1
;;
amp)
printf '%s\n' "$prompt" | timeout "${LOOP_AGENT_TIMEOUT:-600}" \
amp --dangerously-allow-all > "$output_file" 2>&1
;;
*)
log "ERROR: Unknown tool '$TOOL'"
exit 1
;;
esac
) || agent_exit=$?
if [ "$agent_exit" -ne 0 ] && [ ! -s "$output_file" ]; then
log "WARNING: Agent exited with code $agent_exit and produced no output."
fi
cat "$output_file"
rm -f "$output_file"
LOOP_AGENT_TMPFILE=""
# Read verdict from file if evaluator wrote one
if [ "$role" = "evaluator" ] && [ -f "$LOOP_DIR/.verdict" ]; then
cat "$LOOP_DIR/.verdict"
fi
}
@@ -371,18 +310,7 @@ while [ "$ITERATION" -lt "$MAX_ITERATIONS" ]; do
exit 0
fi
if [ "${LOOP_HEADLESS:-false}" != "true" ]; then
# Interactive: run directly, no capture. User sees full CC UI.
run_agent "$GENERATOR_PROMPT" "generator"
GENERATOR_OUTPUT=""
else
# Headless: capture output for parsing.
GENERATOR_OUTPUT=$(run_agent "$GENERATOR_PROMPT" "generator")
if [ -z "$GENERATOR_OUTPUT" ]; then
log "WARNING: Generator produced empty output (timeout or crash). Skipping to next iteration."
continue
fi
fi
run_agent "$GENERATOR_PROMPT" "generator"
# --- Scope budget check ---
# Verify the generator stayed within configured limits (files modified, lines written).
@@ -419,22 +347,12 @@ while [ "$ITERATION" -lt "$MAX_ITERATIONS" ]; do
EVAL_PROMPT=$(build_prompt "evaluator" "$MODE")
if [ "${LOOP_HEADLESS:-false}" != "true" ]; then
# Interactive: run directly, read verdict from file.
run_agent "$EVAL_PROMPT" "evaluator"
if [ -f "$LOOP_DIR/.verdict" ]; then
EVAL_OUTPUT=$(cat "$LOOP_DIR/.verdict")
else
log "WARNING: No verdict file found. Treating as REJECT."
EVAL_OUTPUT="<verdict>REJECT</verdict><rejection_reason>Evaluator produced no verdict file</rejection_reason>"
fi
run_agent "$EVAL_PROMPT" "evaluator"
if [ -f "$LOOP_DIR/.verdict" ]; then
EVAL_OUTPUT=$(cat "$LOOP_DIR/.verdict")
else
# Headless: capture output for parsing.
EVAL_OUTPUT=$(run_agent "$EVAL_PROMPT" "evaluator")
if [ -z "$EVAL_OUTPUT" ]; then
log "WARNING: Evaluator produced empty output. Treating as REJECT."
EVAL_OUTPUT="<verdict>REJECT</verdict><rejection_reason>Evaluator produced no output</rejection_reason>"
fi
log "WARNING: No verdict file found. Treating as REJECT."
EVAL_OUTPUT="<verdict>REJECT</verdict><rejection_reason>Evaluator produced no verdict file</rejection_reason>"
fi
VERDICT=$(parse_verdict "$EVAL_OUTPUT")