
Ralph Loop
- 1.2k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
ralph-loop is an agent skill that maintains ordered, dependency-aware task lists and spec state for long-running coding-agent projects so developers avoid losing context across sessions.
About
ralph-loop is a developer-kit skill that structures agent work into specs, task files, and tracked statuses with dependency ordering. It stores state in JSON alongside markdown specs under docs/specs/, tracking currentTaskIndex, per-task implement/review/sync steps, and completion status like TASK-001 and TASK-002. Developers reach for ralph-loop when multi-step agent projects risk drift, skipped dependencies, or forgotten review gates. The skill emphasizes persistent task graphs and review states rather than one-shot code generation.
- Maintains a living spec directory with currentTaskIndex and state tracking
- Enforces sequential task execution with explicit dependencies
- Tracks implement/review/sync completion states per task
- Generates ready-to-use markdown task files from a master spec
- Hard-gate: only proceeds to next task after current task reaches review-complete
Ralph Loop by the numbers
- 1,214 all-time installs (skills.sh)
- +58 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #408 of 3,301 Productivity & Planning skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill ralph-loopAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do you keep long agent coding projects on track?
Maintain ordered, dependency-aware task lists that keep long-running agent projects on track without losing context.
Who is it for?
Developers running multi-session agent builds that need ordered tasks, reviews, and persistent project state.
Skip if: Single-file edits or quick one-prompt fixes that do not need structured task orchestration.
When should I use this skill?
User starts a multi-step agent project, needs dependency-aware task lists, or wants review/sync gates across sessions.
What you get
Spec directory, tasks markdown, JSON state file, and dependency-ordered TASK files with status tracking
- task graph
- spec files
- state JSON
Files
⚠️ WARNING: This skill was deprecated in favor of a new command ralph-loop-v2 that uses a Python orchestrator script.The old /specs:ralph-loop command will be removed soon. Please migrate to the new command.Ralph Loop — Python Orchestrator
⚠️ IMPORTANT: This skill uses a Python orchestrator script. Do NOT execute arbitrary bash commands. Use Bash ONLY to run ralph_loop.py. All task commands (like /developer-kit-specs:specs.task-implementation) are shown to the user to execute manually.
Overview
The Ralph Loop applies Geoffrey Huntley's "Ralph Wiggum as a Software Engineer" technique to specification-driven development. It uses a Python orchestrator script that manages a state machine: one invocation = one step, state persisted in fix_plan.json.
Key insight: Implementing + reviewing + syncing in one invocation explodes the context window. Solution: each loop iteration does exactly one step, saves state to fix_plan.json, and stops. The next iteration resumes from saved state.
Key improvement: The Python script ralph_loop.py handles all state management, task selection, and command generation. It does NOT execute task commands directly — it shows you the correct command to execute in your CLI.
When to Use
- User runs
/loopcommand for recurring automation - User asks to "automate implementation" or "run tasks in loop"
- User wants to "iterate through tasks step-by-step" or "run workflow automation"
- User needs "context window management" across multiple SDD commands
- User wants to "process task range" from TASK-N to TASK-M
- User needs multi-agent support (different CLIs for different tasks)
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ralph_loop.py │────▶│ fix_plan.json │────▶│ User executes │
│ (orchestrator)│ │ (state file) │ │ command in CLI │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
└──────────────────────────────────────│ Task result │
│ (success/ │
│ failure) │
└─────────────────┘One Step Flow: 1. Run ralph_loop.py --action=loop 2. Script reads fix_plan.json and determines current step 3. Script shows the command to execute (e.g., /developer-kit-specs:specs.task-implementation) 4. User executes the command in their CLI 5. User runs ralph_loop.py --action=loop again 6. Script updates state based on result and shows next command
State Machine
fix_plan.json state machine:
┌─────────────────────────────────────────────────────────────┐
│ state: "init" │
│ → --action=start: Initialize fix_plan.json │
│ → Load tasks from tasks/TASK-*.md files │
│ → Apply task_range filter │
│ │
│ state: "choose_task" │
│ → Pick next pending task (within range, deps satisfied)│
│ → No tasks in range → state: "complete" │
│ → Task found → state: "implementation" │
│ │
│ state: "implementation" │
│ → Show /developer-kit-specs:specs.task-implementation command │
│ → User executes, then runs loop again │
│ → Next state: "review" │
│ │
│ state: "review" ││ → Show /developer-kit-specs:specs.task-implementation --action=cleanup command│},{find: │
│ → User reviews results, then runs loop again │
│ → Issues found → state: "fix" (retry ≤ 3) │
│ → Clean → state: "cleanup" │
│ │
│ state: "fix" │
│ → Show commands to fix issues │
│ → User applies fixes, then runs loop again │
│ → Next state: "review" │
│ │
│ state: "cleanup" │
│ → Show /developer-kit-specs:specs.task-implementation --action=cleanup command│
│ → Next state: "sync" │
│ │
│ state: "sync" │
│ → Show /developer-kit-specs:specs.sync command │
│ → Next state: "update_done" │
│ │
│ state: "update_done" │
│ → Mark task done, commit git changes │
│ → Re-evaluate dependencies │
│ → state: "choose_task" │
│ │
│ state: "complete" | "failed" │
│ → Print result, stop │
└─────────────────────────────────────────────────────────────┘File Location Requirements
⚠️ CRITICAL: The fix_plan.json file MUST ALWAYS be located in:
docs/specs/[ID-feature]/_ralph_loop/fix_plan.jsonThis is enforced by the script to prevent LLMs from creating files in wrong locations.
Migration: If you have an old fix_plan.json in the root of your spec folder, the script will automatically migrate it to _ralph_loop/ on first run.
Instructions
Phase 1: Initialize
Run the Python script with --action=start to scan task files and create fix_plan.json in the correct location:
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=start \
--spec=docs/specs/001-feature/ \
--from-task=TASK-036 \
--to-task=TASK-041Phase 2: Execute Loop Steps
Run the script with --action=loop to get the current state and the command to execute:
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=loop \
--spec=docs/specs/001-feature/The script will show you the exact command to execute for the current step. Execute it in your CLI, then run the loop command again.
Phase 3: Advance State (Manual)
After executing the shown command, manually advance to the next step:
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=next \
--spec=docs/specs/001-feature/This updates fix_plan.json to the next state (e.g., implementation → review).
Phase 4: Monitor Progress
Check status anytime with --action=status:
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=status \
--spec=docs/specs/001-feature/Quick Start
1. Initialize
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=start \
--spec=docs/specs/001-feature/ \
--from-task=TASK-036 \
--to-task=TASK-041 \
--agent=claude2. Run Loop
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=loop \
--spec=docs/specs/001-feature/The script will show you the command to execute. Run it, then run the loop again.
3. Check Status
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=status \
--spec=docs/specs/001-feature/Arguments
| Argument | Description |
|---|---|
--action | start (init), loop (run one step), status, resume, next (advance step) |
--spec | Spec folder path (e.g. docs/specs/001-feature/) |
--from-task | Start of task range (e.g. TASK-036) |
--to-task | End of task range (e.g. TASK-041) |
--agent | Default agent: claude, codex, copilot, kimi, gemini, glm4, minimax |
--no-commit | Skip git commits (for testing) |
Step Details
Step 1: Initialize (--action=start)
The script: 1. Scans tasks/TASK-*.md files in the spec folder 2. Extracts metadata from YAML frontmatter (id, title, status, lang, dependencies, agent) 3. Applies --from-task and --to-task filters 4. Creates fix_plan.json with full state
Step 2: Choose Task (choose_task)
The script: 1. Finds pending tasks within range 2. Checks dependencies are satisfied 3. Selects next task 4. Updates fix_plan.json with current_task 5. Shows command to execute
Step 3: Implementation (implementation)
The script shows:
→ Implementation: TASK-037
Execute:
/developer-kit-specs:specs.task-implementation --task=TASK-037
After execution, update state:
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/Step 4: Review (review)
The script shows:
→ Review: TASK-037 | Retry: 0/3
Execute:
/developer-kit-specs:specs.task-review --task=TASK-037
Review the generated review report, then update state:
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/Step 5: Fix (fix) - If Review Failed
If issues found, script shows fix instructions. After fixes, user runs loop again.
Step 6: Cleanup (cleanup)
The script shows:
→ Cleanup: TASK-037
Execute:
/developer-kit-specs:specs.task-implementation --task=TASK-037 --action=cleanupStep 7: Sync (sync)
The script shows:
→ Sync: TASK-037
Execute:
/developer-kit-specs:specs.sync docs/specs/001-feature/ --after-task=TASK-037Step 8: Update Done (update_done)
The script: 1. Marks task as completed in fix_plan.json 2. Commits git changes (unless --no-commit) 3. Updates iteration count 4. Returns to choose_task
Multi-Agent Support
Default Agent for All Tasks
python3 ralph_loop.py --action=start --spec=... --agent=codexPer-Task Agent
Specify agent in task file YAML frontmatter:
---
id: TASK-036
title: Refactor user service
status: pending
lang: java
agent: codex
---Supported agents: claude, codex, copilot, kimi, gemini, glm4, minimax
Using with /loop (Claude Code)
For automatic scheduling every 5 minutes:
/loop 5m python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=loop \
--spec=docs/specs/001-feature/This will repeatedly run the loop, showing you the next command each time.
Note: The Ralph Loop is now managed directly through the Python script. The deprecated /developer-kit-specs:specs.ralph-loop command has been removed.
Task File Format
Each task should be a separate file: tasks/TASK-XXX.md
---
id: TASK-036
title: Implement user authentication
status: pending
lang: java
dependencies: []
complexity: medium
agent: claude
---
## Description
Implement JWT-based authentication for the API.
## Acceptance Criteria
- [ ] Login endpoint returns JWT token
- [ ] Token validation middleware
- [ ] Refresh token mechanismExamples
Example 1: Basic Usage
# Initialize
python3 ralph_loop.py --action=start \
--spec=docs/specs/001-feature/ \
--from-task=TASK-001 \
--to-task=TASK-005
# Loop until complete
while true; do
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/
# Execute the shown command manually
# Then continue loop
doneExample 2: With Claude Code /loop
# Start with specific range
/loop 5m python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=loop \
--spec=docs/specs/002-tdd-command \
--from-task=TASK-001 \
--to-task=TASK-010Example 3: Multi-Agent Setup
# Initialize with Claude as default
python3 ralph_loop.py --action=start \
--spec=docs/specs/001-feature/ \
--agent=claude
# Some tasks have "agent: codex" in their frontmatter
# Those will show Codex-formatted commandsBest Practices
- One step per invocation: Execute exactly one step, save state, stop
- Trust the state: Read from
fix_plan.json, write tofix_plan.json - No context accumulation: State lives in the file, not in context
- Manual command execution: The script shows commands; you execute them in your CLI
- Retry on review failure: Max 3 retries before failing
- Range filtering: Always filter by
task_range - Dependencies first: Only pick tasks where all dependencies are done
- Git commits: The script auto-commits after each completed task
Constraints and Warnings
- Context explosion: Do NOT implement + review + sync in one invocation — context will overflow
- Max retries: Review failures retry up to 3 times, then fail
- Git state: Ensure clean git state before starting
- Test infrastructure: Loop requires tests to pass — without tests, backpressure is ineffective
- Strict state validation: Valid
state.stepvalues are ONLY:init,choose_task,implementation,review,fix,cleanup,sync,update_done,complete,failed - NO automatic command execution: The script shows commands but does NOT execute them — you must run them in your CLI
Troubleshooting
"fix_plan.json not found"
Run --action=start first:
python3 ralph_loop.py --action=start --spec=docs/specs/001-feature/The script will create fix_plan.json in the correct location:
docs/specs/001-feature/_ralph_loop/fix_plan.json"fix_plan.json in wrong location"
If you see a warning about the file being in the wrong location, the script will guide you through migration:
# Manual migration if needed
mkdir -p docs/specs/001-feature/_ralph_loop
mv docs/specs/001-feature/fix_plan.json docs/specs/001-feature/_ralph_loop/fix_plan.jsonThe script will automatically migrate old files on first run.
"Invalid spec folder"
Run --action=start first:
python3 ralph_loop.py --action=start --spec=docs/specs/001-feature/Task files not found
Ensure tasks are in tasks/TASK-XXX.md format with YAML frontmatter.
Wrong agent commands
Check --agent parameter or task agent: frontmatter field.
References
references/state-machine.md- Complete state machine documentationreferences/multi-cli-integration.md- Multi-CLI setup guidereferences/loop-prompt-template.md- Prompt template for shell loops
{
"spec": "docs/specs/001-example-new-spec/",
"specFile": "docs/specs/001-example-new-spec/2026-04-02--try-new-ralph-loop-skill.md",
"tasksFile": "docs/specs/001-example-new-spec/2026-04-02--try-new-ralph-loop-skill--tasks.md",
"initialized": "2026-04-02",
"currentTaskIndex": 1,
"state": "review",
"tasks": [
{
"id": "TASK-001",
"title": "Try New Ralph Loop Skill Task 001",
"file": "docs/specs/001-example-new-spec/tasks/TASK-001.md",
"dependencies": [],
"status": "completed",
"steps": {
"implement": "done",
"review": "done",
"sync": "done"
}
},
{
"id": "TASK-002",
"title": "Try New Ralph Loop Skill Task 002",
"file": "docs/specs/001-example-new-spec/tasks/TASK-002.md",
"dependencies": [
"TASK-001"
],
"status": "in_review",
"steps": {
"implement": "done",
"review": "pending",
"sync": "pending"
}
},
{
"id": "TASK-003",
"title": "Try New Ralph Loop Skill Task 003",
"file": "docs/specs/001-example-new-spec/tasks/TASK-003.md",
"dependencies": [
"TASK-002"
],
"status": "pending",
"steps": {
"implement": "pending",
"review": "pending",
"sync": "pending"
}
},
{
"id": "TASK-004",
"title": "Try New Ralph Loop Skill Task 004",
"file": "docs/specs/001-example-new-spec/tasks/TASK-004.md",
"dependencies": [
"TASK-003"
],
"status": "pending",
"steps": {
"implement": "pending",
"review": "pending",
"sync": "pending"
}
},
{
"id": "TASK-005",
"title": "Try New Ralph Loop Skill Task 005",
"file": "docs/specs/001-example-new-spec/tasks/TASK-005.md",
"dependencies": [],
"status": "pending",
"steps": {
"implement": "pending",
"review": "pending",
"sync": "pending"
}
}
],
"lastUpdated": "2026-04-02T14:08:44Z"
}
Ralph Loop Prompt Template — One Step Per Invocation
This template generates prompt.md for the shell loop approach. One invocation = one step. The LLM reads state, executes one step, updates state, and stops.
Generated by /developer-kit-specs:specs.ralph-loop --action=start.
Template
Ralph Loop State Machine — One Step Per Invocation
================================================
Read fix_plan.json → Execute current step → Update state → Stop
DO NOT execute multiple steps. One step only.
Current State:
- Step: CURRENT_STEP
- Task: CURRENT_TASK (or "none")
- Range: FROM_TASK → TO_TASK
- Iteration: ITERATION_NUM
- Retry count: RETRY_COUNT (for review failures)
Execute Step: CURRENT_STEP
==========================
[Step-specific instructions below]---
Step: init
1. Read all task files from SPEC_FOLDER/tasks/TASK-*.md
2. Extract from YAML frontmatter: id, title, status, lang, dependencies, complexity
3. Parse --from-task and --to-task to set task_range
4. Filter tasks by range: exclude tasks with number < from_num or > to_num
5. Set state.step = "choose_task"
6. Save fix_plan.json
7. Print: "Initialized | Range: TASK-036→TASK-041 | Tasks in range: N"---
Step: choose_task
1. Read fix_plan.json
2. Filter pending tasks to only those within task_range
3. Filter to tasks where all dependencies are in done array
4. Sort by priority (lower complexity_score = higher priority)
5. If no tasks remain:
- Set state.step = "complete"
- Save and print completion summary
6. Else:
- Pick first task (most important)
- Set state.current_task = TASK-ID
- Set state.current_task_file = task file path
- Set state.current_task_lang = lang
- Set state.step = "implementation"
- Set state.retry_count = 0
7. Save fix_plan.json
8. Print: "Chosen: TASK-ID [title] | Next: implementation"---
Step: implementation
1. Read fix_plan.json to get current_task and current_task_file
2. Read the task file CURRENT_TASK_FILE
3. Run task-implementation:
/developer-kit-specs:specs.task-implementation --lang=LANG --task="TASK_FILE"
4. If implementation succeeds:
- Set state.step = "review"
5. If implementation fails:
- Set state.step = "failed"
- Set state.error = "implementation failed"
6. Save fix_plan.json
7. Print: "Implementation: TASK-ID | Success → review | Failure → failed"---
Step: review
1. Read fix_plan.json to get current_task and current_task_file
2. Run task-review with --no-confirm:
/developer-kit-specs:specs.task-review --no-confirm --lang=LANG "TASK_FILE"
3. Read the review report TASK-FILE--review.md
4. If review passes (all criteria met, no issues):
- Set state.step = "cleanup"
5. If review fails (issues found):
- Increment state.retry_count
- If retry_count >= 3:
- Set state.step = "failed"
- Set state.error = "review failed after 3 retries"
- Else:
- Set state.step = "fix"
6. Save fix_plan.json
7. Print: "Review: TASK-ID | Clean → cleanup | Issues → fix (retry N/3)"---
Step: cleanup
1. Read fix_plan.json to get current_task and current_task_file
2. Run task-implementation with --action=cleanup:
/developer-kit-specs:specs.task-implementation --action=cleanup --lang=LANG --task="TASK_FILE"
3. Set state.step = "sync"
4. Save fix_plan.json
5. Print: "Cleanup: TASK-ID complete | Next: sync"---
Step: fix
1. Read fix_plan.json to get current_task
2. Read the review report: docs/specs/[id]/tasks/TASK-XXX--review.md
3. Fix the reported issues:
- Run /developer-kit-specs:specs.task-implementation --lang=LANG --task="TASK_FILE"
- Or manually apply edits to address findings
4. If fixes succeed:
- Set state.step = "review"
5. If fixes fail:
- Set state.step = "failed"
- Set state.error = "fix failed"
6. Save fix_plan.json
7. Print: "Fix: TASK-ID applied | Next: review"---
Step: sync
1. Read fix_plan.json to get current_task and spec_folder
2. Run specs.sync:
/developer-kit-specs:specs.sync SPEC_FOLDER/ --after-task=TASK-ID
3. Set state.step = "update_done"
4. Save fix_plan.json
5. Print: "Sync: TASK-ID complete | Next: update_done"---
Step: update_done
1. Read fix_plan.json to get current_task
2. Update the task's YAML frontmatter:
- status: completed
- completed_date: YYYY-MM-DD
3. In fix_plan.json:
- Move TASK-ID from pending[] to done[]
- Increment state.iteration
- Increment state.range_progress.done_in_range
- Update state.last_updated
- For each remaining pending task: check if all dependencies are in done[], if yes set dependencies_satisfied = true
4. Set state.step = "choose_task"
5. Save fix_plan.json
6. Print: "Done: TASK-ID | Progress: N/M in range (P%) | Next: choose_task"---
Step: complete
Ralph Loop COMPLETE
=====================
Task Range: FROM_TASK → TO_TASK
Tasks Completed: N/N
Total Iterations: ITERATION_NUM
Deviations Detected: N
All tasks in range implemented and verified.
Run --action=start with a new range to continue.---
Step: failed
Ralph Loop FAILED
==================
Task: CURRENT_TASK
Error: ERROR_MESSAGE
Retry count: RETRY_COUNT/3
Fix the issues manually, then resume:
/developer-kit-specs:specs.ralph-loop --action=loop --spec=SPEC_FOLDER/---
Ralph's Rules (Enforced)
1. ONE STEP PER INVOCATION: Read state, execute ONE step, update state, STOP 2. No context accumulation: Don't keep history in context — read from fix_plan.json 3. Trust the state machine: Follow the step order exactly 4. Retry on review failure: Max 3 retries before failing 5. Range filtering: Always filter by task_range 6. Strict state validation: Valid states are ONLY: init, choose_task, implementation, review, fix, cleanup, sync, update_done, complete, failed. If state.step is anything else, STOP and report a format error. 7. NO human confirmation: After any step, update fix_plan.json and STOP. Do NOT ask the user for confirmation. Use --no-confirm on sub-commands to prevent interactive prompts.
Ralph's Golden Rule
Only one thing per loop. One thing per loop.
Multi-CLI Ralph Loop Integration
This document explains how to run the Ralph Loop across all supported CLI environments using the Python orchestrator script.
Architecture
The Ralph Loop uses a Python script (ralph_loop.py) as the central orchestrator:
1. Script manages state: Reads/writes fix_plan.json 2. Script generates commands: Shows the correct command for your CLI 3. You execute commands: Run the shown command in your CLI 4. Script updates state: Run the script again to advance
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ralph_loop.py │────▶│ fix_plan.json │────▶│ Your CLI │
│ (orchestrator) │ │ (state file) │ │ (Claude/Codex/ │
│ │ │ │ │ Copilot/Kimi) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
▲ │
│ ▼
└──────────────────────────────────────────────┘
(you run script again)Supported CLIs
| CLI | Agent Code | Command Prefix | Example |
|---|---|---|---|
| Claude Code | claude | / | /developer-kit-specs:specs.task-implementation |
| Codex CLI | codex | (none) | task-implementation |
| Copilot CLI | copilot | (none) | task-implementation |
| Kimi CLI | kimi | / | /developer-kit-specs:specs.task-implementation |
Basic Usage (All CLIs)
1. Initialize
# Use python3 on macOS, python on Windows/Linux
python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=start \
--spec=docs/specs/001-feature/ \
--from-task=TASK-036 \
--to-task=TASK-041 \
--agent=claude2. Run Loop
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/Output shows the command to execute:
🔄 Ralph Loop | Iteration 0 | Step: implementation
→ Implementation: TASK-036
Execute:
/developer-kit-specs:specs.task-implementation --task=TASK-036
After execution, update state:
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/3. Execute Shown Command
In your CLI, run the shown command:
/developer-kit-specs:specs.task-implementation --task=TASK-0364. Continue Loop
Run the loop command again:
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/CLI-Specific Examples
Claude Code
# Initialize
python3 ralph_loop.py --action=start \
--spec=docs/specs/001-feature/ \
--from-task=TASK-036 --to-task=TASK-041 \
--agent=claude
# With /loop for automation
/loop 5m python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/Commands will be formatted as: /developer-kit-specs:specs.task-implementation --task=TASK-036
Codex CLI
# Initialize
python3 ralph_loop.py --action=start \
--spec=docs/specs/001-feature/ \
--from-task=TASK-036 --to-task=TASK-041 \
--agent=codex
# Loop
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/Commands will be formatted as: task-implementation --task=TASK-036
Copilot CLI
# Initialize
python3 ralph_loop.py --action=start \
--spec=docs/specs/001-feature/ \
--agent=copilot
# Loop
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/Kimi CLI
# Initialize
python3 ralph_loop.py --action=start \
--spec=docs/specs/001-feature/ \
--agent=kimi
# Loop
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/Commands will be formatted as: /developer-kit-specs:specs.task-implementation --task=TASK-036
Multi-Agent Support
Default Agent
Set default agent for all tasks:
python3 ralph_loop.py --action=start --spec=... --agent=codexPer-Task Agent
Override per task in task file frontmatter:
---
id: TASK-036
title: Refactor service
agent: codex
---Automation Scripts
Bash Loop
#!/bin/bash
# ralph-loop.sh - Run until complete
SPEC="docs/specs/001-feature"
PYTHON="python3" # Use python3 on macOS
while true; do
# Run one step
OUTPUT=$($PYTHON ralph_loop.py --action=loop --spec="$SPEC" 2>&1)
echo "$OUTPUT"
# Check if complete or failed
if echo "$OUTPUT" | grep -q "COMPLETE"; then
echo "✅ All done!"
break
fi
if echo "$OUTPUT" | grep -q "FAILED"; then
echo "❌ Loop failed!"
exit 1
fi
# The script shows the command to execute
# In automation, you would need to execute it programmatically
# For manual use, execute the shown command then continue
echo "Execute the shown command, then press Enter to continue..."
read
doneWith Claude Code /loop
# This will run the loop every 5 minutes
/loop 5m python3 plugins/developer-kit-specs/skills/ralph-loop/scripts/ralph_loop.py \
--action=loop \
--spec=docs/specs/001-feature/Note: With /loop, you'll see the command to execute each iteration. Execute it manually, then the next /loop will continue.
State File
All state lives in fix_plan.json:
{
"spec_id": "001-feature",
"spec_folder": "docs/specs/001-feature/",
"task_range": {
"from": "TASK-036",
"to": "TASK-041",
"total_in_range": 6
},
"default_agent": "claude",
"tasks": [...],
"pending": ["TASK-036", "TASK-037"],
"done": [],
"state": {
"step": "implementation",
"current_task": "TASK-036",
"current_task_file": "docs/specs/001-feature/tasks/TASK-036.md",
"current_task_lang": "java",
"iteration": 0,
"retry_count": 0,
"last_updated": "2026-04-05T10:30:00",
"range_progress": {
"done_in_range": 0,
"total_in_range": 6
}
}
}Legacy Shell Loop (Deprecated)
For non-Claude CLIs without the Python script, you can use a shell loop with prompt.md:
cd docs/specs/001-feature/_ralph_loop/
# Claude Code
while :; do cat prompt.md | claude; done
# Copilot CLI
while :; do cat prompt.md | copilot; done
# Codex CLI
while :; do cat prompt.md | codex; doneNote: The Python orchestrator is the recommended approach for all CLIs.
Command Reference
Script Commands
| Action | Description |
|---|---|
--action=start | Initialize fix_plan.json from task files |
--action=loop | Execute one step, show command to run |
--action=status | Show current state and progress |
--action=resume | Resume from current state |
Generated Commands by Agent
| Step | Claude/Kimi | Codex/Copilot |
|---|---|---|
| Implementation | /developer-kit-specs:specs.task-implementation --task=TASK-036 | task-implementation --task=TASK-036 |
| Review | /developer-kit-specs:specs.task-review --task=TASK-036 | task-review --task=TASK-036 |
| Cleanup | /developer-kit-specs:specs.task-implementation --task=TASK-036 --action=cleanup | specs.task-implementation --task=TASK-036 --action=cleanup |
| Sync | /developer-kit-specs:specs.sync --after-task=TASK-036 | specs.sync --after-task=TASK-036 |
Best Practices
1. Use Python script for state management: Don't manually edit fix_plan.json 2. Execute shown commands: The script generates the correct command for your CLI 3. One step at a time: Don't try to combine steps — context will explode 4. Check status regularly: Use --action=status to see progress 5. Git commits: The script auto-commits after each completed task
Troubleshooting
"fix_plan.json not found"
Run --action=start first.
Wrong command format
Check --agent matches your CLI (claude, codex, copilot, or kimi).
Task not found
Ensure task files are in tasks/TASK-XXX.md format with YAML frontmatter.
Migration from Shell Loop
If you were using the shell loop approach:
1. Before: while :; do cat prompt.md | claude; done 2. After:
python3 ralph_loop.py --action=start --spec=...
python3 ralph_loop.py --action=loop --spec=...
# Execute shown command
python3 ralph_loop.py --action=loop --spec=...The Python script provides:
- Better state management
- Multi-agent support
- Command formatting per CLI
- Progress tracking
- Git integration
Ralph Loop State Machine — CLI-Agnostic
This document describes the Ralph Loop state machine so it can be implemented by any CLI (Claude Code, Copilot CLI, Codex CLI, Gemini CLI, OpenCode CLI).
Core Principle
One /loop invocation = one step. The state machine is implemented as a state file (fix_plan.json). Each invocation reads the current state, executes one step, updates the state, and stops.
Automation Rules
- NO human confirmation: After completing a step, update
fix_plan.jsonand stop immediately. Do NOT prompt the user with "Do you want to proceed?" or present confirmation options. - Use `--no-confirm` on sub-commands: When invoking
task-review(or any other interactive command) inside the loop, always pass--no-confirmso it does not block waiting for user input. - Trust the file: The next loop invocation will read
fix_plan.jsonand continue automatically.
State File
All state lives in docs/specs/[id]/_ralph_loop/fix_plan.json.
Required Fields
{
"spec_id": "001-feature",
"spec_folder": "docs/specs/001-feature/",
"task_range": {
"from": "TASK-036",
"to": "TASK-041",
"from_num": 36,
"to_num": 41,
"total_in_range": 6
},
"state": {
"step": "implementation",
"current_task": "TASK-036",
"current_task_file": "docs/specs/001-feature/tasks/TASK-036.md",
"current_task_lang": "spring",
"iteration": 1,
"retry_count": 0,
"last_updated": "2026-03-25T10:30:00",
"error": null,
"range_progress": {
"done_in_range": 0,
"total_in_range": 6
}
},
"tasks": [...],
"done": [],
"pending": [],
"optional": [],
"superseded": [],
"learnings": []
}State Machine
init ──────────────────────────► choose_task ────────────────────► complete
│ ▲
▼ │
implementation │
│ │
▼ │
review ─────── issues ───► fix ◄────────────┤
│ │ │
clean ▼ ▼ │
cleanup ─────────────────────────────────────┤
│ │
▼ │
sync ───────────────────────────────────────┤
│ │
▼ │
update_done ─────────────────────────────────────┘
│
▼
failed (stop)
↑
└─ retry > 3Step Handlers
init
When: --action=start or fix_plan.json doesn't exist.
Actions: 1. Read all task files from docs/specs/[id]/tasks/TASK-*.md 2. Extract from YAML frontmatter: id, title, status, lang, dependencies, complexity 3. Parse --from-task and --to-task to set task_range 4. Filter tasks by range: exclude tasks with number < from_num or > to_num 5. Initialize state.step = "choose_task" 6. Save fix_plan.json
Output:
Ralph Loop | Step: init | Range: TASK-036→TASK-041
→ Found 6 tasks in range
→ Initialized fix_plan.json
→ Next: choose_taskchoose_task
When: state.step = "choose_task"
Actions: 1. Filter pending tasks to only those within task_range 2. Filter to tasks where all dependencies are in done array 3. Sort by priority (lower complexity_score = higher priority) 4. If no tasks remain:
- Set
state.step = "complete" - Save and stop
5. Pick first task 6. Set state.current_task, state.current_task_file, state.current_task_lang 7. Set state.step = "implementation" 8. Set state.retry_count = 0 9. Save and stop
Output:
Ralph Loop | Step: choose_task | Iteration: 3
→ Selected: TASK-037 "Implement caching layer"
→ Dependencies: satisfied
→ Next: implementationimplementation
When: state.step = "implementation"
Actions: 1. Run task-implementation:
/developer-kit-specs:specs.task-implementation --lang=LANG --task="TASK-FILE"Or for non-Claude CLIs, read the task file and implement manually. 2. If implementation succeeds:
- Set
state.step = "review"
3. If implementation fails:
- Set
state.step = "failed" - Set
state.error = "implementation failed"
4. Save and stop
Output:
Ralph Loop | Step: implementation | Task: TASK-037
→ Running /developer-kit-specs:specs.task-implementation --lang=spring --task="docs/specs/001-feature/tasks/TASK-037.md"
→ Success → Next: reviewreview
When: state.step = "review"
Actions: 1. Run task-review with --no-confirm to prevent interactive blocking:
/developer-kit-specs:specs.task-review --no-confirm --lang=LANG "TASK-FILE"Or for non-Claude CLIs, verify implementation manually:
- Read acceptance criteria from task file
- Check each criterion is met
- Run tests
- Run code review
2. Read the generated review report (TASK-FILE--review.md) to determine pass/fail 3. If review passes (all criteria met, no issues):
- Set
state.step = "cleanup"
4. If review fails (issues found):
- Increment
state.retry_count - If
retry_count >= 3: - Set
state.step = "failed" - Set
state.error = "review failed after 3 retries" - Else:
- Set
state.step = "fix"
5. Save and stop. Do NOT ask the user for confirmation.
Output:
Ralph Loop | Step: review | Task: TASK-037 | Retry: 1/3
→ Running /developer-kit-specs:specs.task-review --no-confirm --lang=spring "docs/specs/001-feature/tasks/TASK-037.md"
→ Reading review report
→ Clean → Next: cleanupfix
When: state.step = "fix"
Actions: 1. Read the review report for the current task:
docs/specs/[id]/tasks/TASK-XXX--review.md2. Fix the issues reported in the review report:
- For Claude Code: run
/developer-kit-specs:specs.task-implementation --lang=LANG --task="TASK-FILE"(the task implementation command should read the review report and apply fixes) - For non-Claude CLIs: manually edit files to address each finding
3. If fixes succeed:
- Set
state.step = "review"
4. If fixes fail:
- Set
state.step = "failed" - Set
state.error = "fix failed"
5. Save and stop
Output:
Ralph Loop | Step: fix | Task: TASK-037 | Retry: 1/3
→ Reading review report: docs/specs/001-feature/tasks/TASK-037--review.md
→ Fixes applied → Next: reviewcleanup
When: state.step = "cleanup"
Actions: 1. Run task-implementation with --action=cleanup:
/developer-kit-specs:specs.task-implementation --action=cleanup --lang=LANG --task="TASK-FILE"Or for non-Claude CLIs:
- Remove debug logs and temporary comments
- Optimize imports
- Format code
- Verify documentation
2. Set state.step = "sync" 3. Save and stop
Output:
Ralph Loop | Step: cleanup | Task: TASK-037
→ Running /developer-kit-specs:specs.task-implementation --action=cleanup --lang=spring --task="docs/specs/001-feature/tasks/TASK-037.md"
→ Cleanup complete → Next: syncsync
When: state.step = "sync"
Actions: 1. Run specs.sync:
/developer-kit-specs:specs.sync SPEC-FOLDER/ --after-task=TASK-IDOr for non-Claude CLIs:
- Read decision-log.md for any deviations
- Compare implementation to spec
- Update spec if needed (with user approval)
2. Set state.step = "update_done" 3. Save and stop
Output:
Ralph Loop | Step: sync | Task: TASK-037
→ Running /developer-kit-specs:specs.sync docs/specs/001-feature/ --after-task=TASK-037
→ Sync complete → Next: update_doneupdate_done
When: state.step = "update_done"
Actions: 1. Update the task's YAML frontmatter: status: completed, completed_date: YYYY-MM-DD 2. In fix_plan.json:
- Move task from
pendingtodone - Increment
state.iteration - Increment
state.range_progress.done_in_range - Update
state.last_updated - For each remaining pending task: check if all dependencies are now in
done, if yes setdependencies_satisfied = true
3. Commit git changes (if clean):
git add -A && git commit -m "Ralph iteration N: TASK-ID [title]"4. Set state.step = "choose_task" 5. Save and stop
Output:
Ralph Loop | Step: update_done | Task: TASK-037
→ Marked TASK-037 done
→ Progress: 2/6 in range (33%)
→ Iteration: 3
→ Next: choose_taskcomplete
When: state.step = "complete"
Actions: Print completion summary and stop:
Ralph Loop | COMPLETE
═══════════════════════════════════════════════════════
Task Range: TASK-036 → TASK-041
Tasks Completed: 6/6
Total Iterations: 18
Deviations Detected: N
Learnings Captured: N
Run --action=start with a new range to continue.failed
When: state.step = "failed"
Actions: Print error and stop:
Ralph Loop | FAILED
═══════════════════════════════════════════════════════
Task: TASK-037
Error: review failed after 3 retries
Fix the issues manually, then resume:
/developer-kit-specs:specs.ralph-loop --action=loop --spec=docs/specs/001-feature/Shell Script Implementation (CLI-Agnostic)
Create docs/specs/[id]/_ralph_loop/run-ralph.sh:
#!/bin/bash
# Ralph Loop State Machine — CLI-Agnostic
# One invocation = one step
SPEC_FOLDER="$1"
FIX_PLAN="$SPEC_FOLDER/_ralph_loop/fix_plan.json"
# Read current step
STEP=$(jq -r '.state.step' "$FIX_PLAN")
TASK=$(jq -r '.state.current_task // "none"' "$FIX_PLAN")
ITERATION=$(jq -r '.state.iteration // 0' "$FIX_PLAN")
echo "Ralph Loop | Iteration: $ITERATION | Step: $STEP | Task: $TASK"
case "$STEP" in
init)
echo "→ Initializing... (already done)"
jq '.state.step = "choose_task"' "$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
;;
choose_task)
# Pick next task within range
# (Implementation: filter pending, check deps, pick simplest)
NEXT=$(jq -r '.pending | .[0]' "$FIX_PLAN" 2>/dev/null)
if [ "$NEXT" = "null" ] || [ -z "$NEXT" ]; then
echo "→ No more tasks in range"
jq '.state.step = "complete"' "$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
else
TASK_FILE=$(jq -r ".tasks[] | select(.id == \"$NEXT\") | .file" "$FIX_PLAN")
TASK_LANG=$(jq -r ".tasks[] | select(.id == \"$NEXT\") | .lang" "$FIX_PLAN")
echo "→ Selected: $NEXT"
jq --arg t "$NEXT" --arg f "$TASK_FILE" --arg l "$TASK_LANG" \
'.state.current_task = $t | .state.current_task_file = $f | .state.current_task_lang = $l | .state.step = "implementation" | .state.retry_count = 0' \
"$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
fi
;;
implementation)
echo "→ [IMPLEMENTATION: Run task-implementation for $TASK]"
# In real implementation: call the actual command
jq '.state.step = "review"' "$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
;;
review)
echo "→ [REVIEW: Run task-review for $TASK]"
# In real implementation: check results, on issues go to fix, on clean go to cleanup
jq '.state.step = "cleanup"' "$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
;;
cleanup)
echo "→ [CLEANUP: Run code-cleanup for $TASK]"
# In real implementation: call the actual command
jq '.state.step = "sync"' "$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
;;
fix)
echo "→ [FIX: Apply fixes for $TASK based on review report]"
# In real implementation: call the actual command or edit files
jq '.state.step = "review"' "$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
;;
sync)
echo "→ [SYNC: Run specs.sync for $TASK]"
# In real implementation: call the actual command
jq '.state.step = "update_done"' "$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
;;
update_done)
echo "→ [UPDATE_DONE: Marking $TASK as done]"
jq --arg t "$TASK" \
'.done += [$t] | .pending -= [$t] | .state.iteration += 1 | .state.step = "choose_task"' \
"$FIX_PLAN" > tmp.json && mv tmp.json "$FIX_PLAN"
;;
complete)
echo "═══════════════════════════════════════════════"
echo "Ralph Loop COMPLETE"
echo "═══════════════════════════════════════════════"
;;
failed)
echo "═══════════════════════════════════════════════"
echo "Ralph Loop FAILED"
ERROR=$(jq -r '.state.error // "unknown"' "$FIX_PLAN")
echo "Error: $ERROR"
echo "═══════════════════════════════════════════════"
;;
esac
echo ""Claude Code /loop Integration
# Start and loop (Claude Code)
/loop 5m /developer-kit-specs:specs.ralph-loop --action=start --spec=docs/specs/001-feature/ --from-task=TASK-036 --to-task=TASK-041
/loop 5m /developer-kit-specs:specs.ralph-loop --action=loop --spec=docs/specs/001-feature/ --from-task=TASK-036 --to-task=TASK-041Claude Code's /loop skill will repeatedly invoke this skill until state.step = "complete" or state.step = "failed".
Non-Claude CLI Shell Loop
cd docs/specs/001-feature/_ralph_loop/
# Claude Code
while :; do claude "Execute one step of Ralph Loop. Read fix_plan.json, run the current step, update state, and stop."; done
# Copilot CLI
while :; do copilot "Execute one step of Ralph Loop. Read fix_plan.json, run the current step, update state, and stop."; done
# Codex CLI
while :; do codex "Execute one step of Ralph Loop. Read fix_plan.json, run the current step, update state, and stop."; done#!/usr/bin/env python3
"""
Ralph Loop - State Machine Orchestrator
This script manages the Ralph Loop state machine for specification-driven development.
It orchestrates task implementation, review, cleanup, and synchronization.
Usage:
python3 ralph_loop.py --action=start --spec=docs/specs/001-feature/
python3 ralph_loop.py --action=loop --spec=docs/specs/001-feature/
python3 ralph_loop.py --action=status --spec=docs/specs/001-feature/
"""
import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any
# Supported agents/CLIs and their slash commands
# Note: cmd_prefix is added BEFORE the template, so templates should NOT start with /
# Use {task} placeholder for task ID, {spec} for spec path
SUPPORTED_AGENTS = {
"claude": {
"name": "Claude Code",
"cmd_prefix": "/",
"task_impl": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task}",
"task_review": "developer-kit-specs:specs.task-review --spec={spec} --task={task}",
"spec_sync": "developer-kit-specs:specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"codex": {
"name": "Codex CLI",
"cmd_prefix": "$",
"task_impl": "specs.task-implementation --spec={spec} --task={task}",
"task_review": "specs.task-review --spec={spec} --task={task}",
"spec_sync": "specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"copilot": {
"name": "GitHub Copilot CLI",
"cmd_prefix": "/",
"task_impl": "specs.task-implementation --spec={spec} --task={task}",
"task_review": "specs.task-review --spec={spec} --task={task}",
"spec_sync": "specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"kimi": {
"name": "Kimi CLI",
"cmd_prefix": "/skill:",
"task_impl": "specs.task-implementation --spec={spec} --task={task}",
"task_review": "specs.task-review --spec={spec} --task={task}",
"spec_sync": "specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"gemini": {
"name": "Gemini CLI",
"cmd_prefix": "/",
"task_impl": "specs.task-implementation --spec={spec} --task={task}",
"task_review": "specs.task-review --spec={spec} --task={task}",
"spec_sync": "specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"glm4": {
"name": "GLM-4 CLI",
"cmd_prefix": "/",
"task_impl": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task}",
"task_review": "developer-kit-specs:specs.task-review --spec={spec} --task={task}",
"spec_sync": "developer-kit-specs:specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"minimax": {
"name": "MiniMax CLI",
"cmd_prefix": "/",
"task_impl": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task}",
"task_review": "developer-kit-specs:specs.task-review --spec={spec} --task={task}",
"spec_sync": "developer-kit-specs:specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"openrouter": {
"name": "OpenRouter CLI",
"cmd_prefix": "/",
"task_impl": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task}",
"task_review": "developer-kit-specs:specs.task-review --spec={spec} --task={task}",
"spec_sync": "developer-kit-specs:specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
"qwen": {
"name": "Qwen Code",
"cmd_prefix": "/",
"task_impl": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task}",
"task_review": "developer-kit-specs:specs.task-review --spec={spec} --task={task}",
"spec_sync": "developer-kit-specs:specs.sync --spec={spec} --after-task={task}",
"code_cleanup": "developer-kit-specs:specs.task-implementation --spec={spec} --task={task} --action=cleanup",
"ralph_loop": "ralph-loop --action=loop --spec={spec}",
},
}
def parse_args():
parser = argparse.ArgumentParser(
description="Ralph Loop State Machine - Orchestrates specification-driven development"
)
parser.add_argument(
"--action", required=True,
choices=["start", "loop", "status", "resume", "next"],
help="Action to execute: start (initialize), loop (run one step), status (show state), resume (continue), next (advance to next step)"
)
parser.add_argument(
"--spec", required=True,
help="Path to specification folder (e.g. docs/specs/001-feature/)"
)
parser.add_argument(
"--from-task", default=None,
help="Start task for range filter (e.g. TASK-036)"
)
parser.add_argument(
"--to-task", default=None,
help="End task for range filter (e.g. TASK-041)"
)
parser.add_argument(
"--agent", default="claude",
choices=list(SUPPORTED_AGENTS.keys()),
help="Default agent/CLI to use for tasks"
)
parser.add_argument(
"--no-commit", action="store_true",
help="Skip git commits (useful for testing)"
)
return parser.parse_args()
def validate_spec_path(spec_path: str) -> bool:
"""
Validates that the spec path follows the correct structure.
Correct format: docs/specs/[ID-feature]/ or [ID-feature]/
"""
spec_dir = Path(spec_path).resolve()
# Check if path exists
if not spec_dir.exists():
return False
# Check if it's a directory
if not spec_dir.is_dir():
return False
# Check if it looks like a spec folder (should contain spec files or tasks/)
has_spec_files = any(spec_dir.glob("*.md"))
has_tasks_dir = (spec_dir / "tasks").exists()
has_ralph_dir = (spec_dir / "_ralph_loop").exists()
if not (has_spec_files or has_tasks_dir or has_ralph_dir):
return False
return True
def get_fix_plan_path(spec_path: str) -> Path:
"""
Returns the path to fix_plan.json.
IMPORTANT: The file MUST ALWAYS be in docs/specs/[ID-feature]/_ralph_loop/fix_plan.json
This is the ONLY valid location to prevent LLMs from creating files in wrong locations.
"""
spec_dir = Path(spec_path)
# The ONLY valid location: _ralph_loop subdirectory
ralph_path = spec_dir / "_ralph_loop" / "fix_plan.json"
# For backwards compatibility: if file exists in root, warn and migrate
direct_path = spec_dir / "fix_plan.json"
if direct_path.exists():
print(f"⚠️ WARNING: fix_plan.json found in wrong location: {direct_path}")
print(f" Valid location is: {ralph_path}")
print(f" Please move it manually:")
print(f" mkdir -p {spec_dir}/_ralph_loop")
print(f" mv {direct_path} {ralph_path}")
# Still return the old path for now to allow migration
return direct_path
return ralph_path
def load_fix_plan(spec_path: str) -> dict:
"""Loads fix_plan.json"""
fix_plan_path = get_fix_plan_path(spec_path)
if not fix_plan_path.exists():
print(f"❌ fix_plan.json not found in {spec_path}")
print(f" Run first: --action=start")
sys.exit(1)
with open(fix_plan_path) as f:
return json.load(f)
def save_fix_plan(spec_path: str, data: dict):
"""
Saves fix_plan.json to the correct location.
IMPORTANT: The file MUST ALWAYS be in docs/specs/[ID-feature]/_ralph_loop/fix_plan.json
This function enforces the correct directory structure.
"""
spec_dir = Path(spec_path)
# ALWAYS use _ralph_loop subdirectory - this is the ONLY valid location
ralph_dir = spec_dir / "_ralph_loop"
fix_plan_path = ralph_dir / "fix_plan.json"
# Create _ralph_loop directory if it doesn't exist
ralph_dir.mkdir(parents=True, exist_ok=True)
# For backwards compatibility: if old file exists in root, migrate it
old_path = spec_dir / "fix_plan.json"
if old_path.exists() and not fix_plan_path.exists():
print(f"📦 Migrating fix_plan.json to correct location...")
import shutil
shutil.move(old_path, fix_plan_path)
print(f" ✅ Migrated to: {fix_plan_path}")
# Update timestamp
data["state"]["last_updated"] = datetime.now().isoformat()
# Save with proper formatting
with open(fix_plan_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n") # Add trailing newline
def find_tasks_file(spec_path: str) -> Optional[Path]:
"""Finds the tasks file in the specification folder"""
spec_dir = Path(spec_path)
# Look for files ending with --tasks.md
for file in spec_dir.glob("*--tasks.md"):
return file
# Look in tasks subdirectory
tasks_subdir = spec_dir / "tasks"
if tasks_subdir.exists():
for file in tasks_subdir.glob("*--tasks.md"):
return file
tasks_file = tasks_subdir / "tasks.md"
if tasks_file.exists():
return tasks_file
# Look for generic tasks.md
tasks_file = spec_dir / "tasks.md"
if tasks_file.exists():
return tasks_file
return None
def find_task_files(spec_path: str) -> List[Path]:
"""Finds all individual task files (TASK-XXX.md)"""
spec_dir = Path(spec_path)
task_files = []
# Look in tasks subdirectory (exclude --review and other auxiliary files)
tasks_dir = spec_dir / "tasks"
if tasks_dir.exists():
task_files.extend(sorted(f for f in tasks_dir.glob("TASK-*.md") if "--" not in f.stem))
return task_files
def parse_task_file(task_file: Path) -> dict:
"""Parses a single task file to extract metadata"""
with open(task_file) as f:
content = f.read()
task_id = task_file.stem # TASK-XXX
task_data = {
"id": task_id,
"file": str(task_file),
"title": "",
"description": "",
"status": "pending",
"lang": "",
"dependencies": [],
"complexity": "medium",
"agent": None,
}
# Parse YAML frontmatter
frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if frontmatter_match:
frontmatter = frontmatter_match.group(1)
# Extract fields (strip quotes in case YAML values are quoted)
id_match = re.search(r'^id:\s*(.+)$', frontmatter, re.MULTILINE)
if id_match:
task_data["id"] = id_match.group(1).strip().strip('"\'')
title_match = re.search(r'^title:\s*(.+)$', frontmatter, re.MULTILINE)
if title_match:
task_data["title"] = title_match.group(1).strip().strip('"\'')
status_match = re.search(r'^status:\s*(.+)$', frontmatter, re.MULTILINE)
if status_match:
task_data["status"] = status_match.group(1).strip().strip('"\'')
lang_match = re.search(r'^lang(?:uage)?:\s*(.+)$', frontmatter, re.MULTILINE)
if lang_match:
task_data["lang"] = lang_match.group(1).strip().strip('"\'')
deps_match = re.search(r'^dependencies:\s*\[(.*?)\]', frontmatter, re.MULTILINE | re.DOTALL)
if deps_match:
deps_str = deps_match.group(1)
task_data["dependencies"] = [d.strip().strip('"\'') for d in deps_str.split(",") if d.strip()]
complexity_match = re.search(r'^complexity:\s*(\w+)', frontmatter, re.MULTILINE)
if complexity_match:
task_data["complexity"] = complexity_match.group(1).strip()
agent_match = re.search(r'^agent:\s*(\w+)', frontmatter, re.MULTILINE)
if agent_match:
task_data["agent"] = agent_match.group(1).strip().lower()
# Extract description from first paragraph after frontmatter
content_without_front = re.sub(r'^---\s*\n.*?\n---\s*\n', '', content, flags=re.DOTALL)
desc_match = re.search(r'##?\s*Description\s*\n\s*(.+?)(?:\n\n|\n##|\Z)', content_without_front, re.DOTALL | re.IGNORECASE)
if desc_match:
task_data["description"] = desc_match.group(1).strip()[:100]
else:
# First paragraph
first_para = content_without_front.strip().split('\n\n')[0]
task_data["description"] = first_para[:100] if first_para else ""
return task_data
def parse_tasks_from_files(spec_path: str) -> List[dict]:
"""Parses all task files from the tasks directory"""
task_files = find_task_files(spec_path)
tasks = []
for task_file in task_files:
task_data = parse_task_file(task_file)
tasks.append(task_data)
# Sort by task ID
tasks.sort(key=lambda t: t["id"])
return tasks
def filter_tasks_by_range(tasks: List[dict], from_task: Optional[str], to_task: Optional[str]) -> List[dict]:
"""Filters tasks by range"""
if not from_task and not to_task:
return tasks
def task_num(task_id: str) -> int:
match = re.search(r'(\d+)', task_id)
return int(match.group(1)) if match else 0
from_num = task_num(from_task) if from_task else 0
to_num = task_num(to_task) if to_task else float('inf')
filtered = []
for task in tasks:
num = task_num(task["id"])
if from_num <= num <= to_num:
filtered.append(task)
return filtered
def get_next_pending_task(fix_plan: dict) -> Optional[dict]:
"""Finds the next pending task with satisfied dependencies"""
tasks = fix_plan.get("tasks", [])
completed_tasks = {t["id"] for t in tasks if t["status"] in ["done", "completed", "implemented", "reviewed"]}
for task in tasks:
if task["status"] not in ["pending", "in_progress"]:
continue
# Check dependencies
deps_satisfied = all(dep in completed_tasks for dep in task.get("dependencies", []))
if deps_satisfied:
return task
return None
def get_agent_config(fix_plan: dict, args_agent: str = None) -> dict:
"""Returns the agent configuration to use"""
# 1. Use agent from CLI args if specified
if args_agent and args_agent in SUPPORTED_AGENTS:
return SUPPORTED_AGENTS[args_agent]
# 2. Use agent from current task
current_task_id = fix_plan.get("state", {}).get("current_task")
if current_task_id:
for task in fix_plan.get("tasks", []):
if task["id"] == current_task_id and task.get("agent"):
return SUPPORTED_AGENTS.get(task["agent"], SUPPORTED_AGENTS["claude"])
# 3. Use default agent from fix_plan
default_agent = fix_plan.get("default_agent", "claude")
return SUPPORTED_AGENTS.get(default_agent, SUPPORTED_AGENTS["claude"])
def format_command(agent: dict, cmd_type: str, **kwargs) -> str:
"""Formats a command for the specified agent"""
cmd_template = agent.get(cmd_type, cmd_type)
cmd = cmd_template.format(**kwargs)
return f"{agent['cmd_prefix']}{cmd}"
def run_git_commit(spec_path: str, task_id: str, task_title: str, iteration: int) -> bool:
"""Commits changes for a completed task"""
try:
# Check if there are changes to commit
result = subprocess.run(
["git", "-C", spec_path, "status", "--porcelain"],
capture_output=True,
text=True
)
if not result.stdout.strip():
print(f" No changes to commit")
return True
# Stage all changes
subprocess.run(
["git", "-C", spec_path, "add", "-A"],
capture_output=True,
check=True
)
# Commit
commit_msg = f"Ralph iteration {iteration}: {task_id} - {task_title[:50]}"
subprocess.run(
["git", "-C", spec_path, "commit", "-m", commit_msg],
capture_output=True,
check=True
)
print(f" Committed: {commit_msg}")
return True
except subprocess.CalledProcessError as e:
print(f" ⚠️ Git commit failed: {e}")
return False
except FileNotFoundError:
print(f" ⚠️ Git not found")
return False
def update_task_status(fix_plan: dict, task_id: str, new_status: str):
"""Updates the status of a task in the tasks list"""
for task in fix_plan.get("tasks", []):
if task["id"] == task_id:
task["status"] = new_status
break
def action_start(spec_path: str, from_task: Optional[str], to_task: Optional[str], agent: str = "claude"):
"""Initializes fix_plan.json from task files"""
print(f"🚀 Ralph Loop | Initializing...")
print(f" Spec: {spec_path}")
# Parse task files
tasks = parse_tasks_from_files(spec_path)
if not tasks:
# Fallback: try to find tasks.md
tasks_file = find_tasks_file(spec_path)
if tasks_file:
print(f" Tasks file: {tasks_file}")
print(" ⚠️ No individual task files found. Creating from tasks.md...")
# Create basic task entries from tasks.md parsing
# This is a simplified version
else:
print("❌ No tasks found. Create task files in tasks/ directory.")
sys.exit(1)
print(f" Found {len(tasks)} tasks")
# Apply range filter
filtered_tasks = filter_tasks_by_range(tasks, from_task, to_task)
# Separate into categories
pending = [t["id"] for t in filtered_tasks if t["status"] in ["pending", "in_progress"]]
done = [t["id"] for t in filtered_tasks if t["status"] in ["done", "completed", "implemented", "reviewed"]]
# Create fix_plan with full structure
fix_plan = {
"spec_id": Path(spec_path).name,
"spec_folder": spec_path,
"task_range": {
"from": from_task,
"to": to_task,
"from_num": int(re.search(r'(\d+)', from_task).group(1)) if from_task else 0,
"to_num": int(re.search(r'(\d+)', to_task).group(1)) if to_task else 999,
"total_in_range": len(filtered_tasks),
},
"default_agent": agent,
"tasks": filtered_tasks,
"pending": pending,
"done": done,
"optional": [],
"superseded": [],
"learnings": [],
"state": {
"step": "choose_task",
"current_task": None,
"current_task_file": None,
"current_task_lang": None,
"iteration": 0,
"retry_count": 0,
"review_file_retry": 0, # separate counter for file validation failures
"review_file_error": None, # last file validation error message
"last_updated": datetime.now().isoformat(),
"error": None,
"range_progress": {
"done_in_range": len(done),
"total_in_range": len(filtered_tasks),
}
}
}
save_fix_plan(spec_path, fix_plan)
agent_config = SUPPORTED_AGENTS[agent]
print(f"✅ fix_plan.json created")
print(f" Tasks in range: {len(filtered_tasks)}")
print(f" Pending: {len(pending)}")
print(f" Done: {len(done)}")
print(f" Default agent: {agent_config['name']}")
if from_task or to_task:
print(f" Range: {from_task or 'START'} → {to_task or 'END'}")
print(f"\n📋 Initial state: {fix_plan['state']['step']}")
print(f"\n➡️ Next: Run loop to start processing")
ralph_cmd = format_command(agent_config, "ralph_loop", action="loop", spec=spec_path)
print(f" {ralph_cmd}")
def action_loop(spec_path: str, args_agent: str = None, no_commit: bool = False):
"""Executes one step of the state machine"""
fix_plan = load_fix_plan(spec_path)
state = fix_plan["state"]
step = state["step"]
agent_config = get_agent_config(fix_plan, args_agent)
print(f"🔄 Ralph Loop | Iteration {state['iteration']} | Step: {step}")
# Handle each step
if step == "init":
handle_init(spec_path, fix_plan)
elif step == "choose_task":
handle_choose_task(spec_path, fix_plan, agent_config)
elif step == "implementation":
handle_implementation(spec_path, fix_plan, agent_config)
elif step == "review":
handle_review(spec_path, fix_plan, agent_config)
elif step == "fix":
handle_fix(spec_path, fix_plan, agent_config)
elif step == "cleanup":
handle_cleanup(spec_path, fix_plan, agent_config)
elif step == "sync":
handle_sync(spec_path, fix_plan, agent_config)
elif step == "update_done":
handle_update_done(spec_path, fix_plan, agent_config, no_commit)
elif step == "complete":
handle_complete(fix_plan)
elif step == "failed":
handle_failed(fix_plan)
else:
print(f"❌ Unknown state: {step}")
print(f" Valid states: init, choose_task, implementation, review, fix, cleanup, sync, update_done, complete, failed")
# Provide helpful guidance for common errors
if "-" in step:
suggested = step.replace("-", "_")
print(f"\n💡 HINT: The state '{step}' uses a dash (-) instead of an underscore (_).")
print(f" Did you mean: '{suggested}'?")
print(f"\n To fix this, run:")
print(f" sed -i 's/\"{step}\"/\"{suggested}\"/g' {get_fix_plan_path(spec_path)}")
sys.exit(1)
def handle_init(spec_path: str, fix_plan: dict):
"""Handle init step - transition to choose_task"""
print("→ Initializing...")
fix_plan["state"]["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
print("→ Initialized | Next: choose_task")
def handle_choose_task(spec_path: str, fix_plan: dict, agent_config: dict):
"""Handle choose_task step - select next task"""
next_task = get_next_pending_task(fix_plan)
if not next_task:
# Check for misalignment: pending list not empty but no pending tasks found
pending_list = fix_plan.get("pending", [])
if pending_list:
# Misalignment detected: pending list has items but task statuses don't match
print("⚠️ WARNING: Task status misalignment detected!")
print(f" Pending list has {len(pending_list)} task(s), but no pending tasks found in task list.")
# Get the first task from pending list
first_pending_id = pending_list[0]
tasks = fix_plan.get("tasks", [])
for task in tasks:
if task["id"] == first_pending_id:
next_task = task
# Fix the status if it's incorrectly marked as completed
if task["status"] == "completed":
task["status"] = "pending"
print(f" Fixed {first_pending_id}: status 'completed' → 'pending'")
# Save immediately to persist the fix
save_fix_plan(spec_path, fix_plan)
print(f" Using first pending task: {first_pending_id}")
break
if not next_task:
print(f" ⚠️ Task {first_pending_id} not found in task list!")
if not next_task:
fix_plan["state"]["step"] = "complete"
save_fix_plan(spec_path, fix_plan)
print("→ No more tasks in range")
print("═══════════════════════════════════════════════════════")
print("Ralph Loop COMPLETE")
print("═══════════════════════════════════════════════════════")
return
task_id = next_task["id"]
task_title = next_task.get("title", "")
fix_plan["state"]["current_task"] = task_id
fix_plan["state"]["current_task_file"] = next_task.get("file", "")
fix_plan["state"]["current_task_lang"] = next_task.get("lang", "")
fix_plan["state"]["step"] = "implementation"
fix_plan["state"]["retry_count"] = 0
save_fix_plan(spec_path, fix_plan)
print(f"→ Selected: {task_id}")
if task_title:
print(f" Title: {task_title}")
deps = next_task.get("dependencies", [])
if deps:
print(f" Dependencies: {', '.join(deps)} ✓")
print(f"→ Next: implementation")
print("")
print("Execute:")
cmd = format_command(agent_config, "task_impl", spec=spec_path, task=task_id)
print(f" {cmd}")
def handle_implementation(spec_path: str, fix_plan: dict, agent_config: dict):
"""Handle implementation step"""
current_task = fix_plan["state"].get("current_task")
task_file = fix_plan["state"].get("current_task_file", "")
task_lang = fix_plan["state"].get("current_task_lang", "")
if not current_task:
fix_plan["state"]["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
print("⚠️ No current task, returning to choose_task")
return
# Check for task-specific agent
task_agent = None
for task in fix_plan.get("tasks", []):
if task["id"] == current_task and task.get("agent"):
task_agent = SUPPORTED_AGENTS.get(task["agent"])
break
if task_agent:
agent_config = task_agent
print(f"🤖 Using agent: {agent_config['name']}")
print(f"→ Implementation: {current_task}")
print("")
print("Execute:")
cmd = format_command(agent_config, "task_impl", spec=spec_path, task=current_task)
print(f" {cmd}")
print("")
print("After execution, update state:")
print(" python3 ralph_loop.py --action=loop --spec=" + spec_path)
def handle_review(spec_path: str, fix_plan: dict, agent_config: dict):
"""Handle review step"""
current_task = fix_plan["state"].get("current_task")
task_file = fix_plan["state"].get("current_task_file", "")
task_lang = fix_plan["state"].get("current_task_lang", "")
if not current_task:
fix_plan["state"]["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
return
retry_count = fix_plan["state"].get("retry_count", 0)
review_file_error = fix_plan["state"].get("review_file_error")
review_file_retry = fix_plan["state"].get("review_file_retry", 0)
# Check for task-specific agent
task_agent = None
for task in fix_plan.get("tasks", []):
if task["id"] == current_task and task.get("agent"):
task_agent = SUPPORTED_AGENTS.get(task["agent"])
break
if task_agent:
agent_config = task_agent
print(f"→ Review: {current_task} | Retry: {retry_count}/3")
# If there was a review file error from the previous --action=next, surface it to the agent
if review_file_error:
print(f"")
print(f"⚠️ REVIEW FILE ERROR (attempt {review_file_retry}/3):")
print(f" {review_file_error}")
print(f"")
print(f" The review file MUST be created with this exact frontmatter structure:")
print(f" ---")
print(f" review_status: PASSED # or FAILED")
print(f" critical_issues: 0 # required if FAILED")
print(f" major_issues: 0 # required if FAILED")
print(f" ---")
print(f"")
print(f" Expected file path: tasks/{current_task}--review.md")
print(f"")
print("")
print("Execute:")
cmd = format_command(agent_config, "task_review", spec=spec_path, task=current_task)
print(f" {cmd}")
print("")
print("Review the generated review report, then update state:")
print(" python3 ralph_loop.py --action=loop --spec=" + spec_path)
def handle_fix(spec_path: str, fix_plan: dict, agent_config: dict):
"""Handle fix step - apply fixes from review"""
current_task = fix_plan["state"].get("current_task")
if not current_task:
fix_plan["state"]["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
return
# Check for task-specific agent
task_agent = None
for task in fix_plan.get("tasks", []):
if task["id"] == current_task and task.get("agent"):
task_agent = SUPPORTED_AGENTS.get(task["agent"])
break
if task_agent:
agent_config = task_agent
print(f"→ Fix: {current_task}")
print("")
print("Steps:")
print(f" 1. Read review report: {current_task}--review.md")
print(f" 2. Apply fixes")
print(f" 3. Update state:")
print(f" python3 ralph_loop.py --action=loop --spec=" + spec_path)
print("")
print("Or execute directly:")
cmd = format_command(agent_config, "task_impl", spec=spec_path, task=current_task)
print(f" {cmd}")
def handle_cleanup(spec_path: str, fix_plan: dict, agent_config: dict):
"""Handle cleanup step"""
current_task = fix_plan["state"].get("current_task")
task_file = fix_plan["state"].get("current_task_file", "")
task_lang = fix_plan["state"].get("current_task_lang", "")
if not current_task:
fix_plan["state"]["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
return
# Check for task-specific agent
task_agent = None
for task in fix_plan.get("tasks", []):
if task["id"] == current_task and task.get("agent"):
task_agent = SUPPORTED_AGENTS.get(task["agent"])
break
if task_agent:
agent_config = task_agent
print(f"→ Cleanup: {current_task}")
print("")
print("Execute:")
cmd = format_command(agent_config, "code_cleanup", spec=spec_path, task=current_task)
print(f" {cmd}")
print("")
print("After cleanup, update state:")
print(" python3 ralph_loop.py --action=loop --spec=" + spec_path)
def handle_sync(spec_path: str, fix_plan: dict, agent_config: dict):
"""Handle sync step"""
current_task = fix_plan["state"].get("current_task")
if not current_task:
fix_plan["state"]["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
return
# Check for task-specific agent
task_agent = None
for task in fix_plan.get("tasks", []):
if task["id"] == current_task and task.get("agent"):
task_agent = SUPPORTED_AGENTS.get(task["agent"])
break
if task_agent:
agent_config = task_agent
print(f"→ Sync: {current_task}")
print("")
print("Execute:")
cmd = format_command(agent_config, "spec_sync", spec=spec_path, task=current_task)
print(f" {cmd}")
print("")
print("After sync, update state:")
print(" python3 ralph_loop.py --action=loop --spec=" + spec_path)
def handle_update_done(spec_path: str, fix_plan: dict, agent_config: dict, no_commit: bool):
"""Handle update_done step - mark task complete and commit"""
current_task = fix_plan["state"].get("current_task")
if not current_task:
fix_plan["state"]["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
return
# Find task details
task_title = ""
for task in fix_plan.get("tasks", []):
if task["id"] == current_task:
task["status"] = "completed"
task_title = task.get("title", "")
break
# Update lists
if "pending" not in fix_plan:
fix_plan["pending"] = []
if "done" not in fix_plan:
fix_plan["done"] = []
if current_task in fix_plan["pending"]:
fix_plan["pending"].remove(current_task)
if current_task not in fix_plan["done"]:
fix_plan["done"].append(current_task)
# Update state
iteration = fix_plan["state"].get("iteration", 0) + 1
fix_plan["state"]["iteration"] = iteration
fix_plan["state"]["current_task"] = None
fix_plan["state"]["current_task_file"] = None
fix_plan["state"]["current_task_lang"] = None
fix_plan["state"]["step"] = "choose_task"
fix_plan["state"]["retry_count"] = 0
# Update progress
done_count = len(fix_plan.get("done", []))
total_count = fix_plan.get("task_range", {}).get("total_in_range", 0)
if "range_progress" not in fix_plan["state"]:
fix_plan["state"]["range_progress"] = {}
fix_plan["state"]["range_progress"]["done_in_range"] = done_count
save_fix_plan(spec_path, fix_plan)
print(f"→ Done: {current_task}")
if task_title:
print(f" {task_title}")
# Git commit
if not no_commit:
print("→ Committing changes...")
run_git_commit(spec_path, current_task, task_title, iteration)
progress_pct = (done_count / total_count * 100) if total_count > 0 else 0
print(f"→ Progress: {done_count}/{total_count} in range ({progress_pct:.0f}%)")
print(f"→ Iteration: {iteration}")
print(f"→ Next: choose_task")
def handle_complete(fix_plan: dict):
"""Handle complete state"""
done_count = len(fix_plan.get("done", []))
total_count = fix_plan.get("task_range", {}).get("total_in_range", 0)
iteration = fix_plan["state"].get("iteration", 0)
print("═══════════════════════════════════════════════════════")
print("Ralph Loop COMPLETE")
print("═══════════════════════════════════════════════════════")
print(f"Task Range: {fix_plan.get('task_range', {}).get('from', 'START')} → {fix_plan.get('task_range', {}).get('to', 'END')}")
print(f"Tasks Completed: {done_count}/{total_count}")
print(f"Total Iterations: {iteration}")
print("")
print("All tasks in range implemented and verified.")
print("Run --action=start with a new range to continue.")
def handle_failed(fix_plan: dict):
"""Handle failed state"""
current_task = fix_plan["state"].get("current_task", "N/A")
error = fix_plan["state"].get("error", "Unknown error")
retry_count = fix_plan["state"].get("retry_count", 0)
print("═══════════════════════════════════════════════════════")
print("Ralph Loop FAILED")
print("═══════════════════════════════════════════════════════")
print(f"Task: {current_task}")
print(f"Error: {error}")
print(f"Retry count: {retry_count}/3")
print("")
print("Fix the issues manually, then resume:")
print(" python3 ralph_loop.py --action=loop --spec=" + fix_plan.get("spec_folder", ""))
def action_status(spec_path: str):
"""Shows current status"""
fix_plan = load_fix_plan(spec_path)
state = fix_plan["state"]
print("📊 Ralph Loop Status")
print("═══════════════════════════════════════════════════════")
print(f"Spec: {spec_path}")
print(f"Step: {state['step']}")
print(f"Iteration: {state['iteration']}")
if fix_plan.get('default_agent'):
agent_name = SUPPORTED_AGENTS.get(fix_plan['default_agent'], {}).get('name', fix_plan['default_agent'])
print(f"Default Agent: {agent_name}")
if state.get('current_task'):
print(f"Current Task: {state['current_task']}")
if state.get('current_task_lang'):
print(f"Language: {state['current_task_lang']}")
if state.get('retry_count', 0) > 0:
print(f"Retry: {state['retry_count']}/3")
done_count = len(fix_plan.get("done", []))
pending_count = len(fix_plan.get("pending", []))
total_count = fix_plan.get("task_range", {}).get("total_in_range", 0)
print("")
print(f"Progress: {done_count}/{total_count} done, {pending_count} pending")
if fix_plan.get("tasks"):
print("")
print("Tasks:")
for task in fix_plan["tasks"]:
status = task.get("status", "pending")
icon = {
"pending": "⏳",
"in_progress": "🔄",
"done": "✅",
"completed": "✅",
"failed": "❌"
}.get(status, "❓")
agent_info = f" [{task.get('agent', '')}]" if task.get("agent") else ""
print(f" {icon} {task['id']}{agent_info}: {task.get('title', task.get('description', ''))[:40]}")
def action_resume(spec_path: str, args_agent: str = None, no_commit: bool = False):
"""Resumes the loop from current state"""
fix_plan = load_fix_plan(spec_path)
state = fix_plan["state"]
print(f"▶️ Ralph Loop | Resume from {state['step']}")
if state["step"] in ["complete", "failed"]:
print(f" Final state: {state['step']}")
print(" To restart, use --action=start")
return
# Continue with loop
action_loop(spec_path, args_agent, no_commit)
def validate_review_file(spec_path: str, task_id: str) -> tuple[bool, str]:
"""
Structural pre-validation of the review file before semantic check.
Verifies: file exists, has frontmatter delimiters, has valid review_status field.
Returns: (True, "") if valid, (False, "<error message>") if not.
"""
spec_dir = Path(spec_path)
review_file = spec_dir / "tasks" / f"{task_id}--review.md"
if not review_file.exists():
return False, (
f"Review file not found: tasks/{task_id}--review.md. "
f"The agent must create this file with a valid YAML frontmatter."
)
try:
with open(review_file, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return False, f"Cannot read review file: {e}"
frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if not frontmatter_match:
return False, (
f"Review file tasks/{task_id}--review.md has no valid YAML frontmatter. "
f"The file must start with '---' delimiters containing the frontmatter block."
)
frontmatter = frontmatter_match.group(1)
status_match = re.search(r'^review_status:\s*(\w+)', frontmatter, re.MULTILINE | re.IGNORECASE)
if not status_match:
return False, (
f"Review file tasks/{task_id}--review.md is missing the 'review_status' field. "
f"Required frontmatter: review_status: PASSED|FAILED"
)
review_status = status_match.group(1).upper()
if review_status not in {"PASSED", "FAILED"}:
return False, (
f"Invalid review_status value '{review_status}' in tasks/{task_id}--review.md. "
f"Allowed values: PASSED or FAILED."
)
return True, ""
def check_review_result(spec_path: str, task_id: str) -> tuple[bool, str]:
"""
Check the review report to determine if review passed or failed.
First tries to parse the YAML frontmatter for structured review_status field.
Falls back to text pattern matching if YAML frontmatter is not present.
Returns:
tuple: (passed: bool, reason: str)
- passed: True if review passed, False if failed
- reason: Description of the result
"""
spec_dir = Path(spec_path)
# Look for review report file: tasks/TASK-XXX--review.md
review_file = spec_dir / "tasks" / f"{task_id}--review.md"
if not review_file.exists():
# No review report found - assume not implemented
return False, f"Review report not found: {review_file}"
try:
with open(review_file, 'r', encoding='utf-8') as f:
content = f.read()
content_lower = content.lower()
# === METHOD 1: Parse YAML frontmatter (preferred, structured approach) ===
frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if frontmatter_match:
frontmatter = frontmatter_match.group(1)
# Extract review_status from frontmatter
status_match = re.search(r'^review_status:\s*(\w+)', frontmatter, re.MULTILINE | re.IGNORECASE)
if status_match:
review_status = status_match.group(1).upper()
if review_status == "PASSED":
return True, "Review status: PASSED (from YAML frontmatter)"
elif review_status == "FAILED":
# Check for critical/major issues count
critical_match = re.search(r'^critical_issues:\s*(\d+)', frontmatter, re.MULTILINE | re.IGNORECASE)
major_match = re.search(r'^major_issues:\s*(\d+)', frontmatter, re.MULTILINE | re.IGNORECASE)
critical_count = int(critical_match.group(1)) if critical_match else 0
major_count = int(major_match.group(1)) if major_match else 0
return False, f"Review status: FAILED with {critical_count} critical, {major_count} major issues"
elif review_status == "PARTIAL":
return False, "Review status: PARTIAL (needs more work)"
else:
return False, f"Unknown review_status: {review_status}"
# === METHOD 2: Fallback to text pattern matching (for legacy reviews) ===
# Check for common failure indicators in review reports (English + Italian)
failure_indicators = [
# English
"❌ not started",
"❌ not implemented",
"❌ fail",
"❌ failed",
"❌ missing",
"implementation status: ❌",
"implementation: not started",
"implementation: not implemented",
"acceptance criteria: 0/",
"acceptance criteria: 1/",
"acceptance criteria: 2/",
"acceptance criteria: 3/",
"acceptance criteria: 4/",
"passed: 0/",
"passed: 1/",
"passed: 2/",
"passed: 3/",
"passed: 4/",
# Italian
"❌ non implementato",
"❌ non iniziato",
"❌ fallito",
"❌ mancante",
"stato implementazione: ❌",
"implementazione: non iniziata",
"implementazione: non implementata",
"criteri di accettazione: 0/",
"criteri di accettazione: 1/",
"criteri di accettazione: 2/",
"criteri di accettazione: 3/",
"criteri di accettazione: 4/",
"esito: ❌",
"non conforme",
"non soddisfatti",
"task non implementato",
"needs revision",
"need revision",
"deve essere risolto",
"correggere",
]
success_indicators = [
# English
"✅ implemented",
"✅ complete",
"✅ pass",
"✅ passed",
"implementation status: ✅",
"implementation: complete",
"implementation: done",
"acceptance criteria: 5/5",
"acceptance criteria: 4/4",
"acceptance criteria: 3/3",
"acceptance criteria: 100%",
"passed: 5/5",
"passed: 4/4",
"passed: 3/3",
"all criteria met",
"all tests passed",
"no issues found",
"review passed",
# Italian
"✅ implementato",
"✅ completato",
"✅ passato",
"✅ superato",
"stato implementazione: ✅",
"implementazione: completata",
"implementazione: completato",
"criteri di accettazione: 5/5",
"criteri di accettazione: 4/4",
"criteri di accettazione: 3/3",
"criteri di accettazione: 100%",
"criteri: 5/5",
"criteri: 4/4",
"criteri: 3/3",
"tutti i criteri soddisfatti",
"tutti i test passano",
"nessun problema trovato",
"nessun issue",
"review superata",
"esito: ✅",
]
# Check for critical issues that should fail the review regardless of other indicators
critical_issue_patterns = [
r"critical.*issue",
r"issue.*critical",
r"problema.*critico",
r"critico",
r"blocking issue",
r"must fix",
r"deve essere corretto",
r"deve essere risolto",
r"blocking",
r"needs fix",
r"needs revision",
r"non conforme",
]
for pattern in critical_issue_patterns:
if re.search(pattern, content_lower):
return False, f"Found critical issue indicator: '{pattern}'"
# Check for explicit failure indicators FIRST (take precedence over success)
for indicator in failure_indicators:
if indicator.lower() in content_lower:
return False, f"Found failure indicator: '{indicator}'"
# Check for explicit success indicators
for indicator in success_indicators:
if indicator.lower() in content_lower:
return True, f"Found success indicator: '{indicator}'"
# Check acceptance criteria percentage
# Pattern: "X/Y soddisfatte" or "X/Y satisfied"
criteria_match = re.search(r'(\d+)\s*/\s*(\d+)\s*(?:soddisfatte|satisfied|pass)', content_lower)
if criteria_match:
passed = int(criteria_match.group(1))
total = int(criteria_match.group(2))
if passed >= total:
return True, f"All acceptance criteria met: {passed}/{total}"
else:
return False, f"Acceptance criteria not fully met: {passed}/{total}"
# Check for "Not Started" or "Not Implemented"
if "not started" in content_lower or "not implemented" in content_lower:
return False, "Implementation not started"
# Default: FAIL rather than pass (safer - require explicit success indicators)
return False, "Review report exists but no clear success indicators found"
except Exception as e:
return False, f"Error reading review report: {e}"
def action_next(spec_path: str, args_agent: str = None, no_commit: bool = False):
"""Manually advance to next step (checks review results before advancing)"""
fix_plan = load_fix_plan(spec_path)
state = fix_plan["state"]
current_step = state["step"]
print(f"⏭️ Ralph Loop | Advancing from '{current_step}' to next step")
if current_step in ["complete", "failed"]:
print(f" Final state: {current_step}")
print(" Cannot advance further.")
return
# Define standard step transitions
step_transitions = {
"init": "choose_task",
"choose_task": "implementation",
"implementation": "review",
"fix": "review",
"cleanup": "sync",
"sync": "update_done",
"update_done": "choose_task",
}
# Special handling for review step - check results before advancing
if current_step == "review":
current_task = state.get("current_task")
retry_count = state.get("retry_count", 0)
max_retries = 3
if not current_task:
print(" ⚠️ No current task, returning to choose_task")
state["step"] = "choose_task"
save_fix_plan(spec_path, fix_plan)
return
max_file_retries = 3
# === PRE-VALIDATION: structural check before semantic review ===
file_valid, file_error = validate_review_file(spec_path, current_task)
if not file_valid:
# Do NOT increment retry_count — this is not a review failure
review_file_retry = state.get("review_file_retry", 0) + 1
state["review_file_retry"] = review_file_retry
state["review_file_error"] = file_error
print(f" ⚠️ Review file invalid (attempt {review_file_retry}/{max_file_retries}): {file_error}")
if review_file_retry >= max_file_retries:
print(f" ❌ Max file retries ({max_file_retries}) exceeded - review file never created correctly")
state["step"] = "failed"
state["error"] = f"Review file validation failed after {max_file_retries} attempts: {file_error}"
else:
print(f" 🔄 Staying on 'review' — agent must create/fix the review file")
# state["step"] remains "review" — no assignment needed
save_fix_plan(spec_path, fix_plan)
print(f"")
print("Run loop to see next command:")
print(f" python3 ralph_loop.py --action=loop --spec={spec_path}")
return
# File is structurally valid — reset file retry counters
state["review_file_retry"] = 0
state["review_file_error"] = None
# === SEMANTIC CHECK: check review content ===
review_passed, reason = check_review_result(spec_path, current_task)
if review_passed:
print(f" ✅ Review passed: {reason}")
print(f" Advanced: review → cleanup")
state["step"] = "cleanup"
state["retry_count"] = 0
save_fix_plan(spec_path, fix_plan)
else:
print(f" ❌ Review failed: {reason}")
retry_count += 1
state["retry_count"] = retry_count
if retry_count >= max_retries:
print(f" ❌ Max retries ({max_retries}) exceeded")
print(f" Advanced: review → failed")
state["step"] = "failed"
state["error"] = f"Review failed after {max_retries} retries: {reason}"
save_fix_plan(spec_path, fix_plan)
else:
print(f" 🔄 Retry {retry_count}/{max_retries}")
print(f" Advanced: review → fix")
state["step"] = "fix"
save_fix_plan(spec_path, fix_plan)
print(f"")
print("Run loop to see next command:")
print(f" python3 ralph_loop.py --action=loop --spec={spec_path}")
return
# Standard transition for other steps
next_step = step_transitions.get(current_step)
if not next_step:
print(f" Unknown step: {current_step}")
return
# Special handling for update_done - need to mark task complete
if current_step == "update_done":
handle_update_done(spec_path, fix_plan, get_agent_config(fix_plan, args_agent), no_commit)
return
# Update state
state["step"] = next_step
save_fix_plan(spec_path, fix_plan)
print(f" Advanced: {current_step} → {next_step}")
print(f"")
print("Run loop to see next command:")
print(f" python3 ralph_loop.py --action=loop --spec={spec_path}")
def main():
args = parse_args()
# Validate spec folder structure
if not validate_spec_path(args.spec):
print(f"❌ Invalid spec folder: {args.spec}")
print(f" Expected format: docs/specs/[ID-feature]/ or [ID-feature]/")
print(f" Folder must exist and contain spec files or tasks/ directory")
sys.exit(1)
# Ensure _ralph_loop directory exists for clean state
spec_dir = Path(args.spec).resolve()
ralph_dir = spec_dir / "_ralph_loop"
if not ralph_dir.exists():
ralph_dir.mkdir(parents=True, exist_ok=True)
if args.action == "start":
action_start(args.spec, args.from_task, args.to_task, args.agent)
elif args.action == "loop":
action_loop(args.spec, args.agent, args.no_commit)
elif args.action == "status":
action_status(args.spec)
elif args.action == "resume":
action_resume(args.spec, args.agent, args.no_commit)
elif args.action == "next":
action_next(args.spec, args.agent, args.no_commit)
elif args.action == "next":
action_next(args.spec, args.agent, args.no_commit)
if __name__ == "__main__":
main()
Related skills
FAQ
What files does ralph-loop manage?
ralph-loop organizes docs/specs directories with a spec markdown file, a tasks markdown file, per-task TASK-###.md files, and a JSON state tracking currentTaskIndex, dependencies, and implement/review/sync status.
When should ralph-loop run?
ralph-loop fits long-running agent projects where tasks have dependencies and need review or sync steps. Use it when context loss or skipped ordering would break multi-step implementations.
Is Ralph Loop safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.