-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathralph.sh
More file actions
executable file
·333 lines (284 loc) · 13.3 KB
/
ralph.sh
File metadata and controls
executable file
·333 lines (284 loc) · 13.3 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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/bin/bash
# Ralph Wiggum - Long-running AI agent loop
# Usage: ./ralph.sh [--help] [--once] [--max-iterations N] [N]
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
PRD_FILE="$SCRIPT_DIR/.agent/prd/PRD.md"
PROGRESS_FILE="$SCRIPT_DIR/.agent/logs/LOG.md"
HISTORY_DIR="$SCRIPT_DIR/.agent/history"
source "$SCRIPT_DIR/scripts/lib/constants.sh"
source "$SCRIPT_DIR/scripts/lib/logging.sh"
source "$SCRIPT_DIR/scripts/lib/preflight.sh"
source "$SCRIPT_DIR/scripts/lib/timing.sh"
source "$SCRIPT_DIR/scripts/lib/terminal.sh"
source "$SCRIPT_DIR/scripts/lib/spinner.sh"
source "$SCRIPT_DIR/scripts/lib/preview.sh"
source "$SCRIPT_DIR/scripts/lib/output.sh"
source "$SCRIPT_DIR/scripts/lib/cleanup.sh"
source "$SCRIPT_DIR/scripts/lib/promise.sh"
source "$SCRIPT_DIR/scripts/lib/notify.sh"
source "$SCRIPT_DIR/scripts/lib/display.sh"
source "$SCRIPT_DIR/scripts/lib/args.sh"
# Timing
START_TIME=$(date +%s)
ITERATION_TIMES=()
TOTAL_ITERATION_TIME=0
PREV_ITERATION_TIME=0
# Session ID for unique history file naming (YYYYMMDD-HHMMSS format). This is used to prevent overwrites between runs.
SESSION_ID=$(date +%Y%m%d-%H%M%S)
# Temporary files for spinner communication
STEP_FILE=$(mktemp)
PREVIEW_LINE_FILE=$(mktemp)
# Background process tracking for cleanup
AGENT_PID=""
OUTPUT_FILE=""
FULL_OUTPUT_FILE=""
# Set up traps
trap cleanup EXIT
trap handle_interrupt INT
# Parse arguments (sets MAX_ITERATIONS, ONCE_FLAG)
parse_arguments "$@"
# 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 "---" >> "$PROGRESS_FILE"
fi
# Pre-flight checks
check_git_repo
check_required_files
check_history_dir
check_ansi_support
show_ralph
echo -e " ${C}Starting Ralph${R} ・ ${Y}v$VERSION${R} ・ Max iterations: ${Y}$MAX_ITERATIONS${R}"
echo ""
for i in $(seq 1 $MAX_ITERATIONS); do
ITERATION_START=$(date +%s)
# Initialize step timing for this iteration
init_iteration_step_times
echo -e "${B}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
echo -e " ↪ ${R}Iteration ${Y}$i${R} of ${Y}$MAX_ITERATIONS${R}"
echo -e "${B}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
echo -e ""
# Run Agent with the ralph prompt (prepend PROJECT_ROOT)
PROMPT_CONTENT="PROJECT_ROOT=$SCRIPT_DIR
$(cat $SCRIPT_DIR/.agent/PROMPT.md)"
# Start spinner
start_spinner
# Initialize rolling preview
init_rolling_preview
# Run Agent and capture output while updating spinner and preview
OUTPUT_FILE=$(mktemp)
FULL_OUTPUT_FILE=$(mktemp)
# Use script to provide pseudo-TTY for docker sandbox.
# This is the main command loop.
export PROMPT_CONTENT
export DOCKER_DEFAULT_PLATFORM=linux/amd64 # Needed for Playwright.
script -q "$OUTPUT_FILE" bash -c 'docker sandbox run claude . -- --effort max --model opus --output-format stream-json --verbose -p "$PROMPT_CONTENT"' >/dev/null 2>&1 &
AGENT_PID=$!
# Track position in output file for incremental reading
LAST_POS=0
# Monitor output and update spinner step and rolling preview
while kill -0 "$AGENT_PID" 2>/dev/null; do
if [ -f "$OUTPUT_FILE" ]; then
# Get current file size
CURRENT_SIZE=$(stat -f%z "$OUTPUT_FILE" 2>/dev/null || stat -c%s "$OUTPUT_FILE" 2>/dev/null || echo "0")
# Read new content if file has grown
if [ "$CURRENT_SIZE" -gt "$LAST_POS" ]; then
# Read new lines
while IFS= read -r line; do
if [ -n "$line" ]; then
# Parse JSON and extract text content
parsed=$(parse_json_content "$line")
if [ -n "$parsed" ]; then
# Save to full output
echo "$parsed" >> "$FULL_OUTPUT_FILE"
# Update spinner step
update_spinner_step "$parsed"
# Update preview line under spinner
update_preview_line "$parsed"
fi
fi
done < <(tail -c +$((LAST_POS + 1)) "$OUTPUT_FILE" 2>/dev/null)
LAST_POS=$CURRENT_SIZE
fi
fi
sleep 0.2 || true
done
wait "$AGENT_PID" || true
AGENT_PID="" # Clear PID after process exits
# Process any remaining output
if [ -f "$OUTPUT_FILE" ]; then
CURRENT_SIZE=$(stat -f%z "$OUTPUT_FILE" 2>/dev/null || stat -c%s "$OUTPUT_FILE" 2>/dev/null || echo "0")
if [ "$CURRENT_SIZE" -gt "$LAST_POS" ]; then
while IFS= read -r line; do
if [ -n "$line" ]; then
parsed=$(parse_json_content "$line")
if [ -n "$parsed" ]; then
echo "$parsed" >> "$FULL_OUTPUT_FILE"
fi
fi
done < <(tail -c +$((LAST_POS + 1)) "$OUTPUT_FILE" 2>/dev/null)
fi
fi
OUTPUT=$(cat "$FULL_OUTPUT_FILE" 2>/dev/null || cat "$OUTPUT_FILE")
# Check for Docker daemon not ready error
if echo "$OUTPUT" | grep -q "docker daemon not ready"; then
stop_spinner
clear_rolling_preview
echo ""
echo -e "${RD}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
echo -e " ❌ ${RD}Docker Error${R}"
echo -e " Docker daemon is not ready. Please ensure Docker is running."
echo -e "${RD}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
rm -f "$OUTPUT_FILE" "$FULL_OUTPUT_FILE"
exit $EXIT_DOCKER_ERROR
fi
# Check for invalid API key / authentication error
if echo "$OUTPUT" | grep -q "Invalid API key"; then
stop_spinner
clear_rolling_preview
echo ""
echo -e "${RD}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
echo -e " ❌ ${RD}Authentication Error${R}"
echo -e " Invalid API key. Please authenticate inside the Docker sandbox."
echo -e ""
echo -e " Run the following command and follow the login instructions:"
echo -e " ${C}docker sandbox run claude . --${R}"
echo -e "${RD}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
rm -f "$OUTPUT_FILE" "$FULL_OUTPUT_FILE"
exit $EXIT_AUTH_ERROR
fi
# Save cleaned output to history file (SESSION_ID prevents overwrites between runs)
# Strip ANSI control characters for clean, readable history
HISTORY_FILE="$HISTORY_DIR/ITERATION-${SESSION_ID}-${i}.txt"
strip_ansi_file "$OUTPUT_FILE" "$HISTORY_FILE"
# Extract final summary before removing files
FINAL_SUMMARY=$(extract_final_summary "$OUTPUT_FILE")
rm -f "$FULL_OUTPUT_FILE"
FULL_OUTPUT_FILE="" # Clear after removal
# Stop spinner
stop_spinner
# Finalize step timing for this iteration (record time spent in last step)
record_step_time ""
# Clear rolling preview area to make room for summary
clear_rolling_preview
# Display the final summary (persists after iteration)
if [ -n "$FINAL_SUMMARY" ]; then
display_final_summary "$FINAL_SUMMARY" 10
else
# Fallback: show last 10 lines of parsed output if no result found
FALLBACK_SUMMARY=$(echo "$OUTPUT" | tail -n 10)
if [ -n "$FALLBACK_SUMMARY" ]; then
display_final_summary "$FALLBACK_SUMMARY" 10
fi
fi
# Clean up the raw output file (history file already saved)
rm -f "$OUTPUT_FILE"
OUTPUT_FILE="" # Clear after removal
# Calculate iteration duration
ITERATION_END=$(date +%s)
ITERATION_DURATION=$((ITERATION_END - ITERATION_START))
ITERATION_TIMES+=($ITERATION_DURATION)
TOTAL_ITERATION_TIME=$((TOTAL_ITERATION_TIME + ITERATION_DURATION))
ITERATION_AVG=$((TOTAL_ITERATION_TIME / ${#ITERATION_TIMES[@]}))
ITERATION_STR=$(format_duration $ITERATION_DURATION)
AVG_STR=$(format_duration $ITERATION_AVG)
DELTA_STR=$(format_delta $ITERATION_DURATION $PREV_ITERATION_TIME)
PREV_ITERATION_TIME=$ITERATION_DURATION
# Check for completion signal
# Note: We check both $OUTPUT and $FINAL_SUMMARY because the JSON parsing
# in parse_json_content truncates text at escaped quotes, but FINAL_SUMMARY
# is correctly extracted using jq from the result message
if has_complete_tag "$OUTPUT" || has_complete_tag "$FINAL_SUMMARY"; then
ELAPSED=$(($(date +%s) - START_TIME))
ELAPSED_STR=$(format_duration $ELAPSED)
echo ""
echo -e "${GR}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
echo -e " 🎉 ${GR}Ralph completed all tasks!${R}"
echo -e " ✅ Finished at iteration ${GR}$i${R} of ${GR}$MAX_ITERATIONS${R}"
if [ -n "$DELTA_STR" ]; then
echo -e " ⏱️ Iteration $i: ${Y}$ITERATION_STR${R} ($DELTA_STR) ${C}│${R} Average: ${Y}$AVG_STR${R}"
else
echo -e " ⏱️ Iteration $i: ${Y}$ITERATION_STR${R} ${C}│${R} Average: ${Y}$AVG_STR${R}"
fi
echo -e " ⏱️ Total time: ${Y}$ELAPSED_STR${R}"
display_session_step_totals
echo -e "${GR}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
exit $EXIT_COMPLETE
fi
# Check for BLOCKED tag - agent needs human help
# Check both $OUTPUT and $FINAL_SUMMARY (see completion signal comment above)
if has_blocked_tag "$OUTPUT" || has_blocked_tag "$FINAL_SUMMARY"; then
BLOCKED_REASON=$(extract_blocked_reason "$OUTPUT")
# If not found in OUTPUT, try FINAL_SUMMARY
[ -z "$BLOCKED_REASON" ] && BLOCKED_REASON=$(extract_blocked_reason "$FINAL_SUMMARY")
ELAPSED=$(($(date +%s) - START_TIME))
ELAPSED_STR=$(format_duration $ELAPSED)
play_notification_sound
show_notification "Ralph - BLOCKED" "$BLOCKED_REASON"
display_blocked_message "$BLOCKED_REASON" "$i"
if [ -n "$DELTA_STR" ]; then
echo -e " ⏱️ Iteration $i: ${Y}$ITERATION_STR${R} ($DELTA_STR) ${C}│${R} Average: ${Y}$AVG_STR${R}"
else
echo -e " ⏱️ Iteration $i: ${Y}$ITERATION_STR${R} ${C}│${R} Average: ${Y}$AVG_STR${R}"
fi
echo -e " ⏱️ Total time: ${Y}$ELAPSED_STR${R}"
display_session_step_totals
exit $EXIT_BLOCKED
fi
# Check for DECIDE tag - agent needs human decision
# Check both $OUTPUT and $FINAL_SUMMARY (see completion signal comment above)
if has_decide_tag "$OUTPUT" || has_decide_tag "$FINAL_SUMMARY"; then
DECIDE_QUESTION=$(extract_decide_question "$OUTPUT")
# If not found in OUTPUT, try FINAL_SUMMARY
[ -z "$DECIDE_QUESTION" ] && DECIDE_QUESTION=$(extract_decide_question "$FINAL_SUMMARY")
ELAPSED=$(($(date +%s) - START_TIME))
ELAPSED_STR=$(format_duration $ELAPSED)
play_notification_sound
show_notification "Ralph - Decision Needed" "$DECIDE_QUESTION"
display_decide_message "$DECIDE_QUESTION" "$i"
if [ -n "$DELTA_STR" ]; then
echo -e " ⏱️ Iteration $i: ${Y}$ITERATION_STR${R} ($DELTA_STR) ${C}│${R} Average: ${Y}$AVG_STR${R}"
else
echo -e " ⏱️ Iteration $i: ${Y}$ITERATION_STR${R} ${C}│${R} Average: ${Y}$AVG_STR${R}"
fi
echo -e " ⏱️ Total time: ${Y}$ELAPSED_STR${R}"
display_session_step_totals
exit $EXIT_DECIDE
fi
# Calculate elapsed time
ELAPSED=$(($(date +%s) - START_TIME))
ELAPSED_STR=$(format_duration $ELAPSED)
if [ -n "$DELTA_STR" ]; then
echo -e "${G} └── ✓ Iteration $i complete${R} ${C}│${R} Iteration: ${Y}$ITERATION_STR${R} ($DELTA_STR) ${C}│${R} Average: ${Y}$AVG_STR${R} ${C}│${R} Total: ${Y}$ELAPSED_STR${R}"
else
echo -e "${G} └── ✓ Iteration $i complete${R} ${C}│${R} Iteration: ${Y}$ITERATION_STR${R} ${C}│${R} Average: ${Y}$AVG_STR${R} ${C}│${R} Total: ${Y}$ELAPSED_STR${R}"
fi
# Display per-iteration step times
STEP_TIMES_OUTPUT=$(format_step_times "ITERATION")
if [ -n "$STEP_TIMES_OUTPUT" ]; then
echo -e "${G} └──${R} $STEP_TIMES_OUTPUT"
fi
sleep 2 || true
done
# Calculate final elapsed time
ELAPSED=$(($(date +%s) - START_TIME))
ELAPSED_STR=$(format_duration $ELAPSED)
# Calculate final average (if any iterations completed)
if [ ${#ITERATION_TIMES[@]} -gt 0 ]; then
FINAL_AVG=$((TOTAL_ITERATION_TIME / ${#ITERATION_TIMES[@]}))
FINAL_AVG_STR=$(format_duration $FINAL_AVG)
fi
echo ""
echo -e "${Y}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
echo -e " ⚠️ ${Y}Ralph reached max iterations${R} (${M}$MAX_ITERATIONS${R})"
if [ ${#ITERATION_TIMES[@]} -gt 0 ]; then
echo -e " ⏱️ Average iteration time: ${Y}$FINAL_AVG_STR${R}"
fi
echo -e " ⏱️ Total time: ${Y}$ELAPSED_STR${R}"
display_session_step_totals
echo -e " 📋 Check progress: ${G}$PROGRESS_FILE${R}"
echo -e "${Y}░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░${R}"
exit $EXIT_MAX_ITERATIONS