#!/usr/bin/env sh # Headless loop script for overnight Claude Code automation. # Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete). # Rate-limit aware: detects limits, sleeps until reset, and retries automatically. set -eu PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}" LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}" ITERATION_COUNT="${ITERATION_COUNT:-1}" ITERATION_DELAY="${ITERATION_DELAY:-600}" CLAUDE_BIN="${CLAUDE_BIN:-claude}" RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}" # Default: wait 1 hour on rate limit MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}" # Max retries before giving up cd "$PROJECT_DIR" log() { echo "$1" | tee -a "$LOG_FILE" } # Check if plan has remaining tasks plan_has_tasks() { grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null } # Detect rate limit from Claude output check_rate_limit() { # Check last 50 lines of log for rate limit indicators tail -50 "$LOG_FILE" 2>/dev/null | grep -qi \ -e "rate.limit" \ -e "too.many.requests" \ -e "429" \ -e "quota.exceeded" \ -e "usage.limit" \ -e "limit.reached" 2>/dev/null } log "=== Overnight loop started $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ===" log " PROMPT_FILE=$PROMPT_FILE" log " CLAUDE_AUTONOMOUS=${CLAUDE_AUTONOMOUS:-0}" log " ITERATION_COUNT=$ITERATION_COUNT" log " RATE_LIMIT_WAIT=${RATE_LIMIT_WAIT}s" log "" i=1 rate_limit_retries=0 while [ "$i" -le "$ITERATION_COUNT" ]; do log "--- Iteration $i/$ITERATION_COUNT @ $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ---" export CLAUDE_PROJECT_DIR="$PROJECT_DIR" export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}" # Run Claude with autonomous permissions; prompt from file if [ -f "$PROMPT_FILE" ]; then "$CLAUDE_BIN" -p --dangerously-skip-permissions --output-format=stream-json \ < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE" || true else log "Error: $PROMPT_FILE not found" exit 1 fi # Check for rate limit after Claude exits if check_rate_limit; then rate_limit_retries=$((rate_limit_retries + 1)) if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then log "Rate limited $rate_limit_retries times — giving up." log "Scheduling retry via launchd..." # Schedule a retry using launchd for after rate limit resets PLIST_LABEL="com.aiui.overnight-retry" PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist" RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M') RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1) RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2) cat > "$PLIST_PATH" < Label ${PLIST_LABEL} ProgramArguments /bin/sh -c cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH} StartCalendarInterval Hour ${RETRY_HOUR} Minute ${RETRY_MIN} EnvironmentVariables CLAUDE_AUTONOMOUS 1 CLAUDE_PROJECT_DIR ${PROJECT_DIR} PATH /usr/local/bin:/usr/bin:/bin:$HOME/.local/bin StandardOutPath ${LOG_FILE} StandardErrorPath ${LOG_FILE} PLIST launchctl load "$PLIST_PATH" 2>/dev/null || true log "Scheduled retry at ~${RETRY_TIME} via launchd ($PLIST_PATH)" log "The plist auto-removes after running." exit 0 fi log "Rate limit detected (attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES)." log "Sleeping ${RATE_LIMIT_WAIT}s until $(date -v+${RATE_LIMIT_WAIT}S '+%H:%M:%S' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M:%S')..." sleep "$RATE_LIMIT_WAIT" # Check if plan still has tasks before retrying if ! plan_has_tasks; then log "All plan tasks completed during rate limit wait. Done." break fi log "Retrying after rate limit..." continue # Retry same iteration fi # Reset rate limit counter on successful run rate_limit_retries=0 i=$((i + 1)) if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then log "Waiting ${ITERATION_DELAY}s before next iteration..." sleep "$ITERATION_DELAY" fi done log "=== Loop complete $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ==="