diff --git a/For Others/setup.sh b/For Others/setup.sh index e866cf26..8aa112e2 100755 --- a/For Others/setup.sh +++ b/For Others/setup.sh @@ -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" < + + + 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} + + StartCalendarIntervalHour${RETRY_HOUR}Minute${RETRY_MIN} + EnvironmentVariables + CLAUDE_AUTONOMOUS1 + 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}"; 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 "" diff --git a/For Others/templates/hooks/allow-sleep.sh b/For Others/templates/hooks/allow-sleep.sh deleted file mode 100755 index 1c4b9dfc..00000000 --- a/For Others/templates/hooks/allow-sleep.sh +++ /dev/null @@ -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 diff --git a/For Others/templates/hooks/prevent-sleep.sh b/For Others/templates/hooks/prevent-sleep.sh deleted file mode 100755 index ba2edf30..00000000 --- a/For Others/templates/hooks/prevent-sleep.sh +++ /dev/null @@ -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 diff --git a/For Others/templates/hooks/stop-hook-autonomous.sh b/For Others/templates/hooks/stop-hook-autonomous.sh deleted file mode 100755 index d9542999..00000000 --- a/For Others/templates/hooks/stop-hook-autonomous.sh +++ /dev/null @@ -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 diff --git a/For Others/templates/loop.sh b/For Others/templates/loop.sh deleted file mode 100755 index 6554d456..00000000 --- a/For Others/templates/loop.sh +++ /dev/null @@ -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" < - - - - 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..." - 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 "" diff --git a/For Others/templates/plan.md b/For Others/templates/plan.md deleted file mode 100644 index 82dde45f..00000000 --- a/For Others/templates/plan.md +++ /dev/null @@ -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. diff --git a/For Others/templates/prompt.md b/For Others/templates/prompt.md deleted file mode 100644 index 71bdfa71..00000000 --- a/For Others/templates/prompt.md +++ /dev/null @@ -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