Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWoivrRUEmJLFFwbsrGkqQ
163 lines
7.1 KiB
Bash
Executable File
163 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# wave-chain.sh <wave>:<task> [<wave>:<task> ...]
|
|
# e.g. wave-chain.sh 9:K-manual 10:L-search
|
|
#
|
|
# Runs waves IN SEQUENCE with no orchestrator attached (2026-09-13: the user asked for
|
|
# wave 10 to start straight after wave 9 without waiting on them). Between waves it
|
|
# runs a mechanical acceptance gate, because starting a worker on a red tree wastes a
|
|
# whole session. This gate is NOT the orchestrator's review — it is the floor below it.
|
|
#
|
|
# Per wave:
|
|
# 1. run-task.sh <task> in the FOREGROUND (quota-aware, resumable)
|
|
# 2. gate: assembleDebug, testDebugUnitTest, verifyPaparazziDebug, test count UP from
|
|
# the XML, 0 failures, no build files touched, no "always 'false'" warning
|
|
# 3. gate red -> ONE fresh fix worker (<task>-fix) pointed at the gate log, re-gate
|
|
# 4. green -> commit + push; red -> write sentinel as FAILED and STOP the chain
|
|
#
|
|
# Holds the sprite lease itself (HAZARD #5). wave-guard.sh cannot be used: its
|
|
# workers_running() sees nothing during the 5-10 min gate between workers and would
|
|
# release the lease mid-chain. Liveness here is `kill -0` on this script's own PID,
|
|
# never pgrep -f (HAZARD #6/#10).
|
|
set -u
|
|
cd "$HOME/bookshelf" || exit 1
|
|
L="$HOME/bookshelf/logs"; mkdir -p "$L"
|
|
CLOG="$L/wave-chain.log"; LEASE="bookshelf-wave"
|
|
RESULTS="app/app/build/test-results/testDebugUnitTest"
|
|
|
|
log() { echo "[$(date -Is)] $*" >> "$CLOG"; }
|
|
|
|
lease_present() { sprite-env curl /v1/tasks 2>/dev/null | grep -q "\"$LEASE\""; }
|
|
lease_acquire() {
|
|
local a
|
|
for a in 1 2 3; do
|
|
sprite-env curl -X DELETE "/v1/tasks/$LEASE" >/dev/null 2>&1
|
|
sprite-env curl -X POST /v1/tasks -H 'Content-Type: application/json' \
|
|
-d "{\"name\":\"$LEASE\",\"expire\":\"3600s\"}" >/dev/null 2>&1
|
|
lease_present && return 0
|
|
sleep 5
|
|
done
|
|
return 1
|
|
}
|
|
lease_release() { sprite-env curl -X DELETE "/v1/tasks/$LEASE" >/dev/null 2>&1; }
|
|
|
|
CHAIN_PID=$$
|
|
(
|
|
last=$(date +%s)
|
|
lease_acquire && log "lease acquired and VERIFIED" || log "FATAL: lease acquire failed"
|
|
while kill -0 "$CHAIN_PID" 2>/dev/null; do
|
|
sleep 30
|
|
now=$(date +%s)
|
|
if [ $((now - last)) -ge 900 ] || ! lease_present; then
|
|
lease_acquire && log "lease renewed" || log "ERROR: lease renewal FAILED"
|
|
last=$now
|
|
fi
|
|
done
|
|
) &
|
|
RENEWER=$!
|
|
trap 'kill $RENEWER 2>/dev/null; lease_release; log "chain exiting, lease released"' EXIT
|
|
|
|
count_tests() {
|
|
python3 - "$RESULTS" <<'PY'
|
|
import glob, sys, xml.etree.ElementTree as ET
|
|
t = s = f = 0
|
|
for p in glob.glob(sys.argv[1] + "/TEST-*.xml"):
|
|
r = ET.parse(p).getroot()
|
|
t += int(r.get("tests")); s += int(r.get("skipped")); f += int(r.get("failures")) + int(r.get("errors"))
|
|
print(t, s, f)
|
|
PY
|
|
}
|
|
|
|
# gate <label> <baseline-test-count> -> 0 green, 1 red. Output in logs/gate-<label>.log
|
|
gate() {
|
|
local label="$1" base="$2" g="$L/gate-$1.log" ok=0
|
|
: > "$g"
|
|
echo "=== gate $label $(date -Is) baseline=$base ===" >> "$g"
|
|
for t in assembleDebug testDebugUnitTest verifyPaparazziDebug; do
|
|
echo "--- ./tasks/gw $t ---" >> "$g"
|
|
./tasks/gw "$t" >> "$g" 2>&1; rc=$?
|
|
echo "--- $t exit $rc ---" >> "$g"
|
|
[ $rc -ne 0 ] && ok=1
|
|
done
|
|
read -r n s f <<<"$(count_tests)"
|
|
echo "tests=$n skipped=$s failures+errors=$f (baseline $base)" >> "$g"
|
|
{ [ "$f" -ne 0 ] || [ "$n" -le "$base" ]; } && { echo "GATE: test count did not go up or failures present" >> "$g"; ok=1; }
|
|
if grep -n "always 'false'" "$g" >> "$g.warn"; then
|
|
echo "GATE: \"always 'false'\" warning present:" >> "$g"; cat "$g.warn" >> "$g"; ok=1
|
|
fi
|
|
rm -f "$g.warn"
|
|
echo "--- git status --porcelain ---" >> "$g"; git status --porcelain >> "$g"
|
|
if git status --porcelain | grep -qE '\.gradle\.kts$|libs\.versions\.toml$|gradle\.properties$|local\.properties$'; then
|
|
echo "GATE: a build file was touched (forbidden)" >> "$g"; ok=1
|
|
fi
|
|
echo "=== gate $label result: $([ $ok -eq 0 ] && echo GREEN || echo RED) ===" >> "$g"
|
|
return $ok
|
|
}
|
|
|
|
log "chain start: $*"
|
|
read -r BASE _ _ <<<"$(count_tests)"
|
|
log "baseline tests from XML: $BASE"
|
|
|
|
for spec in "$@"; do
|
|
W="${spec%%:*}"; T="${spec#*:}"; SENT="$L/WAVE$W-DONE"
|
|
log "wave $W ($T): launching worker"
|
|
./tasks/run-task.sh "$T" "./tasks/$T.txt"
|
|
log "wave $W ($T): worker exited: $(tail -1 "$L/$T.state")"
|
|
|
|
if gate "$T" "$BASE"; then
|
|
GREEN=1
|
|
else
|
|
log "wave $W ($T): gate RED; launching one fix worker"
|
|
cat > "tasks/$T-fix.txt" <<EOF
|
|
A previous worker implemented the task in ~/bookshelf/tasks/$T.txt and left the
|
|
working tree FAILING its acceptance gate. Read, in order: ~/bookshelf/docs/SPEC.md,
|
|
~/bookshelf/tasks/$T.txt (the original instructions — every constraint in it binds
|
|
you too), ~/bookshelf/logs/$T.summary (that worker's report), then the gate output at
|
|
~/bookshelf/logs/gate-$T.log. Lines starting "GATE:" and non-zero exits are the failures.
|
|
|
|
Fix ONLY what makes the gate red. Do not redesign or re-implement the feature. The
|
|
test count must end ABOVE $BASE with zero failures; do not delete or @Ignore tests to
|
|
get there. If a Paparazzi verify failed because snapshots were never recorded, record
|
|
them and LOOK at the PNGs before accepting them.
|
|
|
|
Run in the FOREGROUND (never background a build): ./tasks/gw assembleDebug,
|
|
./tasks/gw testDebugUnitTest, ./tasks/gw verifyPaparazziDebug. Do not git commit/add/push.
|
|
Report what was wrong, what you changed, and the verbatim output tails.
|
|
EOF
|
|
./tasks/run-task.sh "$T-fix" "./tasks/$T-fix.txt"
|
|
log "wave $W ($T-fix): worker exited: $(tail -1 "$L/$T-fix.state")"
|
|
if gate "$T-refix" "$BASE"; then GREEN=1; else GREEN=0; fi
|
|
fi
|
|
|
|
if [ "$GREEN" -ne 1 ]; then
|
|
log "wave $W ($T): gate still RED after fix worker. STOPPING CHAIN."
|
|
{ echo "=== WAVE $W ($T) FAILED GATE $(date -Is) — chain stopped, later waves NOT run ==="
|
|
echo "Work left UNCOMMITTED in the tree. See logs/gate-$T*.log, logs/$T*.summary."; } > "$SENT"
|
|
exit 1
|
|
fi
|
|
|
|
read -r BASE _ _ <<<"$(count_tests)"
|
|
COST="$(jq -r '"$" + ((.total_cost_usd//0)|tostring) + ", " + ((.num_turns//0)|tostring) + " turns"' "$L/$T.json" 2>/dev/null)"
|
|
git add -A
|
|
git commit -q -F - <<EOF
|
|
Wave $W ($T): $( [ "$T" = K-manual ] && echo "add a book by hand, no ISBN required" || echo "search own library + Open Library + Google Books, deduplicated" )
|
|
|
|
Committed by tasks/wave-chain.sh after its mechanical gate passed
|
|
(assembleDebug, testDebugUnitTest = $BASE tests, verifyPaparazziDebug, no build
|
|
files touched, no "always 'false'"). ORCHESTRATOR REVIEW STILL PENDING.
|
|
Prompt: tasks/$T.txt. Worker: $COST.
|
|
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Claude-Session: https://claude.ai/code/session_01RWoivrRUEmJLFFwbsrGkqQ
|
|
EOF
|
|
SHA="$(git rev-parse --short HEAD)"
|
|
if timeout 120 git push -q origin HEAD 2>>"$CLOG"; then PUSHED="pushed"; else PUSHED="PUSH FAILED (see wave-chain.log)"; fi
|
|
log "wave $W ($T): GREEN, committed $SHA, $PUSHED, tests now $BASE"
|
|
{ echo "=== WAVE $W ($T) passed the chain gate $(date -Is) ==="
|
|
echo "commit $SHA ($PUSHED); tests $BASE; worker $COST"
|
|
echo "Mechanical gate only. Orchestrator must still review: read logs/$T.summary,"
|
|
echo "eyeball new Paparazzi PNGs, and read the diff (git show --stat $SHA)."; } > "$SENT"
|
|
done
|
|
|
|
log "chain complete"
|
|
echo "chain complete $(date -Is)" > "$L/CHAIN-DONE"
|