← back to Codex Yolo
scripts/yolo-overnight.sh
141 lines
#!/bin/bash
# YOLO overnight orchestrator
# - Iterates over targets, runs claude-codex 8-way report-only debate per project,
# - Applies ONLY trivial fixes via apply-trivial-patches.cjs (typo / null-guard /
# missing-await / regex-escape — never logic changes)
# - Smokes after each patch; auto-revert + freeze project on 3 consecutive fails
# - Writes proposed (non-trivial) patches to <proj>/_yolo_patches/iter-N.diff
# - Emails Steve a summary at end
# Hard kill: bash deadline OR `pm2 stop yolo-overnight`
set -uo pipefail
ROOT="$HOME/Projects/codex-yolo"
RUN_TS=$(date +%Y%m%d-%H%M%S)
RUN_DIR="$ROOT/runs/$RUN_TS"
mkdir -p "$RUN_DIR"
LOG="$RUN_DIR/orchestrator.log"
exec > >(tee -a "$LOG") 2>&1
TARGETS=(debate-ring-viewer small-business-builder animals)
ROUNDS_PER_DEBATE=2
MAX_ITER=4
DEADLINE_HOURS=8
DEADLINE=$(( $(date +%s) + DEADLINE_HOURS*3600 ))
echo "[yolo] start $(date -Iseconds) deadline=$(date -r $DEADLINE -Iseconds)"
echo "[yolo] targets=${TARGETS[*]} max_iter=$MAX_ITER rounds=$ROUNDS_PER_DEBATE"
# Per-project freeze state stored as files (bash 3 on macOS has no
# associative arrays; using $RUN_DIR/$proj.frozen as the flag).
is_frozen() { [ -f "$RUN_DIR/$1.frozen" ]; }
freeze() { touch "$RUN_DIR/$1.frozen"; }
iter=0
while [ $iter -lt $MAX_ITER ] && [ $(date +%s) -lt $DEADLINE ]; do
iter=$((iter+1))
echo "============================================================"
echo "[yolo] iteration $iter/$MAX_ITER $(date -Iseconds)"
echo "============================================================"
for t in "${TARGETS[@]}"; do
is_frozen "$t" && { echo "[yolo] $t frozen, skipping"; continue; }
[ $(date +%s) -ge $DEADLINE ] && { echo "[yolo] deadline hit, stopping"; break 2; }
PROJ="$HOME/Projects/$t"
[ -d "$PROJ/.git" ] || { echo "[yolo] $t not a git repo, skipping"; continue; }
NAME="yolo-$t-iter$iter-$RUN_TS"
echo "[yolo] === $t iter=$iter (run=$NAME) ==="
# 1) Run debate (report-only, 2 rounds, ~10-15 min)
bash "$HOME/.claude/skills/claude-codex/scripts/start.sh" \
--name "$NAME" --workdir "$PROJ" \
--rounds "$ROUNDS_PER_DEBATE" --report-only \
>> "$LOG" 2>&1
PID_FILE="$HOME/.claude/skills/claude-codex/runs/$NAME/loop.pid"
[ -f "$PID_FILE" ] && DEBATE_PID=$(cat "$PID_FILE") || DEBATE_PID=
# Wait for debate to finish (poll the pid)
while [ -n "$DEBATE_PID" ] && kill -0 "$DEBATE_PID" 2>/dev/null; do
[ $(date +%s) -ge $DEADLINE ] && { kill "$DEBATE_PID" 2>/dev/null; break; }
sleep 30
done
DEBATE_DIR="$HOME/.claude/skills/claude-codex/runs/$NAME"
if [ ! -f "$DEBATE_DIR/final_report.md" ] && [ ! -d "$DEBATE_DIR/round_1" ]; then
echo "[yolo] $t debate produced no output, freezing"
freeze "$t"
continue
fi
# 2) Apply trivial patches + write _yolo_patches/iter-N.diff for the rest
/opt/homebrew/bin/node "$ROOT/scripts/apply-trivial-patches.cjs" \
--debate "$DEBATE_DIR" --project "$PROJ" --iter "$iter" \
--max-applied 5 \
>> "$LOG" 2>&1 || echo "[yolo] apply failed for $t (non-fatal)"
# 3) Smoke test
if ! bash "$ROOT/scripts/smoke-test.sh" "$t" >> "$LOG" 2>&1; then
echo "[yolo] $t SMOKE FAILED → reverting"
# Trivial patches were applied to tracked files in the working tree but
# not yet committed (commit happens only on a passing smoke below).
# Discard the uncommitted patches only — never reset past HEAD, which
# would drop unrelated commits. Untracked _yolo_patches/ diffs are kept.
cd "$PROJ" && git reset -q --hard HEAD
FAIL_COUNT_FILE="$RUN_DIR/$t.fail-count"
F=$(cat "$FAIL_COUNT_FILE" 2>/dev/null || echo 0)
F=$((F+1))
echo "$F" > "$FAIL_COUNT_FILE"
[ "$F" -ge 3 ] && { echo "[yolo] $t hit 3 fails, freezing"; freeze "$t"; }
else
echo "[yolo] $t smoke ok"
echo 0 > "$RUN_DIR/$t.fail-count"
# Commit (local only — never push)
cd "$PROJ" && git add -A && \
(git diff --cached --quiet || \
git -c user.email=yolo@local -c user.name=yolo commit -q \
-m "yolo: iter $iter trivial patches from claude-codex debate")
fi
# 4) Status snapshot for the morning email
cp -f "$DEBATE_DIR/final_report.md" "$RUN_DIR/$t-iter$iter-final.md" 2>/dev/null || true
done
echo "[yolo] iter $iter complete; sleeping 60s before next pass"
sleep 60
done
# 5) Build + email summary
echo "[yolo] building summary $(date -Iseconds)"
SUMMARY="$RUN_DIR/SUMMARY.md"
{
echo "# YOLO overnight summary — $(date -Iseconds)"
echo ""
echo "**Run:** \`$RUN_DIR\`"
echo "**Iterations:** $iter / $MAX_ITER"
echo ""
for t in "${TARGETS[@]}"; do
echo "## $t"
is_frozen "$t" && echo " - **FROZEN** (smoke fails)"
cd "$HOME/Projects/$t" 2>/dev/null && \
echo " - commits this run: $(git log --oneline --since="$RUN_TS" 2>/dev/null | wc -l | tr -d ' ')" && \
echo " - latest:" && git log --oneline -3 2>/dev/null | sed 's/^/ /'
PATCH_DIR="$HOME/Projects/$t/_yolo_patches"
[ -d "$PATCH_DIR" ] && {
echo " - proposed (non-trivial) diffs:"
ls "$PATCH_DIR"/*.diff 2>/dev/null | sed 's/^/ - /'
}
echo ""
done
} > "$SUMMARY"
# Email via George
curl -sS --max-time 12 -X POST http://localhost:9850/api/send \
-u admin:DWSecure2024! \
-H 'content-type: application/json' \
-d "$(/opt/homebrew/bin/node -e "
const fs=require('fs');
const body=fs.readFileSync('$SUMMARY','utf8');
process.stdout.write(JSON.stringify({to:'steveabramsdesigns@gmail.com',subject:'YOLO overnight summary '+new Date().toISOString().slice(0,10),body:body}));
")" >> "$LOG" 2>&1 || echo "[yolo] email failed"
echo "[yolo] done $(date -Iseconds)"