#!/bin/bash # Ralph pentru Claude Code - Loop autonom de agent AI (W3: smart gates + DAG + rate limit) # Adaptat din Ralph original (snarktank/ralph) pentru Claude Code CLI # Usage: ./ralph.sh [max_iterations] [project_dir] # # Env vars (opționale): # RALPH_MAX_TURNS — --max-turns per iter (default 30) # RALPH_RATE_LIMIT_SLEEP — sleep după rate limit detection (default 1800 = 30min) # RALPH_DAG_HELPER — path la tools/ralph_dag.py (auto-detect default) # RALPH_PYTHON — interpreter Python pentru DAG helper (default python3) set -e MAX_ITERATIONS=${1:-10} PROJECT_DIR=${2:-$(pwd)} 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" SCREENSHOTS_DIR="$SCRIPT_DIR/screenshots" LAST_BRANCH_FILE="$SCRIPT_DIR/.last-branch" PROMPT_FILE="$SCRIPT_DIR/prompt.md" # W3 config MAX_TURNS=${RALPH_MAX_TURNS:-30} RATE_LIMIT_SLEEP=${RALPH_RATE_LIMIT_SLEEP:-1800} RALPH_PYTHON=${RALPH_PYTHON:-python3} # DAG helper auto-detect: prefer co-located cu echo-core; fallback la $SCRIPT_DIR if [ -n "$RALPH_DAG_HELPER" ] && [ -f "$RALPH_DAG_HELPER" ]; then DAG_HELPER="$RALPH_DAG_HELPER" elif [ -f "/home/moltbot/echo-core/tools/ralph_dag.py" ]; then DAG_HELPER="/home/moltbot/echo-core/tools/ralph_dag.py" elif [ -f "/home/moltbot/echo-core-qc/tools/ralph_dag.py" ]; then DAG_HELPER="/home/moltbot/echo-core-qc/tools/ralph_dag.py" elif [ -f "$SCRIPT_DIR/ralph_dag.py" ]; then DAG_HELPER="$SCRIPT_DIR/ralph_dag.py" else DAG_HELPER="" fi # Usage helper auto-detect (rate limit budget tracking — best effort, niciodată # blochează rularea Ralph dacă lipsește) if [ -n "$RALPH_USAGE_HELPER" ] && [ -f "$RALPH_USAGE_HELPER" ]; then USAGE_HELPER="$RALPH_USAGE_HELPER" elif [ -f "/home/moltbot/echo-core/tools/ralph_usage.py" ]; then USAGE_HELPER="/home/moltbot/echo-core/tools/ralph_usage.py" elif [ -f "/home/moltbot/echo-core-instr/tools/ralph_usage.py" ]; then USAGE_HELPER="/home/moltbot/echo-core-instr/tools/ralph_usage.py" elif [ -f "$SCRIPT_DIR/ralph_usage.py" ]; then USAGE_HELPER="$SCRIPT_DIR/ralph_usage.py" else USAGE_HELPER="" fi USAGE_FILE="$SCRIPT_DIR/usage.jsonl" # Verifică că jq este instalat if ! command -v jq &> /dev/null; then echo "Eroare: jq nu este instalat. Rulează: apt install jq" exit 1 fi # Verifică că claude este instalat if ! command -v claude &> /dev/null; then echo "Eroare: Claude Code CLI nu este instalat." echo "Instalează cu: npm install -g @anthropic-ai/claude-code" exit 1 fi # Verifică agent-browser (opțional, pentru verificări UI) if ! command -v agent-browser &> /dev/null; then echo "Notă: agent-browser nu este instalat." echo "Pentru verificări vizuale UI, instalează cu: npm install -g agent-browser && agent-browser install" echo "Continuăm fără verificări browser..." echo "" fi # Verifică existența fișierelor necesare if [ ! -f "$PRD_FILE" ]; then echo "Eroare: prd.json nu există în $SCRIPT_DIR" echo "Generează mai întâi un PRD folosind skill-ul /prd și apoi /ralph" exit 1 fi if [ ! -f "$PROMPT_FILE" ]; then echo "Eroare: prompt.md nu există în $SCRIPT_DIR" exit 1 fi # Arhivare rulare anterioară dacă branch-ul s-a schimbat 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 "Arhivare rulare anterioară: $LAST_BRANCH" mkdir -p "$ARCHIVE_FOLDER" [ -f "$PRD_FILE" ] && cp "$PRD_FILE" "$ARCHIVE_FOLDER/" [ -f "$PROGRESS_FILE" ] && cp "$PROGRESS_FILE" "$ARCHIVE_FOLDER/" echo " Arhivat în: $ARCHIVE_FOLDER" # Reset progress file echo "# Ralph Progress Log" > "$PROGRESS_FILE" echo "Started: $(date)" >> "$PROGRESS_FILE" echo "Branch: $CURRENT_BRANCH" >> "$PROGRESS_FILE" echo "---" >> "$PROGRESS_FILE" fi fi # Salvează branch-ul curent 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 # Creează directoare necesare mkdir -p "$SCRIPT_DIR/logs" "$SCRIPT_DIR/archive" "$SCRIPT_DIR/screenshots" # Creează .gitignore dacă nu există if [ ! -f "$PROJECT_DIR/.gitignore" ]; then cat > "$PROJECT_DIR/.gitignore" << 'GITIGNORE' # Python __pycache__/ *.py[cod] *.pyo *.egg-info/ dist/ build/ .coverage htmlcov/ .pytest_cache/ # Virtual environment venv/ .venv/ # Ralph runtime scripts/ralph/.ralph.pid scripts/ralph/.last-branch scripts/ralph/logs/ scripts/ralph/screenshots/ scripts/ralph/archive/ # IDE .vscode/ .idea/ # OS .DS_Store Thumbs.db GITIGNORE echo "Created .gitignore" fi # Inițializare progress file dacă nu există if [ ! -f "$PROGRESS_FILE" ]; then echo "# Ralph Progress Log" > "$PROGRESS_FILE" echo "Started: $(date)" >> "$PROGRESS_FILE" echo "---" >> "$PROGRESS_FILE" fi # Funcție pentru a verifica dacă toate story-urile sunt complete check_all_complete() { local incomplete=$(jq '[.userStories[] | select(.passes != true)] | length' "$PRD_FILE" 2>/dev/null || echo "999") [ "$incomplete" -eq 0 ] } # W3: nimic eligibil = toate sunt fie passes, fie failed, fie blocked check_no_eligible() { local n=$(jq '[.userStories[] | select(.passes != true and .failed != true and .blocked != true)] | length' "$PRD_FILE" 2>/dev/null || echo "999") [ "$n" -eq 0 ] } # W3: alege next eligible story via DAG helper. Print story ID sau "" dacă nimic. dag_next_story() { if [ -n "$DAG_HELPER" ]; then "$RALPH_PYTHON" "$DAG_HELPER" next-story "$PRD_FILE" 2>/dev/null || echo "" else # Fallback simplu (fără DAG): primul story cu passes!=true && failed!=true && blocked!=true, priority asc jq -r '[.userStories[] | select(.passes != true and .failed != true and .blocked != true)] | sort_by(.priority) | .[0].id // ""' "$PRD_FILE" fi } dag_incr_retry() { local sid="$1" if [ -n "$DAG_HELPER" ]; then "$RALPH_PYTHON" "$DAG_HELPER" incr-retry "$PRD_FILE" "$sid" 2>/dev/null || echo "0" else echo "0" fi } dag_mark_failed() { local sid="$1" reason="$2" if [ -n "$DAG_HELPER" ]; then "$RALPH_PYTHON" "$DAG_HELPER" mark-failed "$PRD_FILE" "$sid" "$reason" 2>/dev/null || true fi } dag_force_tags() { local sid="$1" if [ -n "$DAG_HELPER" ]; then "$RALPH_PYTHON" "$DAG_HELPER" force-tags "$PRD_FILE" "$sid" "$PROJECT_DIR" 2>/dev/null || true fi } # W3: detectează rate limit în output Claude (heuristic — Anthropic nu are exit code dedicat) is_rate_limited() { local output="$1" echo "$output" | grep -qiE "rate limit|rate_limit_exceeded|429|too many requests" } # Afișare status inițial echo "" echo "=======================================================================" echo " RALPH pentru Claude Code - Agent Autonom " echo "=======================================================================" PROJECT_NAME=$(jq -r '.projectName // "Unknown"' "$PRD_FILE") BRANCH_NAME=$(jq -r '.branchName // "N/A"' "$PRD_FILE") TOTAL_STORIES=$(jq '.userStories | length' "$PRD_FILE") COMPLETE_STORIES=$(jq '[.userStories[] | select(.passes == true)] | length' "$PRD_FILE") echo " Proiect: $PROJECT_NAME" echo " Branch: $BRANCH_NAME" echo " Stories: $COMPLETE_STORIES / $TOTAL_STORIES complete" echo " Max iterații: $MAX_ITERATIONS" echo " Screenshots: $SCREENSHOTS_DIR" echo "=======================================================================" echo "" # Verificare rapidă - poate toate sunt deja complete? if check_all_complete; then echo "Toate story-urile sunt deja complete!" exit 0 fi # Tracker pentru rate limit retry (max 1 retry de iterație-rate-limit per rulare) RATE_LIMIT_RETRY_USED=0 # Loop principal for i in $(seq 1 $MAX_ITERATIONS); do echo "" echo "===================================================================" echo " Ralph Iterația $i din $MAX_ITERATIONS" echo "===================================================================" # W3: alege next story via DAG (propagă blocked dacă vreun dep a eșuat) CURRENT_STORY=$(dag_next_story) if [ -z "$CURRENT_STORY" ]; then echo "" echo "===================================================================" if check_all_complete; then echo " TOATE STORY-URILE DIN PRD SUNT COMPLETE!" exit 0 else echo " NICIUN STORY ELIGIBIL (toate fie complete, fie failed, fie blocked)" echo " Stories incomplete:" jq -r '.userStories[] | select(.passes != true) | " - \(.id): \(.title) [failed=\(.failed // false) blocked=\(.blocked // false) retries=\(.retries // 0)]"' "$PRD_FILE" exit 0 fi fi # Status curent COMPLETE_NOW=$(jq '[.userStories[] | select(.passes == true)] | length' "$PRD_FILE") NEXT_TITLE=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | "\(.id): \(.title)"' "$PRD_FILE") STORY_TAGS=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | (.tags // []) | join(",")' "$PRD_FILE") STORY_RETRIES=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | (.retries // 0)' "$PRD_FILE") echo " Progress: $COMPLETE_NOW / $TOTAL_STORIES stories complete" echo " Next: $NEXT_TITLE [tags: ${STORY_TAGS:-}, retries: $STORY_RETRIES]" echo "" # Pregătește prompt-ul cu context FULL_PROMPT=$(cat <}) ## PRD (prd.json): $(cat "$PRD_FILE") ## Progress până acum (progress.txt): $(cat "$PROGRESS_FILE") ## Instrucțiuni pentru această iterație: $(cat "$PROMPT_FILE") EOF ) # Execută Claude Code în modul non-interactiv LOG_FILE="$SCRIPT_DIR/logs/iteration-$i-$(date +%Y%m%d-%H%M%S).log" mkdir -p "$SCRIPT_DIR/logs" # --output-format json + --max-turns pentru control runtime set +e echo "$FULL_PROMPT" | claude -p \ --dangerously-skip-permissions \ --output-format json \ --max-turns "$MAX_TURNS" \ 2>&1 | tee "$LOG_FILE" CLAUDE_EXIT=${PIPESTATUS[1]} set -e OUTPUT=$(cat "$LOG_FILE") # Rate limit budget tracking (best-effort, never blocks Ralph) if [ -n "$USAGE_HELPER" ]; then "$RALPH_PYTHON" "$USAGE_HELPER" append \ "$USAGE_FILE" "$LOG_FILE" \ --slug "$PROJECT_NAME" \ --story-id "$CURRENT_STORY" \ --iter "$i" 2>/dev/null || true fi # W3: rate limit detection (max 1 retry per rulare) if is_rate_limited "$OUTPUT" || [ "$CLAUDE_EXIT" = "29" ]; then if [ "$RATE_LIMIT_RETRY_USED" = "0" ]; then echo "" echo " ⏸️ Rate limit detectat. Sleep ${RATE_LIMIT_SLEEP}s, apoi retry o dată." RATE_LIMIT_RETRY_USED=1 echo "## Rate limit la iter $i — sleep $RATE_LIMIT_SLEEP" >> "$PROGRESS_FILE" sleep "$RATE_LIMIT_SLEEP" continue # retry aceeași iterație else echo " ❌ Rate limit din nou — abort run, mark $CURRENT_STORY rate_limited" dag_mark_failed "$CURRENT_STORY" "rate_limited" echo "## Rate limit final la iter $i — abort" >> "$PROGRESS_FILE" exit 2 fi fi # Verifică dacă toate task-urile sunt complete if echo "$OUTPUT" | grep -q "COMPLETE"; then echo "" echo "===================================================================" echo " RALPH A TERMINAT TOATE TASK-URILE!" echo " Completat la iterația $i din $MAX_ITERATIONS" echo "===================================================================" exit 0 fi # Verifică și prin prd.json if check_all_complete; then echo "" echo "===================================================================" echo " TOATE STORY-URILE DIN PRD SUNT COMPLETE!" echo "===================================================================" exit 0 fi # W3: tag validation post-iter — chiar dacă Opus a marcat docs, dacă diff atinge .vue/.tsx, force ui dag_force_tags "$CURRENT_STORY" >/dev/null 2>&1 || true # W3: dacă story-ul curent ÎNCĂ nu trece (passes==false), incrementăm retries STILL_INCOMPLETE=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | (.passes == true)' "$PRD_FILE") if [ "$STILL_INCOMPLETE" != "true" ]; then NEW_RETRY=$(dag_incr_retry "$CURRENT_STORY") echo " Story $CURRENT_STORY încă incomplet. Retries: $NEW_RETRY/3" if [ "$NEW_RETRY" -ge 3 ] 2>/dev/null; then echo " ❌ $CURRENT_STORY failed: max_retries — sare la următorul" # mark-failed e deja făcut de incr-retry când >=3, dar idempotent o re-aplicăm dag_mark_failed "$CURRENT_STORY" "max_retries" fi else echo " ✅ Story $CURRENT_STORY marcat passes=true în iterația asta." fi echo " Iterația $i completă. Continuăm..." sleep 2 done echo "" echo "===================================================================" echo " Ralph a atins limita de iterații ($MAX_ITERATIONS)" echo " Verifică progress.txt pentru status." echo "===================================================================" echo "" # Afișează stories incomplete echo "Stories incomplete:" jq -r '.userStories[] | select(.passes != true) | " - \(.id): \(.title)"' "$PRD_FILE" exit 1