#!/bin/bash # Ralph - Autonomous Loop for PRD Implementation # Usage: ./ralph.sh [max_iterations] set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" PRD_FILE="$SCRIPT_DIR/prd.json" PROGRESS_FILE="$SCRIPT_DIR/progress.txt" PROMPT_FILE="$SCRIPT_DIR/prompt.md" LOG_DIR="$SCRIPT_DIR/logs" MAX_ITERATIONS=${1:-50} ITERATION=0 # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color log() { echo -e "${BLUE}[Ralph]${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$PROGRESS_FILE" } error() { echo -e "${RED}[Ralph ERROR]${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$PROGRESS_FILE" } success() { echo -e "${GREEN}[Ralph]${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$PROGRESS_FILE" } # Check prerequisites if [ ! -f "$PRD_FILE" ]; then error "prd.json not found at $PRD_FILE" exit 1 fi if ! command -v claude &> /dev/null; then error "claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code" exit 1 fi if ! command -v jq &> /dev/null; then error "jq not found. Install with: apt install jq" exit 1 fi # Create log directory mkdir -p "$LOG_DIR" # Get project info PROJECT_NAME=$(jq -r '.projectName' "$PRD_FILE") BRANCH_NAME=$(jq -r '.branchName' "$PRD_FILE") log "Starting Ralph for project: $PROJECT_NAME" log "Max iterations: $MAX_ITERATIONS" # Create branch if not exists cd "$PROJECT_ROOT" CURRENT_BRANCH=$(git branch --show-current) if [ "$CURRENT_BRANCH" != "$BRANCH_NAME" ]; then if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then log "Switching to existing branch: $BRANCH_NAME" git checkout "$BRANCH_NAME" else log "Creating new branch: $BRANCH_NAME" git checkout -b "$BRANCH_NAME" fi fi # Function to get next pending story get_next_story() { jq -r '.userStories | map(select(.passes == false)) | sort_by(.priority) | .[0] | .id // empty' "$PRD_FILE" } # Function to get story details get_story_details() { local story_id=$1 jq -r --arg id "$story_id" '.userStories[] | select(.id == $id)' "$PRD_FILE" } # Function to mark story as passed mark_story_passed() { local story_id=$1 local notes=$2 local tmp_file=$(mktemp) jq --arg id "$story_id" --arg notes "$notes" \ '(.userStories[] | select(.id == $id)) |= (.passes = true | .notes = $notes)' \ "$PRD_FILE" > "$tmp_file" && mv "$tmp_file" "$PRD_FILE" } # Function to count stories count_stories() { local total=$(jq '.userStories | length' "$PRD_FILE") local passed=$(jq '[.userStories[] | select(.passes == true)] | length' "$PRD_FILE") echo "$passed/$total" } # Main loop while [ $ITERATION -lt $MAX_ITERATIONS ]; do ITERATION=$((ITERATION + 1)) log "=== Iteration $ITERATION/$MAX_ITERATIONS ===" # Get next story NEXT_STORY=$(get_next_story) if [ -z "$NEXT_STORY" ]; then success "All stories completed! 🎉" break fi log "Working on story: $NEXT_STORY" # Get story details for prompt STORY_JSON=$(get_story_details "$NEXT_STORY") STORY_TITLE=$(echo "$STORY_JSON" | jq -r '.title') # Create iteration prompt ITERATION_PROMPT="You are implementing user story $NEXT_STORY: $STORY_TITLE Read the full PRD at scripts/ralph/prd.json for context and CSS rules. Story details: $STORY_JSON IMPORTANT CSS RULES (from PRD): - NEVER use hardcoded values - always use design tokens - Check docs/DESIGN_TOKENS.md and docs/CSS_PATTERNS.md before writing CSS - Test in BOTH light and dark mode - NEVER use :deep() in components Your task: 1. Implement this story following all acceptance criteria 2. Run tests/typecheck to verify 3. If ALL criteria pass, respond with: STORY_PASSED 4. If blocked or need clarification, respond with: STORY_BLOCKED: Do NOT move to other stories. Focus only on $NEXT_STORY." # Run Claude LOG_FILE="$LOG_DIR/iteration_${ITERATION}_${NEXT_STORY}.log" log "Running Claude... (log: $LOG_FILE)" # Run claude with the prompt (--output-format json avoids streaming mode issues) CLAUDE_OUTPUT=$(cd "$PROJECT_ROOT" && claude -p "$ITERATION_PROMPT" --output-format json 2>&1 | tee "$LOG_FILE") # Check result if echo "$CLAUDE_OUTPUT" | grep -q "STORY_PASSED"; then success "Story $NEXT_STORY passed!" mark_story_passed "$NEXT_STORY" "Completed in iteration $ITERATION" # Commit changes cd "$PROJECT_ROOT" if [ -n "$(git status --porcelain)" ]; then git add -A git commit -m "feat($PROJECT_NAME): Complete $NEXT_STORY - $STORY_TITLE Implemented by Ralph autonomous loop. Iteration: $ITERATION Co-Authored-By: Claude " log "Changes committed" fi elif echo "$CLAUDE_OUTPUT" | grep -q "STORY_BLOCKED"; then BLOCK_REASON=$(echo "$CLAUDE_OUTPUT" | grep "STORY_BLOCKED" | sed 's/STORY_BLOCKED://') error "Story $NEXT_STORY blocked: $BLOCK_REASON" log "Stopping loop due to blocked story" break else log "Story $NEXT_STORY not yet complete, continuing..." fi # Progress update PROGRESS=$(count_stories) log "Progress: $PROGRESS stories completed" # Small delay between iterations sleep 2 done # Final summary echo "" log "=== Ralph Session Complete ===" PROGRESS=$(count_stories) log "Final progress: $PROGRESS stories completed" log "Branch: $BRANCH_NAME" log "Logs: $LOG_DIR" # Show remaining stories REMAINING=$(jq -r '[.userStories[] | select(.passes == false)] | length' "$PRD_FILE") if [ "$REMAINING" -gt 0 ]; then echo -e "\n${YELLOW}Remaining stories:${NC}" jq -r '.userStories[] | select(.passes == false) | " - \(.id): \(.title)"' "$PRD_FILE" fi