Update loop scripts with rate limit handling, set plan for tonight's run, and update prompt.md with task instructions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
4.8 KiB
Bash
Executable File
143 lines
4.8 KiB
Bash
Executable File
#!/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" <<PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>${PLIST_LABEL}</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/bin/sh</string>
|
|
<string>-c</string>
|
|
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
|
|
</array>
|
|
<key>StartCalendarInterval</key>
|
|
<dict>
|
|
<key>Hour</key>
|
|
<integer>${RETRY_HOUR}</integer>
|
|
<key>Minute</key>
|
|
<integer>${RETRY_MIN}</integer>
|
|
</dict>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>CLAUDE_AUTONOMOUS</key>
|
|
<string>1</string>
|
|
<key>CLAUDE_PROJECT_DIR</key>
|
|
<string>${PROJECT_DIR}</string>
|
|
<key>PATH</key>
|
|
<string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
|
|
</dict>
|
|
<key>StandardOutPath</key>
|
|
<string>${LOG_FILE}</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>${LOG_FILE}</string>
|
|
</dict>
|
|
</plist>
|
|
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) ==="
|