Skip to main content
PROJECT(1) PROJECT(1)

NAME

Ralph Wiggum loops for LLM coding

Leveraging Ralph Wiggum bash loops for streamlining development against large task lists

SYNOPSIS

Date: January 18, 2026
Status: Complete
Tags: [LLM] [Coding] [Claude]
DESCRIPTION

I’m a bit late on this one, but I read about Ralph Wiggum loops and became interested. The concept is pretty straightforward: the longer you run a session, the bigger the context window gets, and the more likely you are to receive hallucinations.

Instead, we craft a detailed PRD, use that to generate tasks, and then craft a prompt for the loop. This gives claude all of the context it needs to start fresh, but the activity logs lets it know what we’ve done recently as well. You include testing criteria so it won’t move forward in error, and you have a pretty good approach.

Creating the Task List

After I have a PRD that I like, I tell claude to analyze it and create the tasks. Here is an example prompt I used recently to do this. It then updated the plan.md file for me to use in my later prompt.

Review the @[prd-file] and then create a new series of tasks to accomplish it. we should then analyze the complexity of each task to determine if it needs to be subtasks. lets make sure we include tests for each task to ensure that we got it right. write the tasks out to plan.md

Ralph

I took the basic loop approach, but I also wanted to be able to see the activity along the way so I knew what was happening in the background. I did some basic tailing with jq initially, but ultimately created a method that used inotify to pickup on file changes, and display it in the foreground.

ralph.sh

#!/bin/bash
set -euo pipefail

# Detect project path from current directory
cwd=$(pwd)
proj_name=$(echo "$cwd" | tr '/' '-')
proj="$HOME/.claude/projects/$proj_name"

# Verify it exists
if [[ ! -d "$proj" ]]; then
  echo "❌ No Claude project found for: $cwd"
  echo "   Expected: $proj"
  echo ""
  echo "   Have you run claude in this directory before?"
  exit 1
fi

max_iterations="${1:-10}"

# Pids to track
tail_pid=""
inotify_pid=""

cleanup() {
  echo -e "\n🛑 Stopping..."
  [[ -n "$tail_pid" ]] && kill "$tail_pid" 2>/dev/null
  [[ -n "$inotify_pid" ]] && kill "$inotify_pid" 2>/dev/null
  rm -f "/tmp/ralph-latest-file-$$" "/tmp/ralph-result-$$.txt"
  wait 2>/dev/null
  exit 0
}
trap cleanup SIGINT SIGTERM EXIT

jq_filter='
if .type == "assistant" then
  .message.content[]? |
  if .type == "text" then "💬 \(.text[0:200])"
  elif .type == "tool_use" then
    if .name == "Write" then "📝 Write: \(.input.file_path)"
    elif .name == "Read" then "👁️  Read: \(.input.file_path)"
    elif .name == "Bash" then "💻 Bash: \(.input.command[0:120])"
    elif .name == "TodoWrite" then "📋 Todo: \(.input.todos[0].content[0:80] // "update")"
    else "🔧 \(.name)"
    end
  else empty end
elif .type == "user" then
  .message.content[]? | select(.type == "tool_result") |
  if .is_error then "   ❌ \(.content)"
  else "   ✅"
  end
else empty end
'

current_file=""

start_tail() {
  local file="$1"
  [[ "$file" == "$current_file" ]] && return
  [[ -n "$tail_pid" ]] && kill "$tail_pid" 2>/dev/null
  
  echo ""
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "📂 $(basename "$file")"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  
  tail -f "$file" 2>/dev/null | jq -r --unbuffered "$jq_filter" &
  tail_pid=$!
  current_file="$file"
}

start_watcher() {
  (
    inotifywait -m -q -e create -e modify -e moved_to "$proj" --format '%f' 2>/dev/null | while read file; do
      if [[ "$file" == *.jsonl ]]; then
        echo "$proj/$file" > "/tmp/ralph-latest-file-$$"
      fi
    done
  ) &
  inotify_pid=$!
}

check_for_new_file() {
  if [[ -f "/tmp/ralph-latest-file-$$" ]]; then
    local new_file
    new_file=$(cat "/tmp/ralph-latest-file-$$")
    if [[ "$new_file" != "$current_file" && -f "$new_file" ]]; then
      start_tail "$new_file"
    fi
  fi
}

# --- Main ---

echo "🚀 Ralph Wiggum Loop"
echo "   Directory: $cwd"
echo "   Project:   $proj"
echo "   Max iterations: $max_iterations"
echo ""

# Check for PROMPT.md
if [[ ! -f "PROMPT.md" ]]; then
  echo "❌ No PROMPT.md found in current directory"
  exit 1
fi

# Start watching for file changes
rm -f "/tmp/ralph-latest-file-$$"
start_watcher

# Seed with latest existing file
latest=$(ls -1t "$proj"/*.jsonl 2>/dev/null | head -1 || true)
[[ -n "$latest" ]] && start_tail "$latest"

for ((i=1; i<=max_iterations; i++)); do
  echo ""
  echo "╔══════════════════════════════════════════════════════╗"
  echo "║  🔄 Iteration $i / $max_iterations"
  echo "╚══════════════════════════════════════════════════════╝"
  
  # Run claude in background so we can monitor file changes
  claude --dangerously-skip-permissions -p "$(cat PROMPT.md)" --output-format text > "/tmp/ralph-result-$$.txt" 2>&1 &
  claude_pid=$!
  
  # Poll while claude runs
  while kill -0 "$claude_pid" 2>/dev/null; do
    check_for_new_file
    sleep 1
  done
  
  # Get result
  wait "$claude_pid" || true
  result=$(cat "/tmp/ralph-result-$$.txt")
  
  # Check exit conditions
  if [[ "$result" == *"<promise>COMPLETE</promise>"* ]]; then
    echo ""
    echo "✅ All tasks complete after $i iterations!"
    exit 0
  elif [[ "$result" == *"<promise>BLOCKED:"* ]]; then
    blocked_reason=$(echo "$result" | grep -oP '(?<=<promise>BLOCKED:)[^<]+' || echo "unknown")
    echo ""
    echo "⚠️  Blocked: $blocked_reason"
    echo "Press Enter to continue or Ctrl+C to stop"
    read -r
  fi
  
  echo "--- End of iteration $i ---"
done

echo ""
echo "⚠️  Reached max iterations ($max_iterations)"
exit 1

PROMPT.md

# Context
- [PRD](link)
- [Development Guide](link) 
- [Task Plan](plan.md)
- [Activity Log](activity.md)

# Instructions

1. **Orient**: Read activity.md for recent context, then read plan.md to find the highest priority task where `passes: false`.

2. **Understand**: Before coding, read any existing files you'll modify. Understand the current state.

3. **Implement**: Complete exactly ONE task. If blocked or unclear, document why in activity.md and move to next task.

4. **Verify**: Run relevant tests or validation before marking complete. Don't mark passes:true unless it actually works.

5. **Document**: Append a dated entry to activity.md:
-```
   ## YYYY-MM-DD HH:MM - Task X.X: [title]
   - What changed
   - Files modified
   - Any issues or notes for next session
-```

6. **Commit**: One commit per task with format: `Task X.X: [description]`

# Rules
- ONE task per session
- Always use .venv for Python
- No git init/remote/push
- Target environment is Docker
- If stuck for 3+ attempts on same issue, mark task as blocked in plan.md and move on

# Exit conditions
- Task complete → end session normally
- All tasks `passes: true` → output `<promise>COMPLETE</promise>`
- Blocked on external dependency → output `<promise>BLOCKED: [reason]</promise>`

Conclusion

This is still a new approach, I’ve only had a few days with it but I’m liking it. For my current project it’s largely application based, but I think this combined with playwright mcp for headless browser control will make new web development projects more interesting too.

manipulate.org
up 750d _