forked from snarktank/ralph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathralph.sh
More file actions
executable file
·229 lines (192 loc) · 8.39 KB
/
ralph.sh
File metadata and controls
executable file
·229 lines (192 loc) · 8.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#!/bin/bash
# Ralph Wiggum - Long-running AI agent loop
# Usage: ./ralph.sh [max_iterations]
#
# Supports multiple AI providers via RALPH_PROVIDER env var:
# amp (default), claude-code, antigravity, codex
#
# Example: RALPH_PROVIDER=claude-code ./ralph.sh 10
set -e
set -o pipefail
MAX_ITERATIONS=${1:-${RALPH_MAX_ITERATIONS:-10}}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRD_FILE="$SCRIPT_DIR/prd.json"
PROGRESS_FILE="$SCRIPT_DIR/progress.txt"
ARCHIVE_DIR="$SCRIPT_DIR/archive"
LAST_BRANCH_FILE="$SCRIPT_DIR/.last-branch"
STATE_DIR="$SCRIPT_DIR/.agent"
SCRATCHPAD_FILE="$STATE_DIR/scratchpad.md"
LAST_SESSION_FILE="$STATE_DIR/last-session"
# Load provider configuration
# shellcheck source=config/ralph.config.sh
source "$SCRIPT_DIR/config/ralph.config.sh"
load_provider_config
# ------------------------------------------------------------------
# Config (override via env vars)
# ------------------------------------------------------------------
MAX_NO_PROGRESS="${RALPH_MAX_NO_PROGRESS:-3}"
MAX_CONSECUTIVE_ERRORS="${RALPH_MAX_CONSECUTIVE_ERRORS:-5}"
ITERATION_TIMEOUT_MINUTES="${RALPH_ITERATION_TIMEOUT_MINUTES:-60}"
SLEEP_SECONDS="${RALPH_SLEEP_SECONDS:-2}"
export RALPH_MODE="${RALPH_MODE:-production}" # playground | pair | production
export RALPH_AUTO_APPROVE="${RALPH_AUTO_APPROVE:-1}" # 1 = proceed after plan in autonomous mode
export RALPH_HUMANS_WRITE_TESTS="${RALPH_HUMANS_WRITE_TESTS:-1}" # 1 = never edit test files (default)
file_fingerprint() {
local filepath="$1"
if [ ! -f "$filepath" ]; then
echo "missing"
return 0
fi
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$filepath" | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$filepath" | awk '{print $1}'
return 0
fi
wc -c < "$filepath" | tr -d ' '
}
# Ensure state directory exists
mkdir -p "$STATE_DIR"
touch "$SCRATCHPAD_FILE"
# Archive previous run if branch changed
if [ -f "$PRD_FILE" ] && [ -f "$LAST_BRANCH_FILE" ]; then
CURRENT_BRANCH=$(jq -r '.branchName // empty' "$PRD_FILE" 2>/dev/null || echo "")
LAST_BRANCH=$(cat "$LAST_BRANCH_FILE" 2>/dev/null || echo "")
if [ -n "$CURRENT_BRANCH" ] && [ -n "$LAST_BRANCH" ] && [ "$CURRENT_BRANCH" != "$LAST_BRANCH" ]; then
DATE=$(date +%Y-%m-%d)
FOLDER_NAME=$(echo "$LAST_BRANCH" | sed 's|^ralph/||')
ARCHIVE_FOLDER="$ARCHIVE_DIR/$DATE-$FOLDER_NAME"
echo "Archiving previous run: $LAST_BRANCH"
mkdir -p "$ARCHIVE_FOLDER"
[ -f "$PRD_FILE" ] && cp "$PRD_FILE" "$ARCHIVE_FOLDER/"
[ -f "$PROGRESS_FILE" ] && cp "$PROGRESS_FILE" "$ARCHIVE_FOLDER/"
[ -f "$SCRATCHPAD_FILE" ] && cp "$SCRATCHPAD_FILE" "$ARCHIVE_FOLDER/scratchpad.md"
echo " Archived to: $ARCHIVE_FOLDER"
# Reset progress + scratchpad for new run
echo "# Ralph Progress Log" > "$PROGRESS_FILE"
echo "Started: $(date)" >> "$PROGRESS_FILE"
echo "---" >> "$PROGRESS_FILE"
: > "$SCRATCHPAD_FILE"
fi
fi
# Track current branch
if [ -f "$PRD_FILE" ]; then
CURRENT_BRANCH=$(jq -r '.branchName // empty' "$PRD_FILE" 2>/dev/null || echo "")
if [ -n "$CURRENT_BRANCH" ]; then
echo "$CURRENT_BRANCH" > "$LAST_BRANCH_FILE"
fi
fi
# Session lineage
export RALPH_PARENT_SESSION_ID=""
if [ -f "$LAST_SESSION_FILE" ]; then
RALPH_PARENT_SESSION_ID="$(cat "$LAST_SESSION_FILE" 2>/dev/null || echo "")"
fi
RAND_SUFFIX=$(( (RANDOM % 900) + 100 ))
if command -v shuf >/dev/null 2>&1; then
RAND_SUFFIX="$(shuf -i 100-999 -n 1)"
fi
export RALPH_SESSION_ID="run-$(date +%Y%m%d-%H%M%S)-$RAND_SUFFIX"
echo "$RALPH_SESSION_ID" > "$LAST_SESSION_FILE"
# Initialize progress file if it doesn't exist
if [ ! -f "$PROGRESS_FILE" ]; then
echo "# Ralph Progress Log" > "$PROGRESS_FILE"
echo "Started: $(date)" >> "$PROGRESS_FILE"
echo "Session: $RALPH_SESSION_ID" >> "$PROGRESS_FILE"
[ -n "$RALPH_PARENT_SESSION_ID" ] && echo "ParentSession: $RALPH_PARENT_SESSION_ID" >> "$PROGRESS_FILE"
echo "---" >> "$PROGRESS_FILE"
fi
echo "Starting Ralph - Max iterations: $MAX_ITERATIONS"
echo "Provider: $RALPH_PROVIDER_NAME ($RALPH_PROVIDER)"
if [ "$RALPH_SANDBOXED" = "1" ]; then
echo "🔒 Sandboxed Mode: ENABLED (using permission allowlists)"
else
echo "⚠️ Sandboxed Mode: DISABLED (full autonomy with dangerous permissions)"
fi
echo "Session ID: $RALPH_SESSION_ID"
[ -n "$RALPH_PARENT_SESSION_ID" ] && echo "Parent Session ID: $RALPH_PARENT_SESSION_ID"
echo "Mode: $RALPH_MODE (auto-approve=$RALPH_AUTO_APPROVE, humans-write-tests=$RALPH_HUMANS_WRITE_TESTS)"
# Circuit Breaker Initialization
NO_PROGRESS_COUNT=0
CONSECUTIVE_ERROR_COUNT=0
CURRENT_PROGRESS_FINGERPRINT="$(file_fingerprint "$PROGRESS_FILE")"
for i in $(seq 1 $MAX_ITERATIONS); do
export RALPH_ITERATION="$i"
echo ""
echo "═══════════════════════════════════════════════════════"
echo " Ralph Iteration $i of $MAX_ITERATIONS (Session: $RALPH_SESSION_ID)"
echo "═══════════════════════════════════════════════════════"
AGENT_STATUS=0
build_agent_command
OUTPUT=$(
envsubst '$RALPH_SESSION_ID $RALPH_PARENT_SESSION_ID $RALPH_ITERATION $RALPH_MODE $RALPH_AUTO_APPROVE $RALPH_HUMANS_WRITE_TESTS $RALPH_PROVIDER $RALPH_PROVIDER_NAME' \
< "$SCRIPT_DIR/prompt.md" \
| "${AGENT_CMD[@]}" 2>&1 | tee /dev/stderr
) || AGENT_STATUS=$?
# ------------------------------------------------------------------
# Circuit Breaker & Safety Checks
# ------------------------------------------------------------------
# 1. Stuck Loop Detection (No Progress)
NEW_PROGRESS_FINGERPRINT="$(file_fingerprint "$PROGRESS_FILE")"
if [ "$NEW_PROGRESS_FINGERPRINT" = "$CURRENT_PROGRESS_FINGERPRINT" ]; then
NO_PROGRESS_COUNT=$((NO_PROGRESS_COUNT + 1))
echo "⚠️ Warning: No progress detected in progress.txt (Counter: $NO_PROGRESS_COUNT/$MAX_NO_PROGRESS)"
else
NO_PROGRESS_COUNT=0
CURRENT_PROGRESS_FINGERPRINT="$NEW_PROGRESS_FINGERPRINT"
fi
if [ "$NO_PROGRESS_COUNT" -ge "$MAX_NO_PROGRESS" ]; then
echo "❌ CIRCUIT BREAKER TRIPPED: No progress for $MAX_NO_PROGRESS iterations."
echo "Stopping loop to prevent infinite retry cycles."
exit 1
fi
# 2. Error Burst Detection
if [ "$AGENT_STATUS" -ne 0 ]; then
CONSECUTIVE_ERROR_COUNT=$((CONSECUTIVE_ERROR_COUNT + 1))
echo "⚠️ Warning: $RALPH_PROVIDER_NAME exited with status $AGENT_STATUS (Counter: $CONSECUTIVE_ERROR_COUNT/$MAX_CONSECUTIVE_ERRORS)"
else
CONSECUTIVE_ERROR_COUNT=0
fi
if [ "$CONSECUTIVE_ERROR_COUNT" -ge "$MAX_CONSECUTIVE_ERRORS" ]; then
echo "❌ CIRCUIT BREAKER TRIPPED: $MAX_CONSECUTIVE_ERRORS consecutive $RALPH_PROVIDER_NAME errors."
echo "Stopping loop to avoid runaway failures."
exit 1
fi
# 3. Usage / Rate Limit Detection (pause required)
if echo "$OUTPUT" | grep -q -i -E "usage limit|5-?hour|rate limit|too many requests|429"; then
echo "⏸️ Detected rate/usage limit in output. Exiting so you can retry later."
exit 2
fi
# 4. Explicit Blocked Signal (human action required)
if echo "$OUTPUT" | grep -q "<promise>BLOCKED</promise>"; then
echo "⛔️ Ralph blocked: agent reported a hard blocker that requires human action."
exit 3
fi
# ------------------------------------------------------------------
# Intelligent Exit Detection:
# Require BOTH an explicit exit signal AND deterministic completion indicators.
HAS_COMPLETE_PROMISE=false
if echo "$OUTPUT" | grep -q "<promise>COMPLETE</promise>"; then
HAS_COMPLETE_PROMISE=true
fi
REMAINING_STORIES=""
if [ -f "$PRD_FILE" ]; then
REMAINING_STORIES="$(jq '[.userStories[] | select(.passes != true)] | length' "$PRD_FILE" 2>/dev/null || echo "")"
fi
if [ "$HAS_COMPLETE_PROMISE" = true ] && [ "$REMAINING_STORIES" = "0" ]; then
echo ""
echo "Ralph completed all tasks!"
echo "Completed at iteration $i of $MAX_ITERATIONS"
exit 0
fi
if [ "$HAS_COMPLETE_PROMISE" = true ] && [ -n "$REMAINING_STORIES" ] && [ "$REMAINING_STORIES" != "0" ]; then
echo "⚠️ Warning: <promise>COMPLETE</promise> seen but prd.json still has $REMAINING_STORIES remaining story(ies). Continuing."
fi
echo "Iteration $i complete. Continuing..."
sleep "$SLEEP_SECONDS"
done
echo ""
echo "Ralph reached max iterations ($MAX_ITERATIONS) without completing all tasks."
echo "Check $PROGRESS_FILE for status."
exit 1