Add cross-platform date utility library to handle differences between GNU date (Linux) and BSD date (macOS). Fixes issues with: - ISO 8601 timestamp formatting (-Iseconds flag) - Date arithmetic operations (-d vs -v flags) Changes: - Created lib/date_utils.sh with get_iso_timestamp() and get_next_hour_time() - Updated ralph_loop.sh to use date utilities (2 instances) - Updated lib/circuit_breaker.sh to use date utilities (4 instances) - Updated lib/response_analyzer.sh to use date utilities (1 instance) All date operations now work consistently across both platforms without modification. The utility automatically detects the OS and uses the appropriate date command syntax. Tested on Linux with GNU date - all syntax checks and integration tests pass.
700 lines
25 KiB
Bash
Executable file
700 lines
25 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
# Claude Code Ralph Loop with Rate Limiting and Documentation
|
|
# Adaptation of the Ralph technique for Claude Code with usage management
|
|
|
|
set -e # Exit on any error
|
|
|
|
# Source library components
|
|
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
|
source "$SCRIPT_DIR/lib/date_utils.sh"
|
|
source "$SCRIPT_DIR/lib/response_analyzer.sh"
|
|
source "$SCRIPT_DIR/lib/circuit_breaker.sh"
|
|
|
|
# Configuration
|
|
PROMPT_FILE="PROMPT.md"
|
|
LOG_DIR="logs"
|
|
DOCS_DIR="docs/generated"
|
|
STATUS_FILE="status.json"
|
|
PROGRESS_FILE="progress.json"
|
|
CLAUDE_CODE_CMD="claude"
|
|
MAX_CALLS_PER_HOUR=100 # Adjust based on your plan
|
|
VERBOSE_PROGRESS=false # Default: no verbose progress updates
|
|
CLAUDE_TIMEOUT_MINUTES=15 # Default: 15 minutes timeout for Claude Code execution
|
|
SLEEP_DURATION=3600 # 1 hour in seconds
|
|
CALL_COUNT_FILE=".call_count"
|
|
TIMESTAMP_FILE=".last_reset"
|
|
USE_TMUX=false
|
|
|
|
# Exit detection configuration
|
|
EXIT_SIGNALS_FILE=".exit_signals"
|
|
MAX_CONSECUTIVE_TEST_LOOPS=3
|
|
MAX_CONSECUTIVE_DONE_SIGNALS=2
|
|
TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
|
|
|
|
# Colors for terminal output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
PURPLE='\033[0;35m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Initialize directories
|
|
mkdir -p "$LOG_DIR" "$DOCS_DIR"
|
|
|
|
# Check if tmux is available
|
|
check_tmux_available() {
|
|
if ! command -v tmux &> /dev/null; then
|
|
log_status "ERROR" "tmux is not installed. Please install tmux or run without --monitor flag."
|
|
echo "Install tmux:"
|
|
echo " Ubuntu/Debian: sudo apt-get install tmux"
|
|
echo " macOS: brew install tmux"
|
|
echo " CentOS/RHEL: sudo yum install tmux"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Setup tmux session with monitor
|
|
setup_tmux_session() {
|
|
local session_name="ralph-$(date +%s)"
|
|
local ralph_home="${RALPH_HOME:-$HOME/.ralph}"
|
|
|
|
log_status "INFO" "Setting up tmux session: $session_name"
|
|
|
|
# Create new tmux session detached
|
|
tmux new-session -d -s "$session_name" -c "$(pwd)"
|
|
|
|
# Split window vertically to create monitor pane on the right
|
|
tmux split-window -h -t "$session_name" -c "$(pwd)"
|
|
|
|
# Start monitor in the right pane
|
|
if command -v ralph-monitor &> /dev/null; then
|
|
tmux send-keys -t "$session_name:0.1" "ralph-monitor" Enter
|
|
else
|
|
tmux send-keys -t "$session_name:0.1" "'$ralph_home/ralph_monitor.sh'" Enter
|
|
fi
|
|
|
|
# Start ralph loop in the left pane (exclude tmux flag to avoid recursion)
|
|
local ralph_cmd
|
|
if command -v ralph &> /dev/null; then
|
|
ralph_cmd="ralph"
|
|
else
|
|
ralph_cmd="'$ralph_home/ralph_loop.sh'"
|
|
fi
|
|
|
|
if [[ "$MAX_CALLS_PER_HOUR" != "100" ]]; then
|
|
ralph_cmd="$ralph_cmd --calls $MAX_CALLS_PER_HOUR"
|
|
fi
|
|
if [[ "$PROMPT_FILE" != "PROMPT.md" ]]; then
|
|
ralph_cmd="$ralph_cmd --prompt '$PROMPT_FILE'"
|
|
fi
|
|
|
|
tmux send-keys -t "$session_name:0.0" "$ralph_cmd" Enter
|
|
|
|
# Focus on left pane (main ralph loop)
|
|
tmux select-pane -t "$session_name:0.0"
|
|
|
|
# Set window title
|
|
tmux rename-window -t "$session_name:0" "Ralph: Loop | Monitor"
|
|
|
|
log_status "SUCCESS" "Tmux session created. Attaching to session..."
|
|
log_status "INFO" "Use Ctrl+B then D to detach from session"
|
|
log_status "INFO" "Use 'tmux attach -t $session_name' to reattach"
|
|
|
|
# Attach to session (this will block until session ends)
|
|
tmux attach-session -t "$session_name"
|
|
|
|
exit 0
|
|
}
|
|
|
|
# Initialize call tracking
|
|
init_call_tracking() {
|
|
log_status "INFO" "DEBUG: Entered init_call_tracking..."
|
|
local current_hour=$(date +%Y%m%d%H)
|
|
local last_reset_hour=""
|
|
|
|
if [[ -f "$TIMESTAMP_FILE" ]]; then
|
|
last_reset_hour=$(cat "$TIMESTAMP_FILE")
|
|
fi
|
|
|
|
# Reset counter if it's a new hour
|
|
if [[ "$current_hour" != "$last_reset_hour" ]]; then
|
|
echo "0" > "$CALL_COUNT_FILE"
|
|
echo "$current_hour" > "$TIMESTAMP_FILE"
|
|
log_status "INFO" "Call counter reset for new hour: $current_hour"
|
|
fi
|
|
|
|
# Initialize exit signals tracking if it doesn't exist
|
|
if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
|
|
echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
|
|
fi
|
|
|
|
# Initialize circuit breaker
|
|
init_circuit_breaker
|
|
|
|
log_status "INFO" "DEBUG: Completed init_call_tracking successfully"
|
|
}
|
|
|
|
# Log function with timestamps and colors
|
|
log_status() {
|
|
local level=$1
|
|
local message=$2
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local color=""
|
|
|
|
case $level in
|
|
"INFO") color=$BLUE ;;
|
|
"WARN") color=$YELLOW ;;
|
|
"ERROR") color=$RED ;;
|
|
"SUCCESS") color=$GREEN ;;
|
|
"LOOP") color=$PURPLE ;;
|
|
esac
|
|
|
|
echo -e "${color}[$timestamp] [$level] $message${NC}"
|
|
echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
|
|
}
|
|
|
|
# Update status JSON for external monitoring
|
|
update_status() {
|
|
local loop_count=$1
|
|
local calls_made=$2
|
|
local last_action=$3
|
|
local status=$4
|
|
local exit_reason=${5:-""}
|
|
|
|
cat > "$STATUS_FILE" << STATUSEOF
|
|
{
|
|
"timestamp": "$(get_iso_timestamp)",
|
|
"loop_count": $loop_count,
|
|
"calls_made_this_hour": $calls_made,
|
|
"max_calls_per_hour": $MAX_CALLS_PER_HOUR,
|
|
"last_action": "$last_action",
|
|
"status": "$status",
|
|
"exit_reason": "$exit_reason",
|
|
"next_reset": "$(get_next_hour_time)"
|
|
}
|
|
STATUSEOF
|
|
}
|
|
|
|
# Check if we can make another call
|
|
can_make_call() {
|
|
local calls_made=0
|
|
if [[ -f "$CALL_COUNT_FILE" ]]; then
|
|
calls_made=$(cat "$CALL_COUNT_FILE")
|
|
fi
|
|
|
|
if [[ $calls_made -ge $MAX_CALLS_PER_HOUR ]]; then
|
|
return 1 # Cannot make call
|
|
else
|
|
return 0 # Can make call
|
|
fi
|
|
}
|
|
|
|
# Increment call counter
|
|
increment_call_counter() {
|
|
local calls_made=0
|
|
if [[ -f "$CALL_COUNT_FILE" ]]; then
|
|
calls_made=$(cat "$CALL_COUNT_FILE")
|
|
fi
|
|
|
|
((calls_made++))
|
|
echo "$calls_made" > "$CALL_COUNT_FILE"
|
|
echo "$calls_made"
|
|
}
|
|
|
|
# Wait for rate limit reset with countdown
|
|
wait_for_reset() {
|
|
local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
|
|
log_status "WARN" "Rate limit reached ($calls_made/$MAX_CALLS_PER_HOUR). Waiting for reset..."
|
|
|
|
# Calculate time until next hour
|
|
local current_minute=$(date +%M)
|
|
local current_second=$(date +%S)
|
|
local wait_time=$(((60 - current_minute - 1) * 60 + (60 - current_second)))
|
|
|
|
log_status "INFO" "Sleeping for $wait_time seconds until next hour..."
|
|
|
|
# Countdown display
|
|
while [[ $wait_time -gt 0 ]]; do
|
|
local hours=$((wait_time / 3600))
|
|
local minutes=$(((wait_time % 3600) / 60))
|
|
local seconds=$((wait_time % 60))
|
|
|
|
printf "\r${YELLOW}Time until reset: %02d:%02d:%02d${NC}" $hours $minutes $seconds
|
|
sleep 1
|
|
((wait_time--))
|
|
done
|
|
printf "\n"
|
|
|
|
# Reset counter
|
|
echo "0" > "$CALL_COUNT_FILE"
|
|
echo "$(date +%Y%m%d%H)" > "$TIMESTAMP_FILE"
|
|
log_status "SUCCESS" "Rate limit reset! Ready for new calls."
|
|
}
|
|
|
|
# Check if we should gracefully exit
|
|
should_exit_gracefully() {
|
|
log_status "INFO" "DEBUG: Checking exit conditions..." >&2
|
|
|
|
if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
|
|
log_status "INFO" "DEBUG: No exit signals file found, continuing..." >&2
|
|
return 1 # Don't exit, file doesn't exist
|
|
fi
|
|
|
|
local signals=$(cat "$EXIT_SIGNALS_FILE")
|
|
log_status "INFO" "DEBUG: Exit signals content: $signals" >&2
|
|
|
|
# Count recent signals (last 5 loops) - with error handling
|
|
local recent_test_loops
|
|
local recent_done_signals
|
|
local recent_completion_indicators
|
|
|
|
recent_test_loops=$(echo "$signals" | jq '.test_only_loops | length' 2>/dev/null || echo "0")
|
|
recent_done_signals=$(echo "$signals" | jq '.done_signals | length' 2>/dev/null || echo "0")
|
|
recent_completion_indicators=$(echo "$signals" | jq '.completion_indicators | length' 2>/dev/null || echo "0")
|
|
|
|
log_status "INFO" "DEBUG: Exit counts - test_loops:$recent_test_loops, done_signals:$recent_done_signals, completion:$recent_completion_indicators" >&2
|
|
|
|
# Check for exit conditions
|
|
|
|
# 1. Too many consecutive test-only loops
|
|
if [[ $recent_test_loops -ge $MAX_CONSECUTIVE_TEST_LOOPS ]]; then
|
|
log_status "WARN" "Exit condition: Too many test-focused loops ($recent_test_loops >= $MAX_CONSECUTIVE_TEST_LOOPS)"
|
|
echo "test_saturation"
|
|
return 0
|
|
fi
|
|
|
|
# 2. Multiple "done" signals
|
|
if [[ $recent_done_signals -ge $MAX_CONSECUTIVE_DONE_SIGNALS ]]; then
|
|
log_status "WARN" "Exit condition: Multiple completion signals ($recent_done_signals >= $MAX_CONSECUTIVE_DONE_SIGNALS)"
|
|
echo "completion_signals"
|
|
return 0
|
|
fi
|
|
|
|
# 3. Strong completion indicators
|
|
if [[ $recent_completion_indicators -ge 2 ]]; then
|
|
log_status "WARN" "Exit condition: Strong completion indicators ($recent_completion_indicators)"
|
|
echo "project_complete"
|
|
return 0
|
|
fi
|
|
|
|
# 4. Check fix_plan.md for completion
|
|
if [[ -f "@fix_plan.md" ]]; then
|
|
local total_items=$(grep -c "^- \[" "@fix_plan.md" 2>/dev/null)
|
|
local completed_items=$(grep -c "^- \[x\]" "@fix_plan.md" 2>/dev/null)
|
|
|
|
# Handle case where grep returns no matches (exit code 1)
|
|
[[ -z "$total_items" ]] && total_items=0
|
|
[[ -z "$completed_items" ]] && completed_items=0
|
|
|
|
log_status "INFO" "DEBUG: @fix_plan.md check - total_items:$total_items, completed_items:$completed_items" >&2
|
|
|
|
if [[ $total_items -gt 0 ]] && [[ $completed_items -eq $total_items ]]; then
|
|
log_status "WARN" "Exit condition: All fix_plan.md items completed ($completed_items/$total_items)" >&2
|
|
echo "plan_complete"
|
|
return 0
|
|
fi
|
|
else
|
|
log_status "INFO" "DEBUG: @fix_plan.md file not found" >&2
|
|
fi
|
|
|
|
log_status "INFO" "DEBUG: No exit conditions met, continuing loop" >&2
|
|
echo "" # Return empty string instead of using return code
|
|
}
|
|
|
|
# Main execution function
|
|
execute_claude_code() {
|
|
local timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
|
|
local output_file="$LOG_DIR/claude_output_${timestamp}.log"
|
|
local loop_count=$1
|
|
local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
|
|
calls_made=$((calls_made + 1))
|
|
|
|
log_status "LOOP" "Executing Claude Code (Call $calls_made/$MAX_CALLS_PER_HOUR)"
|
|
local timeout_seconds=$((CLAUDE_TIMEOUT_MINUTES * 60))
|
|
log_status "INFO" "⏳ Starting Claude Code execution... (timeout: ${CLAUDE_TIMEOUT_MINUTES}m)"
|
|
|
|
# Execute Claude Code with the prompt, streaming output
|
|
if timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1 &
|
|
then
|
|
local claude_pid=$!
|
|
local progress_counter=0
|
|
|
|
# Show progress while Claude Code is running
|
|
while kill -0 $claude_pid 2>/dev/null; do
|
|
progress_counter=$((progress_counter + 1))
|
|
case $((progress_counter % 4)) in
|
|
1) progress_indicator="⠋" ;;
|
|
2) progress_indicator="⠙" ;;
|
|
3) progress_indicator="⠹" ;;
|
|
0) progress_indicator="⠸" ;;
|
|
esac
|
|
|
|
# Get last line from output if available
|
|
local last_line=""
|
|
if [[ -f "$output_file" && -s "$output_file" ]]; then
|
|
last_line=$(tail -1 "$output_file" 2>/dev/null | head -c 80)
|
|
fi
|
|
|
|
# Update progress file for monitor
|
|
cat > "$PROGRESS_FILE" << EOF
|
|
{
|
|
"status": "executing",
|
|
"indicator": "$progress_indicator",
|
|
"elapsed_seconds": $((progress_counter * 10)),
|
|
"last_output": "$last_line",
|
|
"timestamp": "$(date '+%Y-%m-%d %H:%M:%S')"
|
|
}
|
|
EOF
|
|
|
|
# Only log if verbose mode is enabled
|
|
if [[ "$VERBOSE_PROGRESS" == "true" ]]; then
|
|
if [[ -n "$last_line" ]]; then
|
|
log_status "INFO" "$progress_indicator Claude Code: $last_line... (${progress_counter}0s)"
|
|
else
|
|
log_status "INFO" "$progress_indicator Claude Code working... (${progress_counter}0s elapsed)"
|
|
fi
|
|
fi
|
|
|
|
sleep 10
|
|
done
|
|
|
|
# Wait for the process to finish and get exit code
|
|
wait $claude_pid
|
|
local exit_code=$?
|
|
|
|
if [ $exit_code -eq 0 ]; then
|
|
# Only increment counter on successful execution
|
|
echo "$calls_made" > "$CALL_COUNT_FILE"
|
|
|
|
# Clear progress file
|
|
echo '{"status": "completed", "timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"}' > "$PROGRESS_FILE"
|
|
|
|
log_status "SUCCESS" "✅ Claude Code execution completed successfully"
|
|
|
|
# Analyze the response
|
|
log_status "INFO" "🔍 Analyzing Claude Code response..."
|
|
analyze_response "$output_file" "$loop_count"
|
|
local analysis_exit_code=$?
|
|
|
|
# Update exit signals based on analysis
|
|
update_exit_signals
|
|
|
|
# Log analysis summary
|
|
log_analysis_summary
|
|
|
|
# Get file change count for circuit breaker
|
|
local files_changed=$(git diff --name-only 2>/dev/null | wc -l || echo 0)
|
|
local has_errors="false"
|
|
|
|
# Two-stage error detection to avoid JSON field false positives
|
|
# Stage 1: Filter out JSON field patterns like "is_error": false
|
|
# Stage 2: Look for actual error messages in specific contexts
|
|
# Avoid type annotations like "error: Error" by requiring lowercase after ": error"
|
|
if grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
|
|
grep -qE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)'; then
|
|
has_errors="true"
|
|
|
|
# Debug logging: show what triggered error detection
|
|
if [[ "$VERBOSE_PROGRESS" == "true" ]]; then
|
|
log_status "DEBUG" "Error patterns found:"
|
|
grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
|
|
grep -nE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)' | \
|
|
head -3 | while IFS= read -r line; do
|
|
log_status "DEBUG" " $line"
|
|
done
|
|
fi
|
|
|
|
log_status "WARN" "Errors detected in output, check: $output_file"
|
|
fi
|
|
local output_length=$(wc -c < "$output_file" 2>/dev/null || echo 0)
|
|
|
|
# Record result in circuit breaker
|
|
record_loop_result "$loop_count" "$files_changed" "$has_errors" "$output_length"
|
|
local circuit_result=$?
|
|
|
|
if [[ $circuit_result -ne 0 ]]; then
|
|
log_status "WARN" "Circuit breaker opened - halting execution"
|
|
return 3 # Special code for circuit breaker trip
|
|
fi
|
|
|
|
return 0
|
|
else
|
|
# Clear progress file on failure
|
|
echo '{"status": "failed", "timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"}' > "$PROGRESS_FILE"
|
|
|
|
# Check if the failure is due to API 5-hour limit
|
|
if grep -qi "5.*hour.*limit\|limit.*reached.*try.*back\|usage.*limit.*reached" "$output_file"; then
|
|
log_status "ERROR" "🚫 Claude API 5-hour usage limit reached"
|
|
return 2 # Special return code for API limit
|
|
else
|
|
log_status "ERROR" "❌ Claude Code execution failed, check: $output_file"
|
|
return 1
|
|
fi
|
|
fi
|
|
else
|
|
log_status "ERROR" "❌ Failed to start Claude Code process"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Cleanup function
|
|
cleanup() {
|
|
log_status "INFO" "Ralph loop interrupted. Cleaning up..."
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
|
|
exit 0
|
|
}
|
|
|
|
# Set up signal handlers
|
|
trap cleanup SIGINT SIGTERM
|
|
|
|
# Global variable for loop count (needed by cleanup function)
|
|
loop_count=0
|
|
|
|
# Main loop
|
|
main() {
|
|
|
|
log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code"
|
|
log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
|
|
log_status "INFO" "Logs: $LOG_DIR/ | Docs: $DOCS_DIR/ | Status: $STATUS_FILE"
|
|
|
|
# Check if this is a Ralph project directory
|
|
if [[ ! -f "$PROMPT_FILE" ]]; then
|
|
log_status "ERROR" "Prompt file '$PROMPT_FILE' not found!"
|
|
echo ""
|
|
|
|
# Check if this looks like a partial Ralph project
|
|
if [[ -f "@fix_plan.md" ]] || [[ -d "specs" ]] || [[ -f "@AGENT.md" ]]; then
|
|
echo "This appears to be a Ralph project but is missing PROMPT.md."
|
|
echo "You may need to create or restore the PROMPT.md file."
|
|
else
|
|
echo "This directory is not a Ralph project."
|
|
fi
|
|
|
|
echo ""
|
|
echo "To fix this:"
|
|
echo " 1. Create a new project: ralph-setup my-project"
|
|
echo " 2. Import existing requirements: ralph-import requirements.md"
|
|
echo " 3. Navigate to an existing Ralph project directory"
|
|
echo " 4. Or create PROMPT.md manually in this directory"
|
|
echo ""
|
|
echo "Ralph projects should contain: PROMPT.md, @fix_plan.md, specs/, src/, etc."
|
|
exit 1
|
|
fi
|
|
|
|
log_status "INFO" "Starting main loop..."
|
|
log_status "INFO" "DEBUG: About to enter while loop, loop_count=$loop_count"
|
|
|
|
while true; do
|
|
loop_count=$((loop_count + 1))
|
|
log_status "INFO" "DEBUG: Successfully incremented loop_count to $loop_count"
|
|
log_status "INFO" "Loop #$loop_count - calling init_call_tracking..."
|
|
init_call_tracking
|
|
|
|
log_status "LOOP" "=== Starting Loop #$loop_count ==="
|
|
|
|
# Check circuit breaker before attempting execution
|
|
if should_halt_execution; then
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
|
|
log_status "ERROR" "🛑 Circuit breaker has opened - execution halted"
|
|
break
|
|
fi
|
|
|
|
# Check rate limits
|
|
if ! can_make_call; then
|
|
wait_for_reset
|
|
continue
|
|
fi
|
|
|
|
# Check for graceful exit conditions
|
|
local exit_reason=$(should_exit_gracefully)
|
|
if [[ "$exit_reason" != "" ]]; then
|
|
log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
|
|
|
|
log_status "SUCCESS" "🎉 Ralph has completed the project! Final stats:"
|
|
log_status "INFO" " - Total loops: $loop_count"
|
|
log_status "INFO" " - API calls used: $(cat "$CALL_COUNT_FILE")"
|
|
log_status "INFO" " - Exit reason: $exit_reason"
|
|
|
|
break
|
|
fi
|
|
|
|
# Update status
|
|
local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
|
|
update_status "$loop_count" "$calls_made" "executing" "running"
|
|
|
|
# Execute Claude Code
|
|
execute_claude_code "$loop_count"
|
|
local exec_result=$?
|
|
|
|
if [ $exec_result -eq 0 ]; then
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
|
|
|
|
# Brief pause between successful executions
|
|
sleep 5
|
|
elif [ $exec_result -eq 3 ]; then
|
|
# Circuit breaker opened
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
|
|
log_status "ERROR" "🛑 Circuit breaker has opened - halting loop"
|
|
log_status "INFO" "Run 'ralph --reset-circuit' to reset the circuit breaker after addressing issues"
|
|
break
|
|
elif [ $exec_result -eq 2 ]; then
|
|
# API 5-hour limit reached - handle specially
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "api_limit" "paused"
|
|
log_status "WARN" "🛑 Claude API 5-hour limit reached!"
|
|
|
|
# Ask user whether to wait or exit
|
|
echo -e "\n${YELLOW}The Claude API 5-hour usage limit has been reached.${NC}"
|
|
echo -e "${YELLOW}You can either:${NC}"
|
|
echo -e " ${GREEN}1)${NC} Wait for the limit to reset (usually within an hour)"
|
|
echo -e " ${GREEN}2)${NC} Exit the loop and try again later"
|
|
echo -e "\n${BLUE}Choose an option (1 or 2):${NC} "
|
|
|
|
# Read user input with timeout
|
|
read -t 30 -n 1 user_choice
|
|
echo # New line after input
|
|
|
|
if [[ "$user_choice" == "2" ]] || [[ -z "$user_choice" ]]; then
|
|
log_status "INFO" "User chose to exit (or timed out). Exiting loop..."
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "api_limit_exit" "stopped" "api_5hour_limit"
|
|
break
|
|
else
|
|
log_status "INFO" "User chose to wait. Waiting for API limit reset..."
|
|
# Wait for longer period when API limit is hit
|
|
local wait_minutes=60
|
|
log_status "INFO" "Waiting $wait_minutes minutes before retrying..."
|
|
|
|
# Countdown display
|
|
local wait_seconds=$((wait_minutes * 60))
|
|
while [[ $wait_seconds -gt 0 ]]; do
|
|
local minutes=$((wait_seconds / 60))
|
|
local seconds=$((wait_seconds % 60))
|
|
printf "\r${YELLOW}Time until retry: %02d:%02d${NC}" $minutes $seconds
|
|
sleep 1
|
|
((wait_seconds--))
|
|
done
|
|
printf "\n"
|
|
fi
|
|
else
|
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error"
|
|
log_status "WARN" "Execution failed, waiting 30 seconds before retry..."
|
|
sleep 30
|
|
fi
|
|
|
|
log_status "LOOP" "=== Completed Loop #$loop_count ==="
|
|
done
|
|
}
|
|
|
|
# Help function
|
|
show_help() {
|
|
cat << HELPEOF
|
|
Ralph Loop for Claude Code
|
|
|
|
Usage: $0 [OPTIONS]
|
|
|
|
IMPORTANT: This command must be run from a Ralph project directory.
|
|
Use 'ralph-setup project-name' to create a new project first.
|
|
|
|
Options:
|
|
-h, --help Show this help message
|
|
-c, --calls NUM Set max calls per hour (default: $MAX_CALLS_PER_HOUR)
|
|
-p, --prompt FILE Set prompt file (default: $PROMPT_FILE)
|
|
-s, --status Show current status and exit
|
|
-m, --monitor Start with tmux session and live monitor (requires tmux)
|
|
-v, --verbose Show detailed progress updates during execution
|
|
-t, --timeout MIN Set Claude Code execution timeout in minutes (default: $CLAUDE_TIMEOUT_MINUTES)
|
|
--reset-circuit Reset circuit breaker to CLOSED state
|
|
--circuit-status Show circuit breaker status and exit
|
|
|
|
Files created:
|
|
- $LOG_DIR/: All execution logs
|
|
- $DOCS_DIR/: Generated documentation
|
|
- $STATUS_FILE: Current status (JSON)
|
|
|
|
Example workflow:
|
|
ralph-setup my-project # Create project
|
|
cd my-project # Enter project directory
|
|
$0 --monitor # Start Ralph with monitoring
|
|
|
|
Examples:
|
|
$0 --calls 50 --prompt my_prompt.md
|
|
$0 --monitor # Start with integrated tmux monitoring
|
|
$0 --monitor --timeout 30 # 30-minute timeout for complex tasks
|
|
$0 --verbose --timeout 5 # 5-minute timeout with detailed progress
|
|
|
|
HELPEOF
|
|
}
|
|
|
|
# Parse command line arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
-c|--calls)
|
|
MAX_CALLS_PER_HOUR="$2"
|
|
shift 2
|
|
;;
|
|
-p|--prompt)
|
|
PROMPT_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-s|--status)
|
|
if [[ -f "$STATUS_FILE" ]]; then
|
|
echo "Current Status:"
|
|
cat "$STATUS_FILE" | jq . 2>/dev/null || cat "$STATUS_FILE"
|
|
else
|
|
echo "No status file found. Ralph may not be running."
|
|
fi
|
|
exit 0
|
|
;;
|
|
-m|--monitor)
|
|
USE_TMUX=true
|
|
shift
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE_PROGRESS=true
|
|
shift
|
|
;;
|
|
-t|--timeout)
|
|
if [[ "$2" =~ ^[1-9][0-9]*$ ]] && [[ "$2" -le 120 ]]; then
|
|
CLAUDE_TIMEOUT_MINUTES="$2"
|
|
else
|
|
echo "Error: Timeout must be a positive integer between 1 and 120 minutes"
|
|
exit 1
|
|
fi
|
|
shift 2
|
|
;;
|
|
--reset-circuit)
|
|
# Source the circuit breaker library
|
|
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
|
source "$SCRIPT_DIR/lib/circuit_breaker.sh"
|
|
reset_circuit_breaker "Manual reset via command line"
|
|
exit 0
|
|
;;
|
|
--circuit-status)
|
|
# Source the circuit breaker library
|
|
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
|
source "$SCRIPT_DIR/lib/circuit_breaker.sh"
|
|
show_circuit_status
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# If tmux mode requested, set it up
|
|
if [[ "$USE_TMUX" == "true" ]]; then
|
|
check_tmux_available
|
|
setup_tmux_session
|
|
fi
|
|
|
|
# Start the main loop
|
|
main
|