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:
14
README.md
14
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
146
loop.sh
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user