From b96d96b654056d8e2a149610996f27d14c819431 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 08:31:41 +0000 Subject: [PATCH] docs: add overnight automation guide and setup script for others Comprehensive standalone guide + setup script so anyone can replicate the Claude Code overnight automation system for their own projects. Includes loop.sh, hook templates, plan/prompt templates, and an interactive setup script. No project-specific content included. Co-Authored-By: Claude Opus 4.6 --- For Others/README.md | 267 ++++++++++++++++++ For Others/setup.sh | 250 ++++++++++++++++ For Others/templates/hooks/allow-sleep.sh | 15 + For Others/templates/hooks/prevent-sleep.sh | 21 ++ .../templates/hooks/stop-hook-autonomous.sh | 65 +++++ For Others/templates/loop.sh | 224 +++++++++++++++ For Others/templates/plan.md | 19 ++ For Others/templates/prompt.md | 28 ++ 8 files changed, 889 insertions(+) create mode 100644 For Others/README.md create mode 100755 For Others/setup.sh create mode 100755 For Others/templates/hooks/allow-sleep.sh create mode 100755 For Others/templates/hooks/prevent-sleep.sh create mode 100755 For Others/templates/hooks/stop-hook-autonomous.sh create mode 100755 For Others/templates/loop.sh create mode 100644 For Others/templates/plan.md create mode 100644 For Others/templates/prompt.md diff --git a/For Others/README.md b/For Others/README.md new file mode 100644 index 00000000..2c394326 --- /dev/null +++ b/For Others/README.md @@ -0,0 +1,267 @@ +# Claude Code Overnight Automation + +Run Claude Code headlessly overnight to execute a full task checklist — with rate-limit resilience, macOS sleep prevention, and a stop hook that prevents Claude from quitting until every task is done. + +## How It Works + +``` +loop.sh (orchestrator) + | + +--> Reads plan.md for unchecked [ ] tasks + +--> Pipes prompt.md into `claude -p` (headless mode) + | | + | +--> Claude reads your plan, specs, and project rules + | +--> Implements tasks one by one + | +--> Runs typecheck/lint/test after each + | +--> Commits, marks [x], moves to next + | | + | +--> Claude tries to stop + | | + | +--> Stop Hook intercepts + | +--> Checks plan.md for remaining [ ] tasks + | +--> If incomplete: BLOCKS the stop (Claude continues) + | +--> If all done: allows stop + | + +--> Detects rate limits in output + | +--> Sleeps 1 hour, retries (up to 5x) + | +--> After 5 retries: schedules macOS launchd job to resume later + | + +--> Loops N iterations (default 10) + +--> Exits when all tasks checked or iterations exhausted +``` + +### The "Ralph Wiggum" Stop Hook + +The secret sauce. Claude Code supports a `Stop` hook — a shell script that runs every time Claude tries to end its session. By returning `{"decision":"block"}`, the hook **prevents Claude from stopping**. Combined with `--dangerously-skip-permissions`, Claude becomes a fully autonomous task executor that won't quit until the job is done. + +### Sleep Prevention + +On macOS, `caffeinate -i` prevents idle sleep during long runs. A hook starts it when Claude begins and kills it when Claude finishes. + +### Rate Limit Resilience + +If Claude hits API rate limits: +1. **Inline retry**: Sleep 1 hour, then retry the same iteration +2. **Scheduled retry**: After 5 failed retries, create a macOS `launchd` plist that auto-runs the loop later +3. The plist self-destructs after executing + +## Prerequisites + +- **Claude Code CLI** (`claude` command available in PATH) + - Install: https://docs.anthropic.com/en/docs/claude-code + - Must be logged in: run `claude login` first +- **macOS** (for `caffeinate` and `launchd` — see Linux notes below) +- **Git** (the script commits after each task) +- A project with `package.json` or similar build tooling + +## Quick Start + +```bash +# 1. Clone or copy this folder into your project +cp -r "For Others/templates" ~/my-project/loop + +# 2. Run the setup script (creates hooks, updates settings) +cd ~/my-project +bash "path/to/For Others/setup.sh" + +# 3. Edit your task list +vim loop/plan.md + +# 4. Edit your prompt (project-specific rules) +vim loop/prompt.md + +# 5. Start the overnight run +./loop/loop.sh +``` + +Or just run the setup script — it walks you through everything: + +```bash +bash "For Others/setup.sh" +``` + +## File Structure + +After setup, your project will have: + +``` +your-project/ + loop/ + loop.sh # Main orchestrator (run this) + prompt.md # Instructions piped to Claude each iteration + plan.md # Task checklist ([ ] = todo, [x] = done) + loop.log # Full output log (auto-created) + +~/.claude/ + hooks/ + prevent-sleep.sh # Starts caffeinate on session start + stop-hook-autonomous.sh # Blocks stop until tasks complete + allow-sleep.sh # Kills caffeinate on session end + settings.json # Hook registrations (auto-updated by setup) +``` + +## Configuration + +All config is via environment variables (set before running `loop.sh` or export in your shell): + +| Variable | Default | Description | +|----------|---------|-------------| +| `CLAUDE_AUTONOMOUS` | `1` | Set to `0` to disable the stop hook (Claude can quit freely) | +| `ITERATION_COUNT` | `10` | Max loop iterations | +| `ITERATION_DELAY` | `30` | Seconds to pause between iterations | +| `RATE_LIMIT_WAIT` | `3600` | Seconds to sleep when rate limited (1 hour) | +| `MAX_RATE_LIMIT_RETRIES` | `5` | Retries before scheduling launchd | +| `CLAUDE_BIN` | `claude` | Path to Claude CLI binary | +| `PROMPT_FILE` | `loop/prompt.md` | Path to prompt file | +| `LOG_FILE` | `loop/loop.log` | Path to log file | + +### Examples + +```bash +# Quick test run (2 iterations, 10s delay, no stop hook) +CLAUDE_AUTONOMOUS=0 ITERATION_COUNT=2 ITERATION_DELAY=10 ./loop/loop.sh + +# Full overnight run (20 iterations, 1 min between) +ITERATION_COUNT=20 ITERATION_DELAY=60 ./loop/loop.sh + +# Use a custom prompt +PROMPT_FILE=my-prompt.md ./loop/loop.sh +``` + +## Writing Your Plan + +`loop/plan.md` is a markdown checklist. Each line starting with `- [ ]` is a pending task: + +```markdown +## Phase 1: Core Features +- [ ] **1.1** — Add user authentication (JWT + refresh tokens) +- [ ] **1.2** — Create user profile page with avatar upload +- [ ] **1.3** — Add settings page with theme toggle + +## Phase 2: API +- [ ] **2.1** — REST endpoints for CRUD operations +- [ ] **2.2** — WebSocket support for real-time updates + +## Final +- [ ] **FINAL** — Run full test suite, fix any failures, tag release +``` + +Claude will: +1. Find the first `- [ ]` line +2. Read the spec from your prompt or a separate spec file +3. Implement it +4. Mark it `- [x]` +5. Move to the next + +### Tips for good plans + +- **Be specific**: "Add JWT auth with refresh tokens, store in httpOnly cookies" > "Add auth" +- **Order matters**: Put foundational tasks first (types, utils, config) before features that depend on them +- **Include testing gates**: "Run `pnpm test` and fix failures" as part of each task +- **Keep tasks small**: 30-60 minutes of work each. Large tasks lead to context window exhaustion +- **Add a FINAL task**: A catchall that runs the full test suite + +## Writing Your Prompt + +`loop/prompt.md` is what Claude reads at the start of every iteration. Include: + +1. **What files to read** (your plan, specs, project conventions) +2. **Project-specific rules** (coding style, frameworks, constraints) +3. **Per-task workflow** (implement → test → commit → mark done) +4. **Hard rules** (what to never do, minimum effort before skipping) + +See `templates/prompt.md` for a starting template. + +## Operating the Loop + +### Starting + +```bash +# Foreground (see output live) +./loop/loop.sh + +# Background with logging +nohup ./loop/loop.sh > /dev/null 2>&1 & + +# With caffeinate (prevents sleep even if hooks fail) +caffeinate -i ./loop/loop.sh +``` + +### Monitoring + +```bash +# Watch the log live +tail -f loop/loop.log + +# Check progress +grep -c '\- \[x\]' loop/plan.md # completed +grep -c '\- \[ \]' loop/plan.md # remaining + +# Check git commits +git log --oneline -20 +``` + +### Stopping + +- **Let it finish**: The loop stops automatically when all tasks are checked +- **Kill it**: `Ctrl+C` or `kill %1` — Claude's current task will be interrupted but committed work is preserved +- **Disable stop hook**: Set `CLAUDE_AUTONOMOUS=0` in the environment before the next iteration + +### Resuming + +Just run `./loop/loop.sh` again. It reads `plan.md` fresh each iteration, so it picks up where it left off (skipping `[x]` tasks). + +## Customizing the Prompt + +The prompt template has `{{PLACEHOLDER}}` markers. Replace them with your project's specifics: + +| Placeholder | What to put | +|-------------|-------------| +| `{{SPEC_FILE}}` | Path to your detailed spec (e.g., `SPEC.md`, `docs/plan.md`) | +| `{{PROJECT_RULES_FILE}}` | Path to your coding conventions file | +| `{{PROJECT_RULES}}` | Inline coding rules (style, frameworks, constraints) | + +## Troubleshooting + +### Claude exits immediately +- Make sure `claude login` has been run +- Check that `claude -p "hello"` works in your terminal +- Verify `~/.claude/hooks/stop-hook-autonomous.sh` exists and is executable + +### Rate limit loop +- Default wait is 1 hour. Increase `RATE_LIMIT_WAIT` if your limits are longer +- Check `loop.log` for the specific rate limit message +- Claude Max subscriptions have higher limits than API keys + +### Mac goes to sleep +- Run `caffeinate -i ./loop/loop.sh` as a belt-and-suspenders approach +- Check that `~/.claude/hooks/prevent-sleep.sh` is executable: `chmod +x ~/.claude/hooks/prevent-sleep.sh` + +### Tasks not getting marked complete +- Ensure your plan uses exact format: `- [ ]` (dash, space, brackets, space) +- The stop hook matches `^\s*[-*]?\s*\[\s*\]` — standard markdown checkboxes + +### Stop hook not working +- Verify `CLAUDE_AUTONOMOUS=1` is set: `echo $CLAUDE_AUTONOMOUS` +- Check hook is registered in `~/.claude/settings.json` +- Test the hook manually: `echo '{}' | bash ~/.claude/hooks/stop-hook-autonomous.sh` + +## Linux Notes + +The system is macOS-focused but works on Linux with minor changes: + +- **Sleep prevention**: Replace `caffeinate` with `systemd-inhibit --what=idle --who=claude-loop --why="Overnight automation" sleep infinity &` or simply disable sleep via `systemctl mask sleep.target` +- **Scheduled retry**: Replace the launchd plist section in `loop.sh` with a `systemd-run --on-calendar` or `at` command +- **Hooks work identically** — they're plain bash scripts + +## Security Notes + +- `--dangerously-skip-permissions` gives Claude **full system access** within the project. Only run on trusted codebases. +- The loop runs as your user — Claude can read/write anything you can +- API keys in `.env.local` are accessible to Claude during the session +- Review commits after an overnight run before pushing to production +- Consider running in a VM or container for additional isolation + +## License + +MIT. Use it however you want. diff --git a/For Others/setup.sh b/For Others/setup.sh new file mode 100755 index 00000000..e866cf26 --- /dev/null +++ b/For Others/setup.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# ============================================================================ +# Claude Code Overnight Automation — Setup Script +# ============================================================================ +# Creates the loop directory, hook scripts, and configures settings.json. +# Run this from your project root: +# bash path/to/setup.sh +# ============================================================================ +set -euo pipefail + +# ── Colors ─────────────────────────────────────────────────────────────────── +BOLD='\033[1m' +DIM='\033[2m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +RED='\033[0;31m' +NC='\033[0m' + +ok() { echo -e " ${GREEN}+${NC} $1"; } +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 ───────────────────────────────────────────────────── +echo -e " ${BOLD}Checking prerequisites...${NC}" +echo "" + +MISSING=0 + +if command -v claude &>/dev/null; then + ok "Claude CLI found: $(which claude)" +else + err "Claude CLI not found. Install from: https://docs.anthropic.com/en/docs/claude-code" + MISSING=1 +fi + +if command -v git &>/dev/null; then + ok "Git found: $(which git)" +else + err "Git not found. Install git first." + MISSING=1 +fi + +if [[ "$(uname)" == "Darwin" ]]; then + ok "macOS detected (caffeinate + launchd available)" +else + warn "Not macOS — caffeinate/launchd hooks will need manual Linux equivalents" +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" +else + PROJECT_DIR="$(pwd)" + warn "Not in a git repo. Using current directory: $PROJECT_DIR" +fi + +if [[ "$MISSING" -eq 1 ]]; then + echo "" + err "Missing prerequisites. Fix the above and re-run." + exit 1 +fi + +echo "" + +# ── Create loop directory ──────────────────────────────────────────────────── +echo -e " ${BOLD}Setting up loop directory...${NC}" +echo "" + +LOOP_DIR="$PROJECT_DIR/loop" +mkdir -p "$LOOP_DIR" + +# Copy loop.sh +if [[ -f "$LOOP_DIR/loop.sh" ]]; then + warn "loop/loop.sh already exists — skipping (won't overwrite)" +else + cp "$TEMPLATE_DIR/loop.sh" "$LOOP_DIR/loop.sh" + chmod +x "$LOOP_DIR/loop.sh" + ok "Created loop/loop.sh" +fi + +# Copy prompt.md +if [[ -f "$LOOP_DIR/prompt.md" ]]; then + warn "loop/prompt.md already exists — skipping" +else + cp "$TEMPLATE_DIR/prompt.md" "$LOOP_DIR/prompt.md" + ok "Created loop/prompt.md (edit this with your project rules)" +fi + +# Copy plan.md +if [[ -f "$LOOP_DIR/plan.md" ]]; then + warn "loop/plan.md already exists — skipping" +else + cp "$TEMPLATE_DIR/plan.md" "$LOOP_DIR/plan.md" + ok "Created loop/plan.md (edit this with your tasks)" +fi + +echo "" + +# ── Install hooks ──────────────────────────────────────────────────────────── +echo -e " ${BOLD}Installing hooks...${NC}" +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 +done + +echo "" + +# ── Update 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" +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" +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") +PYEOF + ok "Registered hooks in ~/.claude/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" + fi +fi + +echo "" + +# ── Summary ────────────────────────────────────────────────────────────────── +echo -e " ${BOLD}${GREEN}Setup complete!${NC}" +echo "" +echo -e " ${DIM}────────────────────────────────────────────────────────${NC}" +echo "" +echo -e " ${BOLD}How to use:${NC}" +echo "" +echo -e " ${CYAN}1.${NC} Edit your task list:" +echo -e " ${DIM}vim loop/plan.md${NC}" +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 "" +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 "" +echo -e " ${CYAN}3.${NC} Start the overnight run:" +echo -e " ${DIM}./loop/loop.sh${NC}" +echo "" +echo -e " Or with caffeinate (belt-and-suspenders sleep prevention):" +echo -e " ${DIM}caffeinate -i ./loop/loop.sh${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 "" +echo -e " ${BOLD}Configuration:${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 "" +echo -e " ${BOLD}Quick test:${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 new file mode 100755 index 00000000..1c4b9dfc --- /dev/null +++ b/For Others/templates/hooks/allow-sleep.sh @@ -0,0 +1,15 @@ +#!/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 new file mode 100755 index 00000000..ba2edf30 --- /dev/null +++ b/For Others/templates/hooks/prevent-sleep.sh @@ -0,0 +1,21 @@ +#!/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 new file mode 100755 index 00000000..d9542999 --- /dev/null +++ b/For Others/templates/hooks/stop-hook-autonomous.sh @@ -0,0 +1,65 @@ +#!/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 new file mode 100755 index 00000000..6554d456 --- /dev/null +++ b/For Others/templates/loop.sh @@ -0,0 +1,224 @@ +#!/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 new file mode 100644 index 00000000..82dde45f --- /dev/null +++ b/For Others/templates/plan.md @@ -0,0 +1,19 @@ +# 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 new file mode 100644 index 00000000..71bdfa71 --- /dev/null +++ b/For Others/templates/prompt.md @@ -0,0 +1,28 @@ +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