loop: baseline
This commit is contained in:
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
# run-agent-loop.sh — drives Qwen Code one STATUS.md step at a time.
|
||||
# The script (not the model) decides: what's next, whether tests pass, commit vs rollback, when to stop.
|
||||
#
|
||||
# Usage: ./scripts/run-agent-loop.sh
|
||||
# Options (env vars, all optional):
|
||||
# AGENT_CMD agent command; prompt is appended as last arg (default: "qwen --yolo -p")
|
||||
# MAX_STEPS max agent runs in total (default: 60)
|
||||
# MAX_RETRIES attempts per step before giving up (default: 3)
|
||||
# STEP_TIMEOUT per-run time limit, e.g. 30m, 1h (default: 45m)
|
||||
# PAUSE_AT_PHASE 1 = wait for Enter between phases, 0 = don't (default: 1)
|
||||
# STOP_AFTER_PHASE stop once this phase number is finished (default: run to the end)
|
||||
# PROMPT_FILE prompt template (default: PROMPT_ONE_STEP.md)
|
||||
#
|
||||
# WARNING: --yolo lets the agent run ANY shell command without asking.
|
||||
# Run inside this project folder only, ideally in a VM/container, with git initialised.
|
||||
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
AGENT_CMD="${AGENT_CMD:-qwen --yolo -p}"
|
||||
MAX_STEPS="${MAX_STEPS:-60}"
|
||||
MAX_RETRIES="${MAX_RETRIES:-3}"
|
||||
STEP_TIMEOUT="${STEP_TIMEOUT:-45m}"
|
||||
PAUSE_AT_PHASE="${PAUSE_AT_PHASE:-1}"
|
||||
STOP_AFTER_PHASE="${STOP_AFTER_PHASE:-}"
|
||||
PROMPT_FILE="${PROMPT_FILE:-PROMPT_ONE_STEP.md}"
|
||||
|
||||
LOOP_DIR=".loop"; LOG_DIR="$LOOP_DIR/logs"
|
||||
FEEDBACK="$LOOP_DIR/feedback.md"; PASS_FILE="$LOOP_DIR/pass_count"
|
||||
mkdir -p "$LOG_DIR"
|
||||
read -ra AGENT <<< "$AGENT_CMD"
|
||||
|
||||
# --- helpers ---------------------------------------------------------------
|
||||
if command -v timeout >/dev/null 2>&1; then TO=timeout
|
||||
elif command -v gtimeout >/dev/null 2>&1; then TO=gtimeout
|
||||
else TO=""; echo "note: no 'timeout' command found; runs will not be time-limited"; fi
|
||||
with_timeout() { local t="$1"; shift; if [ -n "$TO" ]; then "$TO" "$t" "$@"; else "$@"; fi; }
|
||||
|
||||
# First unchecked numbered step (e.g. 1.5). Stretch goals (S1...) are ignored on purpose.
|
||||
next_step() { sed -n 's/^- \[ \] \*\*\([0-9][0-9]*\.[0-9][0-9]*\)\*\*.*/\1/p' STATUS.md | head -1; }
|
||||
total_steps() { grep -cE '^- \[[ xX]\] \*\*[0-9]+\.[0-9]+\*\*' STATUS.md; }
|
||||
|
||||
TEST_OUT=""; TEST_PASS=""
|
||||
run_tests() { # returns 0 = ok/skipped, 1 = failed
|
||||
TEST_OUT=""; TEST_PASS=""
|
||||
if [ -f package.json ] && grep -q '"test"' package.json \
|
||||
&& find tests -name '*.js' 2>/dev/null | grep -q .; then
|
||||
TEST_OUT="$(with_timeout 5m npm test 2>&1)" || { return 1; }
|
||||
TEST_PASS="$(printf '%s\n' "$TEST_OUT" | grep -E '^(ℹ|#) pass ' | tail -1 | awk '{print $3}')"
|
||||
# guard: the number of passing tests must never drop (stops "fix" by deleting tests)
|
||||
if [ -n "$TEST_PASS" ] && [ -f "$PASS_FILE" ]; then
|
||||
local prev; prev="$(cat "$PASS_FILE")"
|
||||
if [ "$TEST_PASS" -lt "$prev" ] 2>/dev/null; then
|
||||
TEST_OUT="Passing test count dropped from $prev to $TEST_PASS. Do not delete or weaken tests. Restore them.
|
||||
$TEST_OUT"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -x scripts/smoke.sh ]; then # optional extra check you can add later
|
||||
TEST_OUT="$TEST_OUT
|
||||
$(with_timeout 2m ./scripts/smoke.sh 2>&1)" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
rollback() { git reset -q --hard HEAD && git clean -qfd; }
|
||||
commit() { git add -A && git commit -qm "$1" 2>/dev/null || true; }
|
||||
|
||||
# --- preflight -------------------------------------------------------------
|
||||
[ -f STATUS.md ] && [ -f "$PROMPT_FILE" ] || { echo "Run from the project; STATUS.md and $PROMPT_FILE needed."; exit 2; }
|
||||
command -v "${AGENT[0]}" >/dev/null 2>&1 || { echo "Agent command '${AGENT[0]}' not found."; exit 2; }
|
||||
[ "$(total_steps)" -ge 1 ] || { echo "No numbered steps found in STATUS.md (format: '- [ ] **1.1** ...')."; exit 2; }
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || git init -q
|
||||
# make sure commits can't silently fail (no identity configured is a common cause)
|
||||
git config user.email >/dev/null 2>&1 || git config user.email "loop@localhost"
|
||||
git config user.name >/dev/null 2>&1 || git config user.name "agent-loop"
|
||||
git add -A && git commit -qm "loop: baseline" 2>/dev/null
|
||||
git rev-parse --verify HEAD >/dev/null 2>&1 || { echo "Could not create a git baseline commit; aborting (rollback needs one)."; exit 2; }
|
||||
trap 'echo; echo "Interrupted. Working tree left as-is (uncommitted changes may exist)."; exit 130' INT
|
||||
|
||||
# --- main loop -------------------------------------------------------------
|
||||
last_step=""; attempt=0; prev_phase=""
|
||||
echo "Loop starting. Logs in $LOG_DIR/. Ctrl+C to stop."
|
||||
|
||||
for ((n = 1; n <= MAX_STEPS; n++)); do
|
||||
step="$(next_step)"
|
||||
if [ -z "$step" ]; then echo "✅ All numbered steps are ticked. Done."; exit 0; fi
|
||||
phase="${step%%.*}"
|
||||
|
||||
if [ -n "$STOP_AFTER_PHASE" ] && [ "$phase" -gt "$STOP_AFTER_PHASE" ]; then
|
||||
echo "✅ Reached STOP_AFTER_PHASE=$STOP_AFTER_PHASE. Next up would be step $step."; exit 0
|
||||
fi
|
||||
if [ -n "$prev_phase" ] && [ "$phase" != "$prev_phase" ] && [ "$PAUSE_AT_PHASE" = "1" ]; then
|
||||
echo; echo "── Phase $prev_phase complete (HEAD: $(git log -1 --format=%s)). Playtest / review now."
|
||||
read -r -p "Press Enter to start phase $phase (Ctrl+C to stop)… " _ || true
|
||||
fi
|
||||
prev_phase="$phase"
|
||||
|
||||
if [ "$step" = "$last_step" ]; then attempt=$((attempt + 1)); else attempt=1; fi
|
||||
last_step="$step"
|
||||
if [ "$attempt" -gt "$MAX_RETRIES" ]; then
|
||||
echo "🛑 Stuck on step $step after $MAX_RETRIES attempts. See $LOG_DIR/step-$step-try-*.log and $FEEDBACK"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
prompt="$(cat "$PROMPT_FILE")
|
||||
|
||||
## This run
|
||||
Target step: **$step** (attempt $attempt of $MAX_RETRIES)"
|
||||
[ -s "$FEEDBACK" ] && prompt="$prompt
|
||||
|
||||
## Feedback from the previous attempt (fix this first)
|
||||
$(cat "$FEEDBACK")"
|
||||
|
||||
log="$LOG_DIR/step-$step-try-$attempt.log"
|
||||
echo; echo "▶ [$n/$MAX_STEPS] step $step, attempt $attempt → $log"
|
||||
with_timeout "$STEP_TIMEOUT" "${AGENT[@]}" "$prompt" < /dev/null 2>&1 | tee "$log"
|
||||
rc=${PIPESTATUS[0]}
|
||||
note=""; [ "$rc" = "124" ] && note="The previous attempt TIMED OUT after $STEP_TIMEOUT. Do less per run; avoid commands that never exit (e.g. 'npm start' in the foreground)."
|
||||
|
||||
after="$(next_step)"
|
||||
if run_tests; then
|
||||
[ -n "$TEST_PASS" ] && echo "$TEST_PASS" > "$PASS_FILE"
|
||||
if [ "$after" != "$step" ]; then
|
||||
commit "step $step: done"; : > "$FEEDBACK"
|
||||
echo "✔ step $step verified and committed (tests: ${TEST_PASS:-n/a})"
|
||||
else
|
||||
commit "step $step: wip (not ticked)"
|
||||
printf '%s\nCode exists and tests pass, but step %s is NOT ticked in STATUS.md. Check the Acceptance line; if truly met, tick it (and update Session Log + HANDOFF.md). If not, finish the missing work.\n' "$note" "$step" > "$FEEDBACK"
|
||||
echo "… step $step not ticked; keeping work, retrying"
|
||||
fi
|
||||
else
|
||||
rollback
|
||||
{ [ -n "$note" ] && echo "$note"; echo "Your last attempt was rolled back because verification FAILED. Output (tail):"; echo '```'; printf '%s\n' "$TEST_OUT" | tail -40; echo '```'; } > "$FEEDBACK"
|
||||
echo "✘ step $step failed verification; rolled back"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Hit MAX_STEPS=$MAX_STEPS. Re-run to continue."; exit 0
|
||||
Reference in New Issue
Block a user