refactor: make setup.sh self-contained, remove templates folder

All templates are now embedded inline in setup.sh. Users only need
this single script — run `bash setup.sh` from any project root and
everything is created automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 08:36:42 +00:00
co-authored by Claude Opus 4.6
parent b96d96b654
commit eda9e48965
7 changed files with 306 additions and 503 deletions
+306 -131
View File
@@ -1,14 +1,19 @@
#!/usr/bin/env bash
# ============================================================================
# Claude Code Overnight Automation — Setup Script
# Claude Code Overnight Automation — One-File Setup
# ============================================================================
# Creates the loop directory, hook scripts, and configures settings.json.
# Run this from your project root:
# bash path/to/setup.sh
# Run from your project root:
# bash setup.sh
#
# This single script creates everything:
# loop/loop.sh — main orchestrator
# loop/prompt.md — template prompt for Claude
# loop/plan.md — your task checklist
# ~/.claude/hooks/ — sleep prevention + autonomous stop hook
# ~/.claude/settings.json — hook registrations
# ============================================================================
set -euo pipefail
# ── Colors ───────────────────────────────────────────────────────────────────
BOLD='\033[1m'
DIM='\033[2m'
GREEN='\033[0;32m'
@@ -22,94 +27,229 @@ warn() { echo -e " ${YELLOW}!${NC} $1"; }
err() { echo -e " ${RED}x${NC} $1"; }
info() { echo -e " ${DIM}$1${NC}"; }
# ── Header ───────────────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD} Claude Code Overnight Automation${NC}"
echo -e " ${DIM}────────────────────────────────────${NC}"
echo ""
# ── Find template directory ──────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEMPLATE_DIR="$SCRIPT_DIR/templates"
if [[ ! -d "$TEMPLATE_DIR" ]]; then
err "Templates directory not found at: $TEMPLATE_DIR"
err "Make sure setup.sh is in the same folder as the templates/ directory."
exit 1
fi
# ── Check prerequisites ─────────────────────────────────────────────────────
# ── Prerequisites ────────────────────────────────────────────────────────────
echo -e " ${BOLD}Checking prerequisites...${NC}"
echo ""
MISSING=0
if command -v claude &>/dev/null; then
ok "Claude CLI found: $(which claude)"
ok "Claude CLI: $(which claude)"
else
err "Claude CLI not found. Install from: https://docs.anthropic.com/en/docs/claude-code"
err "Claude CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
MISSING=1
fi
if command -v git &>/dev/null; then
ok "Git found: $(which git)"
ok "Git: $(which git)"
else
err "Git not found. Install git first."
err "Git not found."
MISSING=1
fi
if [[ "$(uname)" == "Darwin" ]]; then
ok "macOS detected (caffeinate + launchd available)"
ok "macOS (caffeinate + launchd available)"
else
warn "Not macOS — caffeinate/launchd hooks will need manual Linux equivalents"
warn "Not macOS — sleep hooks need Linux equivalents (see README)"
fi
if git rev-parse --is-inside-work-tree &>/dev/null; then
PROJECT_DIR="$(git rev-parse --show-toplevel)"
ok "Git repo found: $PROJECT_DIR"
ok "Project: $PROJECT_DIR"
else
PROJECT_DIR="$(pwd)"
warn "Not in a git repo. Using current directory: $PROJECT_DIR"
warn "Not a git repo — using: $PROJECT_DIR"
fi
if [[ "$MISSING" -eq 1 ]]; then
echo ""
err "Missing prerequisites. Fix the above and re-run."
exit 1
fi
[[ "$MISSING" -eq 1 ]] && { echo ""; err "Fix the above and re-run."; exit 1; }
echo ""
# ── Create loop directory ───────────────────────────────────────────────────
echo -e " ${BOLD}Setting up loop directory...${NC}"
# ── Create loop/ directory ───────────────────────────────────────────────────
echo -e " ${BOLD}Creating loop files...${NC}"
echo ""
LOOP_DIR="$PROJECT_DIR/loop"
mkdir -p "$LOOP_DIR"
# Copy loop.sh
# ── loop.sh (embedded) ──────────────────────────────────────────────────────
if [[ -f "$LOOP_DIR/loop.sh" ]]; then
warn "loop/loop.sh already exists — skipping (won't overwrite)"
warn "loop/loop.sh exists — skipping"
else
cp "$TEMPLATE_DIR/loop.sh" "$LOOP_DIR/loop.sh"
cat > "$LOOP_DIR/loop.sh" << 'LOOPEOF'
#!/usr/bin/env sh
# Claude Code Overnight Automation — Loop Script
# Usage: ./loop/loop.sh
# Config via env vars: ITERATION_COUNT, ITERATION_DELAY, CLAUDE_AUTONOMOUS, etc.
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" | 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 "OVERNIGHT AUTOMATION STARTED"
log " Project: $PROJECT_DIR"
log " Prompt: $PROMPT_FILE"
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s delay)"
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry ${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. Stopping."; break
fi
section "ITERATION $i/$ITERATION_COUNT"
log " Remaining: $(remaining_tasks)"; log " Next: $(next_task)"; log ""
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
if [ -f "$PROMPT_FILE" ]; then
log " Starting Claude..."; log ""
"$CLAUDE_BIN" -p --dangerously-skip-permissions < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
CLAUDE_EXIT=$?; log ""; log " Exit 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 RETRY"
PLIST_LABEL="com.claude-loop.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}"; exit 0
fi
section "RATE LIMITED — WAITING"
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"; log " Sleeping ${RATE_LIMIT_WAIT}s..."
sleep "$RATE_LIMIT_WAIT"
if ! plan_has_tasks; then banner "ALL TASKS COMPLETE"; break; fi
log " Retrying..."; continue
fi
rate_limit_retries=0
section "ITERATION $i COMPLETE"
log " Remaining: $(remaining_tasks)"; log " Next: $(next_task)"
i=$((i + 1))
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
log " Pausing ${ITERATION_DELAY}s..."; sleep "$ITERATION_DELAY"
fi
done
banner "LOOP FINISHED"
log " Completed $((i - 1)) iterations"; log " Remaining: $(remaining_tasks)"; log ""
LOOPEOF
chmod +x "$LOOP_DIR/loop.sh"
ok "Created loop/loop.sh"
fi
# Copy prompt.md
# ── prompt.md (embedded) ────────────────────────────────────────────────────
if [[ -f "$LOOP_DIR/prompt.md" ]]; then
warn "loop/prompt.md already exists — skipping"
warn "loop/prompt.md exists — skipping"
else
cp "$TEMPLATE_DIR/prompt.md" "$LOOP_DIR/prompt.md"
ok "Created loop/prompt.md (edit this with your project rules)"
cat > "$LOOP_DIR/prompt.md" << 'PROMPTEOF'
You are executing a project roadmap autonomously. Read these files first:
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
2. Read any project documentation (README, CLAUDE.md, etc.) for conventions
## For each task in loop/plan.md:
1. Find the first unchecked `- [ ]` item
2. Understand what needs to be done
3. Implement it following the project's existing patterns and conventions
4. Run the project's type checker / linter / tests — fix all errors
5. Commit with a conventional message: `type(scope): description`
6. Mark the task `- [x]` in `loop/plan.md`
7. Move to the next unchecked task immediately
## Rules
- If tests fail, fix them before moving on
- If a task is difficult, make at least 30 genuine attempts before skipping
- Always run linter + type checker after code changes
- Do not stop until all tasks are checked or you are rate limited
PROMPTEOF
ok "Created loop/prompt.md"
fi
# Copy plan.md
# ── plan.md (embedded) ──────────────────────────────────────────────────────
if [[ -f "$LOOP_DIR/plan.md" ]]; then
warn "loop/plan.md already exists — skipping"
warn "loop/plan.md exists — skipping"
else
cp "$TEMPLATE_DIR/plan.md" "$LOOP_DIR/plan.md"
ok "Created loop/plan.md (edit this with your tasks)"
cat > "$LOOP_DIR/plan.md" << 'PLANEOF'
# Task Plan
## Phase 1
- [ ] **1.1** — First task description
- [ ] **1.2** — Second task description
## Phase 2
- [ ] **2.1** — Third task description
- [ ] **2.2** — Fourth task description
## Final
- [ ] **FINAL** — Run full test suite, fix failures, tag release
PLANEOF
ok "Created loop/plan.md"
fi
echo ""
@@ -121,130 +261,165 @@ echo ""
HOOKS_DIR="$HOME/.claude/hooks"
mkdir -p "$HOOKS_DIR"
for hook in prevent-sleep.sh stop-hook-autonomous.sh allow-sleep.sh; do
if [[ -f "$HOOKS_DIR/$hook" ]]; then
warn "$hook already exists — skipping (won't overwrite)"
else
cp "$TEMPLATE_DIR/hooks/$hook" "$HOOKS_DIR/$hook"
chmod +x "$HOOKS_DIR/$hook"
ok "Installed ~/.claude/hooks/$hook"
fi
# prevent-sleep.sh
if [[ -f "$HOOKS_DIR/prevent-sleep.sh" ]]; then
warn "prevent-sleep.sh exists — skipping"
else
cat > "$HOOKS_DIR/prevent-sleep.sh" << 'HOOKEOF'
#!/usr/bin/env bash
set -euo pipefail
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
if [[ -f "$PID_FILE" ]]; then
old_pid=$(cat "$PID_FILE")
kill -0 "$old_pid" 2>/dev/null && kill "$old_pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
caffeinate -i &
echo $! > "$PID_FILE"
exit 0
HOOKEOF
chmod +x "$HOOKS_DIR/prevent-sleep.sh"
ok "Installed ~/.claude/hooks/prevent-sleep.sh"
fi
# stop-hook-autonomous.sh
if [[ -f "$HOOKS_DIR/stop-hook-autonomous.sh" ]]; then
warn "stop-hook-autonomous.sh exists — skipping"
else
cat > "$HOOKS_DIR/stop-hook-autonomous.sh" << 'HOOKEOF'
#!/usr/bin/env bash
# "Ralph Wiggum" — blocks Claude from stopping until all plan tasks are done.
# Requires CLAUDE_AUTONOMOUS=1 to activate.
set -euo pipefail
BASE="${CLAUDE_PROJECT_DIR:-}"
if [[ -z "$BASE" ]] && command -v jq &>/dev/null; then
BASE=$(jq -r '.cwd // empty' 2>/dev/null || true)
fi
[[ -z "$BASE" ]] && BASE="$(pwd)"
PLAN_FILE="${CLAUDE_PLAN_FILE:-plan.md}"
ALT_FILES="loop/plan.md todo.md loop/todo.md"
AUTO_SLEEP_HOOK="$HOME/.claude/hooks/allow-sleep.sh"
plan=""
for f in "$PLAN_FILE" $ALT_FILES; do
[[ -z "$f" ]] && continue
if [[ "$f" == /* ]]; then path="$f"; else path="$BASE/$f"; fi
if [[ -f "$path" ]]; then plan="$path"; break; fi
done
if [[ -z "${CLAUDE_AUTONOMOUS:-}" ]] || [[ "$CLAUDE_AUTONOMOUS" == "0" ]]; then
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true; exit 0
fi
if [[ -z "$plan" ]] || [[ ! -f "$plan" ]]; then
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true; exit 0
fi
incomplete=$(grep -c -E '^\s*[-*]?\s*\[\s*\]' "$plan" 2>/dev/null || echo 0)
if [[ "${incomplete:-0}" -gt 0 ]]; then
echo '{"decision":"block","reason":"Plan has '"$incomplete"' incomplete task(s). Continue with the next item."}'
exit 0
fi
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
exit 0
HOOKEOF
chmod +x "$HOOKS_DIR/stop-hook-autonomous.sh"
ok "Installed ~/.claude/hooks/stop-hook-autonomous.sh"
fi
# allow-sleep.sh
if [[ -f "$HOOKS_DIR/allow-sleep.sh" ]]; then
warn "allow-sleep.sh exists — skipping"
else
cat > "$HOOKS_DIR/allow-sleep.sh" << 'HOOKEOF'
#!/usr/bin/env bash
set -euo pipefail
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
exit 0
HOOKEOF
chmod +x "$HOOKS_DIR/allow-sleep.sh"
ok "Installed ~/.claude/hooks/allow-sleep.sh"
fi
echo ""
# ── Update settings.json ────────────────────────────────────────────────────
# ── Register hooks in settings.json ─────────────────────────────────────────
echo -e " ${BOLD}Configuring Claude settings...${NC}"
echo ""
SETTINGS_FILE="$HOME/.claude/settings.json"
# Backup existing settings
if [[ -f "$SETTINGS_FILE" ]]; then
cp "$SETTINGS_FILE" "${SETTINGS_FILE}.backup.$(date +%s)"
info "Backed up existing settings.json"
info "Backed up settings.json"
fi
# Check if hooks are already registered
if [[ -f "$SETTINGS_FILE" ]] && grep -q "stop-hook-autonomous" "$SETTINGS_FILE" 2>/dev/null; then
ok "Hooks already registered in settings.json"
ok "Hooks already registered"
else
# Create or update settings.json with hook registrations
if command -v python3 &>/dev/null; then
python3 << 'PYEOF'
import json, os
settings_path = os.path.expanduser("~/.claude/settings.json")
hooks_dir = os.path.expanduser("~/.claude/hooks")
# Load existing or start fresh
if os.path.exists(settings_path):
with open(settings_path) as f:
settings = json.load(f)
else:
settings = {}
# Ensure hooks section exists
if "hooks" not in settings:
settings["hooks"] = {}
hooks = settings["hooks"]
# Add UserPromptSubmit hook (prevent sleep)
if "UserPromptSubmit" not in hooks:
hooks["UserPromptSubmit"] = []
prevent_sleep = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/prevent-sleep.sh"}]}
if not any("prevent-sleep" in json.dumps(h) for h in hooks["UserPromptSubmit"]):
hooks["UserPromptSubmit"].append(prevent_sleep)
# Add Stop hook (autonomous block)
if "Stop" not in hooks:
hooks["Stop"] = []
stop_hook = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/stop-hook-autonomous.sh"}]}
if not any("stop-hook-autonomous" in json.dumps(h) for h in hooks["Stop"]):
hooks["Stop"].append(stop_hook)
# Add SessionEnd hook (allow sleep)
if "SessionEnd" not in hooks:
hooks["SessionEnd"] = []
session_end = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/allow-sleep.sh"}]}
if not any("allow-sleep" in json.dumps(h) for h in hooks["SessionEnd"]):
hooks["SessionEnd"].append(session_end)
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
f.write("\n")
print("done")
p = os.path.expanduser("~/.claude/settings.json")
h = os.path.expanduser("~/.claude/hooks")
s = json.load(open(p)) if os.path.exists(p) else {}
if "hooks" not in s: s["hooks"] = {}
for event, script in [("UserPromptSubmit","prevent-sleep.sh"),("Stop","stop-hook-autonomous.sh"),("SessionEnd","allow-sleep.sh")]:
if event not in s["hooks"]: s["hooks"][event] = []
if not any(script in json.dumps(x) for x in s["hooks"][event]):
s["hooks"][event].append({"matcher":"","hooks":[{"type":"command","command":f"{h}/{script}"}]})
with open(p,"w") as f: json.dump(s,f,indent=2); f.write("\n")
PYEOF
ok "Registered hooks in ~/.claude/settings.json"
ok "Registered hooks in settings.json"
else
warn "python3 not found — you'll need to manually add hooks to ~/.claude/settings.json"
info "See the README for the required settings.json format"
warn "python3 not found — add hooks to ~/.claude/settings.json manually"
fi
fi
echo ""
# ── Summary ──────────────────────────────────────────────────────────────────
# ── Done ─────────────────────────────────────────────────────────────────────
echo -e " ${BOLD}${GREEN}Setup complete!${NC}"
echo ""
echo -e " ${DIM}────────────────────────────────────────────────────────${NC}"
echo -e " ${DIM}════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}How to use:${NC}"
echo -e " ${BOLD}You need to do two things:${NC}"
echo ""
echo -e " ${CYAN}1.${NC} Edit your task list:"
echo -e " ${DIM}vim loop/plan.md${NC}"
echo -e " ${CYAN}1.${NC} ${BOLD}Edit your task list${NC} — add your actual tasks:"
echo ""
echo -e " ${CYAN}2.${NC} Edit your prompt (add your project's rules and specs):"
echo -e " ${DIM}vim loop/prompt.md${NC}"
echo -e " ${DIM}$LOOP_DIR/plan.md${NC}"
echo ""
echo -e " Replace the {{PLACEHOLDERS}} with your project's:"
echo -e " - Spec file path (detailed task descriptions)"
echo -e " - Project rules file path (coding conventions)"
echo -e " - Inline project rules (style, frameworks, constraints)"
echo -e " Format: one ${BOLD}- [ ]${NC} per task. Claude checks them off as it goes."
echo -e " Example:"
echo -e " ${DIM}- [ ] Add user authentication with JWT${NC}"
echo -e " ${DIM}- [ ] Create REST API for CRUD operations${NC}"
echo -e " ${DIM}- [ ] Add unit tests for auth module${NC}"
echo ""
echo -e " ${CYAN}3.${NC} Start the overnight run:"
echo -e " ${DIM}./loop/loop.sh${NC}"
echo -e " ${CYAN}2.${NC} ${BOLD}Edit your prompt${NC} — add project-specific rules:"
echo ""
echo -e " Or with caffeinate (belt-and-suspenders sleep prevention):"
echo -e " ${DIM}caffeinate -i ./loop/loop.sh${NC}"
echo -e " ${DIM}$LOOP_DIR/prompt.md${NC}"
echo ""
echo -e " ${CYAN}4.${NC} Monitor progress:"
echo -e " ${DIM}tail -f loop/loop.log${NC}"
echo -e " ${DIM}grep -c '\\[x\\]' loop/plan.md${NC} # completed tasks"
echo -e " ${DIM}grep -c '\\[ \\]' loop/plan.md${NC} # remaining tasks"
echo -e " Tell Claude about your stack, coding style, test commands,"
echo -e " and anything else it should know about your project."
echo ""
echo -e " ${BOLD}Configuration:${NC}"
echo -e " ${DIM}════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${DIM}CLAUDE_AUTONOMOUS=1${NC} Stop hook active (default)"
echo -e " ${DIM}CLAUDE_AUTONOMOUS=0${NC} Claude can stop freely"
echo -e " ${DIM}ITERATION_COUNT=20${NC} Run 20 iterations"
echo -e " ${DIM}ITERATION_DELAY=60${NC} 60s between iterations"
echo -e " ${BOLD}Then run:${NC}"
echo ""
echo -e " ${BOLD}Quick test:${NC}"
echo -e " ${GREEN}./loop/loop.sh${NC}"
echo ""
echo -e " Or for guaranteed no-sleep (macOS):"
echo -e " ${GREEN}caffeinate -i ./loop/loop.sh${NC}"
echo ""
echo -e " ${BOLD}Monitor:${NC} ${DIM}tail -f loop/loop.log${NC}"
echo -e " ${BOLD}Progress:${NC} ${DIM}grep -c '\\[x\\]' loop/plan.md${NC}"
echo ""
echo -e " ${BOLD}Config (env vars):${NC}"
echo -e " ${DIM}CLAUDE_AUTONOMOUS=0${NC} Let Claude stop freely (for testing)"
echo -e " ${DIM}ITERATION_COUNT=20${NC} More iterations"
echo -e " ${DIM}ITERATION_DELAY=60${NC} Longer pause between iterations"
echo ""
echo -e " ${BOLD}Test run:${NC}"
echo -e " ${DIM}CLAUDE_AUTONOMOUS=0 ITERATION_COUNT=1 ./loop/loop.sh${NC}"
echo ""
echo -e " ${DIM}────────────────────────────────────────────────────────${NC}"
echo -e " ${DIM}Full documentation: README.md${NC}"
echo ""
-15
View File
@@ -1,15 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Allow macOS to sleep again after Claude Code session ends.
# Kills the caffeinate process started by prevent-sleep.sh.
# ============================================================================
set -euo pipefail
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
exit 0
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Prevent macOS from sleeping during Claude Code sessions.
# Started on UserPromptSubmit hook; killed by allow-sleep.sh on Stop/SessionEnd.
# Uses caffeinate -i (prevent idle sleep) without -t so it runs until killed.
# ============================================================================
set -euo pipefail
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
# Kill any existing caffeinate from a previous session
if [[ -f "$PID_FILE" ]]; then
old_pid=$(cat "$PID_FILE")
kill -0 "$old_pid" 2>/dev/null && kill "$old_pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
# Start caffeinate in background (runs until killed)
caffeinate -i &
echo $! > "$PID_FILE"
exit 0
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# "Ralph Wiggum" Stop Hook — Prevents Claude from stopping until all tasks
# in plan.md are complete.
#
# How it works:
# - Runs every time Claude tries to stop (end its session)
# - Reads plan.md and counts unchecked [ ] tasks
# - If tasks remain AND CLAUDE_AUTONOMOUS=1: returns {"decision":"block"}
# which tells Claude to keep going
# - If all tasks done or autonomous mode off: allows the stop
#
# Requires: CLAUDE_AUTONOMOUS=1 environment variable to activate
# ============================================================================
set -euo pipefail
# Determine project directory
BASE="${CLAUDE_PROJECT_DIR:-}"
if [[ -z "$BASE" ]] && command -v jq &>/dev/null; then
BASE=$(jq -r '.cwd // empty' 2>/dev/null || true)
fi
[[ -z "$BASE" ]] && BASE="$(pwd)"
PLAN_FILE="${CLAUDE_PLAN_FILE:-plan.md}"
ALT_FILES="loop/plan.md todo.md loop/todo.md"
AUTO_SLEEP_HOOK="$HOME/.claude/hooks/allow-sleep.sh"
# Find the plan file
plan=""
for f in "$PLAN_FILE" $ALT_FILES; do
[[ -z "$f" ]] && continue
if [[ "$f" == /* ]]; then
path="$f"
else
path="$BASE/$f"
fi
if [[ -f "$path" ]]; then
plan="$path"
break
fi
done
# If autonomous mode is off or no plan file found, allow stop
if [[ -z "${CLAUDE_AUTONOMOUS:-}" ]] || [[ "$CLAUDE_AUTONOMOUS" == "0" ]]; then
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
exit 0
fi
if [[ -z "$plan" ]] || [[ ! -f "$plan" ]]; then
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
exit 0
fi
# Count incomplete tasks (standard markdown checkbox: - [ ] or * [ ])
incomplete=$(grep -c -E '^\s*[-*]?\s*\[\s*\]' "$plan" 2>/dev/null || echo 0)
if [[ "${incomplete:-0}" -gt 0 ]]; then
# Block the stop — Claude must continue
echo '{"decision":"block","reason":"Plan has '"$incomplete"' incomplete task(s). Continue with the next item."}'
exit 0
fi
# All tasks complete — allow stop and allow sleep
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
exit 0
-224
View File
@@ -1,224 +0,0 @@
#!/usr/bin/env sh
# ============================================================================
# Claude Code Overnight Automation — Loop Script
# ============================================================================
# Runs Claude headlessly in a loop, executing tasks from plan.md.
# Rate-limit aware with automatic retry and macOS launchd scheduling.
#
# Usage:
# ./loop/loop.sh # Run with defaults
# ITERATION_COUNT=20 ./loop/loop.sh # 20 iterations
# CLAUDE_AUTONOMOUS=0 ./loop/loop.sh # Allow Claude to stop freely
#
# Environment variables (all optional):
# CLAUDE_AUTONOMOUS — 1 to block stops until plan complete (default: 1)
# ITERATION_COUNT — Max loop iterations (default: 10)
# ITERATION_DELAY — Seconds between iterations (default: 30)
# RATE_LIMIT_WAIT — Seconds to wait on rate limit (default: 3600)
# MAX_RATE_LIMIT_RETRIES — Max retries before launchd (default: 5)
# CLAUDE_BIN — Path to claude binary (default: claude)
# PROMPT_FILE — Path to prompt file (default: loop/prompt.md)
# LOG_FILE — Path to log file (default: loop/loop.log)
# ============================================================================
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"
# ── Logging helpers ──────────────────────────────────────────────────────────
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 ""
}
# ── Task helpers ─────────────────────────────────────────────────────────────
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)"
}
# ── Rate limit detection ────────────────────────────────────────────────────
check_rate_limit() {
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
tail -50 "$LOG_FILE" 2>/dev/null \
| grep -v "^Rate limit" | 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
}
# ── Main loop ────────────────────────────────────────────────────────────────
banner "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
# Stop if no tasks remain
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}"
# Run Claude in headless mode with prompt from file
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
# ── Rate limit handling ──────────────────────────────────────────────────
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 RETRY"
log " Hit rate limit $rate_limit_retries times."
# Schedule retry via macOS launchd (self-destructing plist)
PLIST_LABEL="com.claude-loop.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}"
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..."
sleep "$RATE_LIMIT_WAIT"
if ! plan_has_tasks; then
banner "ALL TASKS COMPLETE (during rate limit wait)"
break
fi
log " Retrying..."
continue
fi
# Reset rate limit counter on successful run
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 ""
-19
View File
@@ -1,19 +0,0 @@
# Task Plan
## Phase 1: Setup
- [ ] **1.1** — Initialize project structure and install dependencies
- [ ] **1.2** — Set up linting, formatting, and type checking
- [ ] **1.3** — Add CI pipeline (test + lint + build)
## Phase 2: Core Features
- [ ] **2.1** — Implement feature A
- [ ] **2.2** — Implement feature B
- [ ] **2.3** — Implement feature C
## Phase 3: Polish
- [ ] **3.1** — Add error handling and edge cases
- [ ] **3.2** — Performance optimization
- [ ] **3.3** — Documentation
## Final
- [ ] **FINAL** — Run full test suite. Fix any failures. Tag release.
-28
View File
@@ -1,28 +0,0 @@
You are executing a project roadmap autonomously. Read these files first:
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
2. `{{SPEC_FILE}}` — Detailed specs for each task
3. `{{PROJECT_RULES_FILE}}` — Project conventions and coding standards
## Project Rules
{{PROJECT_RULES}}
## For each task in loop/plan.md:
1. Find the first unchecked `- [ ]` item
2. Read the detailed spec for that task
3. Implement the feature following the spec and project rules above
4. Run your project's type checker and linter — fix all errors before committing
5. Run your project's tests — fix any failing tests
6. If the task has a **Testing Gate** (e.g., specific test command), run it and fix failures
7. Commit with a conventional commit message: `type(scope): description`
8. Mark the task done `- [x]` in `loop/plan.md`
9. Move to the next unchecked task immediately
## Rules
- Never skip a testing gate — if tests fail, fix them before moving on
- If a task is proving difficult, keep trying different approaches. Make at least 30 genuine attempts before moving on. If you do move on, leave the task unchecked and note the issue.
- Always run type checker + linter after every code change
- Do not stop until all tasks are checked or you are rate limited