loop: baseline

This commit is contained in:
bepis
2026-10-20 20:33:35 +11:00
parent f1ae7b3119
commit 9a308b697d
32 changed files with 959 additions and 2046 deletions
+141
View File
@@ -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