#!/usr/bin/env sh # Headless loop script for overnight Claude Code automation. # Rate-limit aware: detects limits, sleeps until reset, and retries automatically. set -u 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:-10}" ITERATION_DELAY="${ITERATION_DELAY:-30}" CLAUDE_BIN="${CLAUDE_BIN:-claude}" RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}" MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}" CLAUDE_EXIT=0 cd "$PROJECT_DIR" log() { echo "$1" | tee -a "$LOG_FILE" } banner() { log "" log "================================================================" log " $1" log " $(date '+%Y-%m-%d %H:%M:%S')" log "================================================================" log "" } section() { log "" log "----------------------------------------" log " $1" log "----------------------------------------" log "" } plan_has_tasks() { grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null } remaining_tasks() { grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0" } next_task() { grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)" } check_rate_limit() { [ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1 tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit detected" | grep -v "^Sleeping" | grep -v "^=" | grep -v "^-" | grep -qi \ -e "rate.limit" \ -e "too.many.requests" \ -e "429" \ -e "quota.exceeded" \ -e "usage.limit" \ -e "limit.reached" 2>/dev/null } banner "NEODE-UI OVERNIGHT AUTOMATION STARTED" log " Project: $PROJECT_DIR" log " Prompt: $PROMPT_FILE" log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}" log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s between each)" log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry up to ${MAX_RATE_LIMIT_RETRIES}x" log " Tasks left: $(remaining_tasks)" log " Next task: $(next_task)" log "" i=1 rate_limit_retries=0 while [ "$i" -le "$ITERATION_COUNT" ]; do if ! plan_has_tasks; then banner "ALL TASKS COMPLETE" log " No remaining tasks in plan.md. Stopping." break fi section "ITERATION $i/$ITERATION_COUNT" log " Tasks remaining: $(remaining_tasks)" log " Next task: $(next_task)" log "" export CLAUDE_PROJECT_DIR="$PROJECT_DIR" export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}" if [ -f "$PROMPT_FILE" ]; then log " Starting Claude session..." log "" "$CLAUDE_BIN" -p --dangerously-skip-permissions \ < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE" CLAUDE_EXIT=$? log "" log " Claude exited with code: $CLAUDE_EXIT" else log " ERROR: $PROMPT_FILE not found" exit 1 fi if check_rate_limit; then rate_limit_retries=$((rate_limit_retries + 1)) if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then section "RATE LIMITED — SCHEDULING LAUNCHD RETRY" log " Hit rate limit $rate_limit_retries times. Creating launchd job to retry later." PLIST_LABEL="com.neode-ui.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}" log " Plist: $PLIST_PATH (auto-removes after running)" exit 0 fi section "RATE LIMITED — WAITING" log " 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" if ! plan_has_tasks; then banner "ALL TASKS COMPLETE (during rate limit wait)" break fi log " Retrying..." continue fi rate_limit_retries=0 section "ITERATION $i COMPLETE" log " Tasks remaining: $(remaining_tasks)" log " Next task: $(next_task)" i=$((i + 1)) if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then log " Pausing ${ITERATION_DELAY}s before next iteration..." sleep "$ITERATION_DELAY" fi done banner "LOOP FINISHED" log " Completed $((i - 1)) iterations" log " Tasks remaining: $(remaining_tasks)" log ""