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

View File

@@ -72,20 +72,6 @@ Or ask Claude Code "status" — it reads `.loop/prd.json` and `.loop/progress.md
Each generator and evaluator run is a full Claude Code session saved to history. Use `claude -r` to resume any session and inspect what happened, debug a rejection, or continue from where it left off. Each generator and evaluator run is a full Claude Code session saved to history. Use `claude -r` to resume any session and inspect what happened, debug a rejection, or continue from where it left off.
## Headless Mode
For CI or background execution without the interactive UI:
```bash
.loop/loop.sh --headless [options]
--headless Run without interactive UI
--mode <implement|explore|fix> Operating mode
--max <N> Maximum iterations (default: 20)
--skip-eval Skip evaluator pass
--dry-run Print assembled prompts without running
```
## Architecture ## Architecture
### Generator ### Generator

View File

@@ -102,5 +102,5 @@ echo " Next steps (inside Claude Code, in any project):"
echo "" echo ""
echo " /agent-loop:run # Single command — setup, plan, and run" echo " /agent-loop:run # Single command — setup, plan, and run"
echo "" echo ""
echo " Or run headless: .loop/loop.sh" echo " Or run directly: .loop/loop.sh"
echo "" echo ""

146
loop.sh
View File

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