|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# Ralph Loop stop hook. |
| 4 | +# When the agent finishes a turn, this hook decides whether to feed the |
| 5 | +# same prompt back for another iteration or let the session end. |
| 6 | +# |
| 7 | +# Cursor stop hook API: |
| 8 | +# Input: { "status": "completed"|"aborted"|"error", "loop_count": N, ...common } |
| 9 | +# Output: { "followup_message": "<text>" } to continue, or exit 0 with no output to stop |
| 10 | + |
| 11 | +set -euo pipefail |
| 12 | + |
| 13 | +HOOK_INPUT=$(cat) |
| 14 | + |
| 15 | +PROJECT_DIR="${CURSOR_PROJECT_DIR:-.}" |
| 16 | +STATE_FILE="$PROJECT_DIR/.cursor/ralph/scratchpad.md" |
| 17 | +DONE_FLAG="$PROJECT_DIR/.cursor/ralph/done" |
| 18 | + |
| 19 | +# No active loop. Let the session end. |
| 20 | +if [[ ! -f "$STATE_FILE" ]]; then |
| 21 | + exit 0 |
| 22 | +fi |
| 23 | + |
| 24 | +# Parse state file frontmatter |
| 25 | +FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE") |
| 26 | +ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//') |
| 27 | +MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//') |
| 28 | +COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/') |
| 29 | + |
| 30 | +# Validate iteration is numeric |
| 31 | +if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then |
| 32 | + echo "Ralph loop: state file corrupted (iteration: '$ITERATION'). Stopping." >&2 |
| 33 | + rm -f "$STATE_FILE" "$DONE_FLAG" |
| 34 | + exit 0 |
| 35 | +fi |
| 36 | + |
| 37 | +if [[ ! "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then |
| 38 | + echo "Ralph loop: state file corrupted (max_iterations: '$MAX_ITERATIONS'). Stopping." >&2 |
| 39 | + rm -f "$STATE_FILE" "$DONE_FLAG" |
| 40 | + exit 0 |
| 41 | +fi |
| 42 | + |
| 43 | +# Check if completion promise was detected by the afterAgentResponse hook |
| 44 | +if [[ -f "$DONE_FLAG" ]]; then |
| 45 | + echo "Ralph loop: completion promise fulfilled at iteration $ITERATION." >&2 |
| 46 | + rm -f "$STATE_FILE" "$DONE_FLAG" |
| 47 | + exit 0 |
| 48 | +fi |
| 49 | + |
| 50 | +# Check max iterations |
| 51 | +if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then |
| 52 | + echo "Ralph loop: max iterations ($MAX_ITERATIONS) reached." >&2 |
| 53 | + rm -f "$STATE_FILE" "$DONE_FLAG" |
| 54 | + exit 0 |
| 55 | +fi |
| 56 | + |
| 57 | +# Extract prompt text (everything after the closing --- in frontmatter) |
| 58 | +PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$STATE_FILE") |
| 59 | + |
| 60 | +if [[ -z "$PROMPT_TEXT" ]]; then |
| 61 | + echo "Ralph loop: no prompt text found in state file. Stopping." >&2 |
| 62 | + rm -f "$STATE_FILE" "$DONE_FLAG" |
| 63 | + exit 0 |
| 64 | +fi |
| 65 | + |
| 66 | +# Increment iteration |
| 67 | +NEXT_ITERATION=$((ITERATION + 1)) |
| 68 | +TEMP_FILE="${STATE_FILE}.tmp.$$" |
| 69 | +sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$STATE_FILE" > "$TEMP_FILE" |
| 70 | +mv "$TEMP_FILE" "$STATE_FILE" |
| 71 | + |
| 72 | +# Build the followup message: iteration context + original prompt |
| 73 | +if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then |
| 74 | + HEADER="[Ralph loop iteration $NEXT_ITERATION. To complete: output <promise>$COMPLETION_PROMISE</promise> ONLY when genuinely true.]" |
| 75 | +else |
| 76 | + HEADER="[Ralph loop iteration $NEXT_ITERATION.]" |
| 77 | +fi |
| 78 | + |
| 79 | +FOLLOWUP="$HEADER |
| 80 | +
|
| 81 | +$PROMPT_TEXT" |
| 82 | + |
| 83 | +# Output followup_message to continue the loop |
| 84 | +jq -n --arg msg "$FOLLOWUP" '{"followup_message": $msg}' |
| 85 | + |
| 86 | +exit 0 |
0 commit comments