← 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)"