Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 43 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

Agentic looping for Cursor IDE. Keeps the agent working on a task until it's done (or hits a safety limit).

This is a quick port of the "Not-quite-Ralph" loop from the [ralph-wiggum plugin](https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum) for Claude Code. Uses `osascript` on macOS to work around Cursor's 5-iteration stop hook limit.

> **macOS only.** Linux/Windows PRs welcome.
This is a quick port of the "Not-quite-Ralph" loop from the [ralph-wiggum plugin](https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum) for Claude Code.

## What's a Ralph Loop?

Expand All @@ -16,7 +14,7 @@ This implementation isn't the "true" Ralph loop (which uses more sophisticated s

1. Clone this repo somewhere:
```bash
git clone https://github.com/youruser/cursor-ralph.git ~/.cursor-ralph
git clone https://github.com/hexsprite/cursor-ralph.git ~/.cursor-ralph
```

2. Symlink the command into your Cursor commands directory:
Expand All @@ -25,20 +23,49 @@ This implementation isn't the "true" Ralph loop (which uses more sophisticated s
ln -s ~/.cursor-ralph/commands/ralph.md ~/.cursor/commands/ralph.md
```

3. Add the stop hook to your Cursor settings (`~/.cursor/settings.json`):
3. Register the stop hook in **`.cursor/hooks.json`** (project root) or your user hooks config. **You must set `loop_limit`: null** so Cursor does not stop the loop after 5 iterations:

```json
{
"version": 1,
"hooks": {
"stop": [
{
"command": "~/.cursor-ralph/hooks/ralph-loop-stop.sh"
"command": "~/.cursor-ralph/hooks/ralph-loop-stop.sh",
"loop_limit": null
}
]
}
}
```

4. Grant Accessibility permissions to Cursor (System Settings → Privacy & Security → Accessibility). Required for the `osascript` workaround.
See [`hooks/hooks.json.example`](hooks/hooks.json.example) for a copy-paste template.

4. Make hook scripts executable:
```bash
chmod +x ~/.cursor-ralph/hooks/ralph-loop-stop.sh
```

### Linux notes

- Works on Linux with **`loop_limit`: null** — no macOS `osascript` required.
- Requires **`bash`**, **`jq`**, and **`grep`** on PATH.
- Optional legacy fallback: [`hooks/continue-linux.sh`](hooks/continue-linux.sh) simulates typing `/ralph-loop --continue <trace_id>` if you cannot set `loop_limit: null`. Install **`xdotool`** (X11) or **`ydotool`** / **`wtype`** (Wayland).


### Windows notes

- Works on Windows with **`loop_limit`: null** — no macOS `osascript` required.
- Hook scripts are **bash**; use **Git Bash** (bundled with Git for Windows) or ensure bash is on PATH when Cursor runs hooks.
- Use forward slashes or escaped backslashes in `hooks.json` command paths, e.g. `"C:/Users/you/.cursor-ralph/hooks/ralph-loop-stop.sh"`.
- Requires **`jq`** in Git Bash (`winget install jqlang.jq` or download from [jqlang.org](https://jqlang.org/)).
- Optional legacy fallback: [`hooks/continue-windows.ps1`](hooks/continue-windows.ps1) uses PowerShell `SendKeys` when `loop_limit: null` cannot be set. Run via Git Bash: `powershell.exe -File hooks/continue-windows.ps1 "/ralph-loop --continue <trace_id>"`.
- Known Cursor quirk: stop-hook JSON may show as `{}` in Hooks Execution Log on Windows even when valid ([forum thread](https://forum.cursor.com/t/stop-hook-followup-message-not-captured-on-windows-execution-log-shows-despite-valid-json-on-stdout/)); verify behavior in chat, not only the log.

### macOS notes

- Grant Accessibility permissions to Cursor if you use the optional keyboard fallback (System Settings → Privacy & Security → Accessibility).
- With `loop_limit: null`, keyboard simulation is not needed.

## Usage

Expand Down Expand Up @@ -83,15 +110,14 @@ This implementation isn't the "true" Ralph loop (which uses more sophisticated s
│ Stop hook runs after agent response │
│ ├─ Check for completion promise → Done? Clean up & exit │
│ ├─ Check max iterations → Hit limit? Clean up & exit │
│ ├─ Session < 5? → Return followup_message to continue │
│ └─ Session = 5? → osascript types new /ralph-loop command │
│ └─ Return followup_message to continue (loop_limit: null) │
└─────────────────────────────────────────────────────────────┘
(loop continues)
```

The key trick: Cursor limits `followup_message` chains to 5 iterations. When we hit that limit, the stop hook spawns an `osascript` process that waits 1.5 seconds then simulates typing `/ralph-loop --continue <trace_id>` into Cursor. This starts a "new" user message, resetting Cursor's internal counter while preserving our loop state.
Cursor limits `followup_message` chains to **5 iterations by default**. Setting **`loop_limit`: null** on the stop hook removes that cap ([Cursor hooks docs](https://cursor.com/docs/hooks.md)). The previous macOS-only `osascript` workaround is no longer required when this is configured.

## State File

Expand All @@ -103,30 +129,28 @@ Loop state is stored in `/tmp/cursor-ralph-loop-<trace_id>.json`:
"max_iterations": 20,
"completion_promise": "COMPLETE",
"iterations": 7,
"session_iterations": 2,
"stop": false,
"last_output": "Added 3 test files, coverage now at 74%"
}
```

## Requirements

- **macOS** (uses `osascript` for keyboard simulation)
- **jq** (`brew install jq`)
- **Cursor** with Accessibility permissions
- Cursor window must be focused when the session limit is hit
- **Cursor** with project or user **`hooks.json`**
- **`loop_limit`: null** on the Ralph stop hook entry
- **`bash`**, **`jq`**, **`grep`**
- On Windows: **Git Bash** (or another environment where bash hooks run)

## Limitations

- macOS only — `osascript` doesn't exist on Linux/Windows
- Requires Cursor to be focused when session limit hits
- If `osascript` fails, loop stops at iteration 5 (you can manually continue)
- The 1.5s delay between sessions is a bit janky but necessary for reliability
- Without `loop_limit: null`, the loop still stops after 5 iterations on every OS.
- Keyboard fallback scripts require the Cursor window to be focused and OS-specific tooling (optional; not needed with `loop_limit: null`).

## Credits

- Original Ralph Wiggum technique by [Geoffrey Huntley](https://ghuntley.com/ralph/)
- Based on the [ralph-wiggum plugin](https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum) for Claude Code
- Cross-platform hook fix inspired by [ericzakariasson/ralph-loop-plugin](https://github.com/ericzakariasson/ralph-loop-plugin)
- This port by Jordan Baker

## License
Expand Down
51 changes: 14 additions & 37 deletions commands/ralph.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ralph Wiggum Loop Command

Iterative refinement loop that keeps working until complete. The **stop hook** controls iteration and uses `osascript` to bypass Cursor's 5-iteration limit.
Iterative refinement loop that keeps working until complete. The **stop hook** controls iteration via Cursor's `followup_message` API.

## Usage

Expand All @@ -12,7 +12,7 @@ Iterative refinement loop that keeps working until complete. The **stop hook** c
- **`<prompt>`** (required) — Task description
- **`--max-iterations <n>`** — Safety limit (default: 20)
- **`--completion-promise "<text>"`** — Completion signal (default: "COMPLETE")
- **`--continue <trace_id>`** — Resume from existing state (used by osascript auto-continuation)
- **`--continue <trace_id>`** — Resume from existing state (legacy fallback when keyboard continuation is used)

**If no prompt provided**: Print error with usage and stop.

Expand All @@ -29,7 +29,6 @@ Iterative refinement loop that keeps working until complete. The **stop hook** c
"max_iterations": 20,
"completion_promise": "COMPLETE",
"iterations": 0,
"session_iterations": 0,
"stop": false,
"last_output": ""
}
Expand All @@ -41,7 +40,6 @@ Iterative refinement loop that keeps working until complete. The **stop hook** c

1. **Read existing state file** using provided trace_id
2. **Resume working** on the saved prompt
3. Session iteration counter is already reset by the stop hook

## During Work

Expand All @@ -57,38 +55,16 @@ jq --arg out "summary of what you did" '.last_output = $out' "$STATE_FILE" > tmp

## How the Loop Works

The stop hook (`scripts/ralph-loop-stop.sh`) runs after **every** agent response:
The stop hook (`hooks/ralph-loop-stop.sh`) runs after **every** agent response:

1. Increments iteration counter and session counter
1. Increments iteration counter
2. Checks if completion promise found in `last_output`
3. Checks if max iterations reached
4. If session hits 5 (Cursor's limit) but total < max:
- Resets session counter
- Spawns `osascript` to type `/ralph-loop --continue <trace_id>`
- New "user message" resets Cursor's internal limit
5. If not at session limit → returns `followup_message` to continue
6. If done → cleans up state file and exits
4. Otherwise returns `followup_message` to continue the loop

**User clicking Stop** sets `stop: true` in state file — hook detects this and exits.

## Example

```
User: /ralph-loop "Add tests until 80% coverage" --max-iterations 15

Iterations 1-4: Normal followup_message continuation
Iteration 5: Session limit hit
→ osascript types: /ralph-loop --continue abc123
→ New session starts
**Required hook config:** set `"loop_limit": null` on the stop hook entry in `hooks.json` (see `hooks/hooks.json.example`). Without this, Cursor caps automatic follow-ups at 5 iterations on all platforms.

Iterations 6-9: Normal continuation
Iteration 10: Session limit hit again
→ osascript continues...

Iteration 12: Coverage reaches 82%
→ Agent outputs: COMPLETE
→ Hook cleans up, loop ends
```
**User clicking Stop** sets `stop: true` in state file — hook detects this and exits.

## Key Rules

Expand All @@ -98,11 +74,12 @@ Iteration 12: Coverage reaches 82%
4. **Don't loop yourself** — The hook handles iteration automatically
5. **For `--continue`** — Read state from provided trace_id, not CURSOR_TRACE_ID

## macOS Requirement
## Linux / Windows

Works on Linux and Windows when:

The osascript auto-continuation requires:
- macOS (osascript is Mac-only)
- Accessibility permissions for System Events
- Cursor window must be focused when session limit hits
- The stop hook is registered with `"loop_limit": null` (primary fix)
- **`bash`**, **`jq`**, and **`grep`** are on PATH (Git Bash on Windows)
- Hook scripts are executable (`chmod +x hooks/*.sh`)

If osascript fails, the loop stops at iteration 5 and you can manually continue.
Optional keyboard fallback scripts (`continue-linux.sh`, `continue-windows.ps1`) exist for legacy installs that cannot set `loop_limit: null`. Prefer the hooks.json fix.
53 changes: 53 additions & 0 deletions hooks/continue-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Fallback continuation for Linux when stop-hook followup_message hits Cursor's
# default loop_limit (5). Prefer setting "loop_limit": null in hooks.json instead.
#
# Usage: continue-linux.sh "/ralph-loop --continue <trace_id>"
#
# Requires one of: xdotool (X11), ydotool (Wayland/uinput), or wtype (Wayland).

set -euo pipefail

COMMAND_TEXT="${1:-}"
if [[ -z "$COMMAND_TEXT" ]]; then
echo "continue-linux.sh: missing command text" >&2
exit 1
fi

sleep 1.5

type_text() {
local text="$1"
if command -v xdotool >/dev/null 2>&1; then
xdotool type --delay 12 -- "$text"
xdotool key Return
return 0
fi
if command -v ydotool >/dev/null 2>&1; then
ydotool type --delay 12 -- "$text"
ydotool key enter
return 0
fi
if command -v wtype >/dev/null 2>&1; then
wtype -d 12 -- "$text"
wtype -k enter
return 0
fi
return 1
}

focus_cursor() {
if command -v xdotool >/dev/null 2>&1; then
xdotool search --onlyvisible --class cursor windowactivate 2>/dev/null \
|| xdotool search --onlyvisible --class Cursor windowactivate 2>/dev/null \
|| true
fi
}

focus_cursor
sleep 0.3

if ! type_text "$COMMAND_TEXT"; then
echo "continue-linux.sh: install xdotool (X11), ydotool, or wtype (Wayland), or set loop_limit: null in hooks.json" >&2
exit 1
fi
44 changes: 44 additions & 0 deletions hooks/continue-windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Fallback continuation for Windows when stop-hook followup_message hits Cursor's
default loop_limit (5). Prefer setting "loop_limit": null in hooks.json instead.

.PARAMETER CommandText
Full slash command to type, e.g. "/ralph-loop --continue abc123"
#>
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$CommandText
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Start-Sleep -Seconds 1.5

Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class RalphWin32 {
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
}
"@

$cursor = [RalphWin32]::FindWindow([string]::Empty, 'Cursor')
if ($cursor -ne [IntPtr]::Zero) {
[void][RalphWin32]::SetForegroundWindow($cursor)
Start-Sleep -Milliseconds 300
}

$wshell = New-Object -ComObject WScript.Shell
if (-not $wshell.AppActivate('Cursor')) {
Write-Error 'Could not focus Cursor. Bring Cursor to the foreground or set loop_limit: null in hooks.json.'
}

Start-Sleep -Milliseconds 300
$wshell.SendKeys($CommandText)
$wshell.SendKeys('{ENTER}')
11 changes: 11 additions & 0 deletions hooks/hooks.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 1,
"hooks": {
"stop": [
{
"command": "./hooks/ralph-loop-stop.sh",
"loop_limit": null
}
]
}
}
Loading