Awesome-omni-skill factory-ralph-loop

Iterative task execution using the Ralph Loop pattern (named after Ralph Wiggum). Use when you need to repeatedly run an agent until a condition is met—fixing all lint errors, passing all tests, or exhausting PRD tasks. The filesystem serves as memory between iterations.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/ai-agents/factory-ralph-loop" ~/.claude/skills/diegosouzapw-awesome-omni-skill-factory-ralph-loop && rm -rf "$T"
manifest: skills/ai-agents/factory-ralph-loop/SKILL.md
source content

Ralph Loop Pattern

The Ralph Loop (named after Ralph Wiggum) is an agentic pattern where you run an AI agent in a continuous loop until a task is complete. Each iteration starts relatively fresh, with the filesystem serving as persistent memory.

Core Characteristics

  1. Same systemPrompt repeated — The agent receives consistent instructions each iteration
  2. Filesystem as memory — Code changes persist on disk between iterations
  3. Fresh context — Each iteration reduces context pollution vs. single long conversation
  4. Exit condition — Loop ends when tests pass, lint is clean, or work is exhausted
  5. Simple orchestrator — Just
    while (!done) { run agent }

Basic Structure

const maxIterations = 10;
let iteration = 0;
let done = false;

while (!done && iteration < maxIterations) {
  iteration++;
  factory.observe.log("info", `Iteration ${iteration}`, { maxIterations });

  const result = await factory.spawn({
    agent: "worker",
    systemPrompt: "You are fixing issues iteratively",
    prompt: "Fix the next issue",
    model: "anthropic/claude-sonnet-4-6",
    step: iteration,
  });

  // Check exit condition
  done = result.exitCode === 0 && result.text.includes("all clean");

  if (result.exitCode !== 0) {
    factory.observe.log("error", "Agent failed", { iteration, error: result.errorMessage });
    break;
  }
}

Pattern 1: Fix All Lint Errors

Repeatedly run an agent until lint is clean:

import { spawnSync } from "node:child_process";

const maxIterations = 20;
let iteration = 0;

while (iteration < maxIterations) {
  iteration++;

  const lintResult = spawnSync("npm", ["run", "lint"], {
    cwd: process.cwd(),
    encoding: "utf-8",
  });

  if (lintResult.status === 0) {
    factory.observe.log("info", "Lint clean!", { iterations: iteration });
    break;
  }

  factory.observe.log("info", `Iteration ${iteration}`, {
    exitCode: lintResult.status,
    errorCount: (lintResult.stdout.match(/error/gi) || []).length,
  });

  const result = await factory.spawn({
    agent: "linter",
    systemPrompt: `You fix lint errors iteratively.
Run 'npm run lint' to see current errors.
Fix one or more errors, focusing on the most common patterns.
Make minimal, focused changes.`,
    prompt: `Fix lint errors. Current output:\n\n${lintResult.stdout}\n${lintResult.stderr}`,
    model: "mistral/devstral-2512",
    step: iteration,
  });

  if (result.exitCode !== 0) {
    factory.observe.log("error", "Agent failed", { iteration });
    break;
  }
}

Pattern 2: With Progress Tracking

Accumulate state across iterations to show progress:

import { spawnSync } from "node:child_process";

interface ProgressState {
  fixedIssues: string[];
  lastErrorCount: number;
  stagnantIterations: number;
}

const maxIterations = 20;
let iteration = 0;

const progress: ProgressState = {
  fixedIssues: [],
  lastErrorCount: Infinity,
  stagnantIterations: 0,
};

while (iteration < maxIterations) {
  iteration++;

  const lintResult = spawnSync("npm", ["run", "lint"], {
    cwd: process.cwd(),
    encoding: "utf-8",
  });

  const errorCount = (lintResult.stdout.match(/error/gi) || []).length;

  if (lintResult.status === 0) {
    factory.observe.log("info", "All issues fixed!", {
      iterations: iteration,
      fixedIssues: progress.fixedIssues,
    });
    break;
  }

  // Track progress
  if (errorCount >= progress.lastErrorCount) {
    progress.stagnantIterations++;
  } else {
    progress.stagnantIterations = 0;
  }

  // Exit if stagnant
  if (progress.stagnantIterations >= 3) {
    factory.observe.log("warning", "No progress for 3 iterations", { errorCount });
    break;
  }

  factory.observe.log("info", `Iteration ${iteration}`, {
    errorCount,
    lastErrorCount: progress.lastErrorCount,
    fixed: progress.fixedIssues.length,
  });

  progress.lastErrorCount = errorCount;

  const result = await factory.spawn({
    agent: "fixer",
    systemPrompt: `You fix lint errors iteratively.
Track your progress and avoid repeating unsuccessful approaches.
Previous fixes: ${progress.fixedIssues.join(", ") || "none yet"}
Error count: ${errorCount} (was ${progress.lastErrorCount === Infinity ? "unknown" : progress.lastErrorCount})`,
    prompt: `Fix lint errors:\n\n${lintResult.stdout}\n${lintResult.stderr}`,
    model: "anthropic/claude-sonnet-4-6",
    step: iteration,
  });

  if (result.exitCode === 0) {
    const fixMatch = result.text.match(/fixed?:?\s*(.+)/i);
    if (fixMatch) {
      progress.fixedIssues.push(fixMatch[1]);
    }
  }
}

Pattern 3: Loop Until Tests Pass

Run agent repeatedly until test suite passes:

import { spawnSync } from "node:child_process";

const testCommand = "npm test";
const [cmd, ...args] = testCommand.split(" ");
const maxIterations = 10;
let iteration = 0;

while (iteration < maxIterations) {
  iteration++;

  const testResult = spawnSync(cmd, args, {
    cwd: process.cwd(),
    encoding: "utf-8",
    timeout: 60000,
  });

  if (testResult.status === 0) {
    factory.observe.log("info", "Tests passing!", { iterations: iteration });
    break;
  }

  factory.observe.log("info", `Iteration ${iteration}`, {
    exitCode: testResult.status,
    timeout: testResult.signal === "SIGTERM",
  });

  const failureOutput = [testResult.stdout, testResult.stderr]
    .filter(Boolean)
    .join("\n")
    .slice(-5000); // Last 5KB to avoid huge prompt payloads

  const result = await factory.spawn({
    agent: "test-fixer",
    systemPrompt: `You fix failing tests iteratively.
Analyze test output, identify the root cause, and make minimal fixes.
Run the tests again to verify your changes.
Focus on one failure at a time if there are multiple.`,
    prompt: `Fix failing tests. Output from '${testCommand}':\n\n${failureOutput}`,
    model: "anthropic/claude-opus-4-6",
    step: iteration,
  });

  if (result.exitCode !== 0) {
    factory.observe.log("error", "Agent failed", { iteration });
    break;
  }
}

Pattern 4: Exhaustive PRD Implementation

Work through Product Requirements Document tasks until all are complete:

import fs from "node:fs";

interface PRDTask {
  id: string;
  description: string;
  completed: boolean;
}

const prdPath = "./PRD.md";
const tasksPath = "./tasks.json";
const maxIterations = 50;

// Load or initialize tasks
let tasks: PRDTask[];
if (fs.existsSync(tasksPath)) {
  tasks = JSON.parse(fs.readFileSync(tasksPath, "utf-8"));
} else {
  const prdContent = fs.readFileSync(prdPath, "utf-8");
  tasks = parsePRD(prdContent);
  fs.writeFileSync(tasksPath, JSON.stringify(tasks, null, 2));
}

let iteration = 0;

while (iteration < maxIterations) {
  const nextTask = tasks.find(t => !t.completed);
  if (!nextTask) {
    factory.observe.log("info", "All tasks completed!", { iterations: iteration });
    break;
  }

  iteration++;
  factory.observe.log("info", `Iteration ${iteration}: ${nextTask.id}`, {
    remaining: tasks.filter(t => !t.completed).length,
  });

  const result = await factory.spawn({
    agent: "implementer",
    systemPrompt: `You implement PRD tasks iteratively.
Read the PRD at ${prdPath}.
Complete tasks one at a time.
Mark tasks complete by updating ${tasksPath}.`,
    prompt: `Implement: ${nextTask.id} - ${nextTask.description}\n\nCompleted so far:\n${
      tasks.filter(t => t.completed).map(t => `+ ${t.id}`).join("\n")
    }`,
    model: "openai-codex/gpt-5.3-codex",
    step: iteration,
  });

  if (result.exitCode !== 0) {
    factory.observe.log("error", "Agent failed", { iteration, task: nextTask.id });
    break;
  }

  // Reload tasks (agent may have updated them)
  if (fs.existsSync(tasksPath)) {
    tasks = JSON.parse(fs.readFileSync(tasksPath, "utf-8"));
  }
}

function parsePRD(content: string): PRDTask[] {
  const matches = content.matchAll(/^[-*]\s*\[\s*\]\s*(.+)$/gm);
  const tasks: PRDTask[] = [];
  let id = 1;

  for (const match of matches) {
    tasks.push({
      id: `TASK-${id++}`,
      description: match[1].trim(),
      completed: false,
    });
  }

  return tasks;
}

Pattern 5: Combined Safety Checks

Comprehensive safety and exit logic:

import { spawnSync } from "node:child_process";

const maxIterations = 20;
const maxStagnantIterations = 3;
const maxFailedIterations = 2;
const checkCommand = "npm run lint";

let iteration = 0;
let stagnantCount = 0;
let failedCount = 0;
let lastCheckOutput = "";

while (iteration < maxIterations) {
  iteration++;

  // Periodic check
  const [cmd, ...args] = checkCommand.split(" ");
  const checkResult = spawnSync(cmd, args, {
    cwd: process.cwd(),
    encoding: "utf-8",
  });

  if (checkResult.status === 0) {
    factory.observe.log("info", "Check passed!", { iterations: iteration });
    break;
  }

  // Track stagnation
  const currentOutput = checkResult.stdout + checkResult.stderr;
  if (currentOutput === lastCheckOutput) {
    stagnantCount++;
    factory.observe.log("warning", "No change detected", { stagnantCount });
  } else {
    stagnantCount = 0;
  }
  lastCheckOutput = currentOutput;

  if (stagnantCount >= maxStagnantIterations) {
    factory.observe.log("error", "Stagnant iterations exceeded", { stagnantCount });
    break;
  }

  factory.observe.log("info", `Iteration ${iteration}`, {
    stagnantCount,
    failedCount,
    max: maxIterations,
  });

  const result = await factory.spawn({
    agent: "worker",
    systemPrompt: "You are fixing issues iteratively",
    prompt: "Continue fixing issues",
    model: "anthropic/claude-sonnet-4-6",
    step: iteration,
  });

  if (result.exitCode !== 0) {
    failedCount++;
    factory.observe.log("error", "Agent failed", { iteration, failedCount });

    if (failedCount >= maxFailedIterations) {
      factory.observe.log("error", "Failed iterations exceeded", { failedCount });
      break;
    }
  } else {
    failedCount = 0;
  }
}

Best Practices

1. Set max iterations

Always have an upper bound to prevent infinite loops:

const maxIterations = 20; // Sensible default

2. Detect stagnation

Track if the agent is making progress:

if (currentState === lastState) {
  stagnantCount++;
  if (stagnantCount >= 3) break;
}

3. Use bash exit conditions

Shell out to authoritative checks (tests, lint, build):

const result = spawnSync("npm", ["test"], { encoding: "utf-8" });
if (result.status === 0) break;

4. Provide context to agent

Include iteration number, progress, previous attempts:

prompt: `Iteration ${iteration}/${maxIterations}
Fixed so far: ${fixed.join(", ")}
Current errors: ${errorCount}
...`

5. Log everything

Observability is critical for debugging loops:

factory.observe.log("info", "Loop state", {
  iteration,
  errorCount,
  stagnantCount,
  lastChange
});

6. Limit context size

Truncate large outputs to avoid prompt bloat:

const recentOutput = fullOutput.slice(-5000); // Last 5KB

7. Allow early exit

If the goal is achieved, return immediately:

if (testsPassing) break;

When to Use Ralph Loop

Good for:

  • Fixing lint/type errors iteratively
  • Making tests pass one by one
  • Implementing PRD tasks sequentially
  • Refactoring with incremental validation
  • Code generation with iterative refinement

Not ideal for:

  • Tasks requiring deep context across iterations
  • Complex multi-step reasoning within a single problem
  • When the agent needs to remember detailed discussions
  • Parallel work (use
    Promise.all
    with
    factory.spawn
    instead)

Advanced: Nested Loops

You can nest Ralph Loops for hierarchical work:

const modules = ["src/auth", "src/api", "src/db"];

for (const module of modules) {
  factory.observe.log("info", `Processing module: ${module}`);

  let iteration = 0;
  while (iteration < 10) {
    iteration++;

    const result = await factory.spawn({
      agent: "module-fixer",
      systemPrompt: `Fix issues in ${module}`,
      prompt: "Run checks and fix issues",
      model: "mistral/devstral-2512",
      step: iteration,
    });

    const check = spawnSync("npm", ["run", "lint", module], {
      cwd: process.cwd(),
      encoding: "utf-8",
    });

    if (check.status === 0) break;
  }
}

Summary

The Ralph Loop is a simple but powerful pattern:

  • While loop around
    await factory.spawn()
  • Filesystem persistence between iterations
  • Bash exit conditions for authoritative checks
  • Progress tracking to detect stagnation
  • Max iterations for safety

It works because the agent sees fresh context each iteration, making progress incrementally while the filesystem accumulates changes. Perfect for iterative tasks where "run it again" is a valid strategy.

factory-ralph-loop — OpenSkillIndex