diff --git a/.ralphy/progress.txt b/.ralphy/progress.txt
new file mode 100644
index 0000000..7c396bd
--- /dev/null
+++ b/.ralphy/progress.txt
@@ -0,0 +1,2 @@
+# Ralphy Progress Log
+
diff --git a/PRD.md b/PRD.md
index 804cb1b..7e1f6b0 100644
--- a/PRD.md
+++ b/PRD.md
@@ -119,3 +119,16 @@ AnchorOS es un sistema operativo para salones de belleza orientado a agenda, pag
## 9. Estado del Documento
Este PRD es la fuente única de verdad funcional del sistema AnchorOS.
+
+## 10. Tasks
+
+- [x] Configurar estructura del proyecto con timestamps UTC en backend
+- [ ] Implementar UUID como claves primarias para todas las entidades
+- [ ] Agregar generación de Short ID con verificación de unicidad
+- [ ] Crear control de acceso basado en roles (Admin, Manager, Staff, Artist, Customer)
+- [ ] Implementar manejo de zonas horarias (UTC en backend, local en frontend)
+- [ ] Construir sistema de bookings con funcionalidad de agenda
+- [ ] Integrar pagos con Stripe usando short ID como referencia
+- [ ] Agregar logging de auditoría para acciones automáticas
+- [ ] Crear niveles de membresía (Free, Gold) con beneficios
+- [ ] Implementar sistema de invitaciones para tier Gold (5 semanales, reseteables)
diff --git a/ralphy.sh b/ralphy.sh
index dc38b1d..8a41481 100755
--- a/ralphy.sh
+++ b/ralphy.sh
@@ -696,3 +696,1675 @@ parse_args() {
shift
;;
--qwen)
+ AI_ENGINE="qwen"
+ shift
+ ;;
+ --droid)
+ AI_ENGINE="droid"
+ shift
+ ;;
+ --init)
+ INIT_MODE=true
+ shift
+ ;;
+ --config)
+ SHOW_CONFIG=true
+ shift
+ ;;
+ --add-rule)
+ ADD_RULE="$2"
+ shift 2
+ ;;
+ --no-commit)
+ AUTO_COMMIT=false
+ shift
+ ;;
+ --max-iterations)
+ MAX_ITERATIONS="$2"
+ shift 2
+ ;;
+ --max-retries)
+ MAX_RETRIES="$2"
+ shift 2
+ ;;
+ --retry-delay)
+ RETRY_DELAY="$2"
+ shift 2
+ ;;
+ --dry-run)
+ DRY_RUN=true
+ shift
+ ;;
+ --parallel)
+ PARALLEL=true
+ shift
+ ;;
+ --max-parallel)
+ MAX_PARALLEL="$2"
+ shift 2
+ ;;
+ --branch-per-task)
+ BRANCH_PER_TASK=true
+ shift
+ ;;
+ --base-branch)
+ BASE_BRANCH="$2"
+ shift 2
+ ;;
+ --create-pr)
+ CREATE_PR=true
+ shift
+ ;;
+ --draft-pr)
+ PR_DRAFT=true
+ shift
+ ;;
+ --prd)
+ PRD_FILE="$2"
+ shift 2
+ ;;
+ --yaml)
+ PRD_SOURCE="yaml"
+ PRD_FILE="$2"
+ shift 2
+ ;;
+ --github)
+ PRD_SOURCE="github"
+ GITHUB_REPO="$2"
+ shift 2
+ ;;
+ --github-label)
+ GITHUB_LABEL="$2"
+ shift 2
+ ;;
+ -v|--verbose)
+ VERBOSE=true
+ shift
+ ;;
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ --version)
+ show_version
+ exit 0
+ ;;
+ *)
+ SINGLE_TASK="$1"
+ shift
+ ;;
+ esac
+ done
+
+ # Validate arguments
+ if [[ "$INIT_MODE" == true ]]; then
+ init_ralphy_config
+ exit 0
+ fi
+
+ if [[ "$SHOW_CONFIG" == true ]]; then
+ show_ralphy_config
+ exit 0
+ fi
+
+ if [[ -n "$ADD_RULE" ]]; then
+ add_ralphy_rule "$ADD_RULE"
+ exit 0
+ fi
+
+ # If no single task and no PRD file, show help
+ if [[ -z "$SINGLE_TASK" ]] && [[ ! -f "$PRD_FILE" ]]; then
+ if [[ "$PRD_SOURCE" == "github" ]]; then
+ # GitHub mode doesn't need local file
+ :
+ else
+ echo "No PRD file found. Run './ralphy.sh --init' to create config, or create PRD.md, or specify a task."
+ exit 1
+ fi
+ fi
+
+# ============================================
+# PRE-FLIGHT CHECKS
+# ============================================
+
+check_requirements() {
+ local missing=()
+
+ # Check for PRD source
+ case "$PRD_SOURCE" in
+ markdown)
+ if [[ ! -f "$PRD_FILE" ]]; then
+ log_error "$PRD_FILE not found in current directory"
+ log_info "Create a PRD.md file with tasks marked as '- [ ] Task description'"
+ log_info "Or use: --yaml tasks.yaml for YAML task files"
+ exit 1
+ fi
+ ;;
+ yaml)
+ if [[ ! -f "$PRD_FILE" ]]; then
+ log_error "$PRD_FILE not found in current directory"
+ log_info "Create a tasks.yaml file with tasks in YAML format"
+ log_info "Or use: --prd PRD.md for Markdown task files"
+ exit 1
+ fi
+ if ! command -v yq &>/dev/null; then
+ log_error "yq is required for YAML parsing. Install from https://github.com/mikefarah/yq"
+ exit 1
+ fi
+ ;;
+ github)
+ if [[ -z "$GITHUB_REPO" ]]; then
+ log_error "GitHub repository not specified. Use --github owner/repo"
+ exit 1
+ fi
+ if ! command -v gh &>/dev/null; then
+ log_error "GitHub CLI (gh) is required. Install from https://cli.github.com/"
+ exit 1
+ fi
+ ;;
+ esac
+
+ # Check for AI CLI
+ case "$AI_ENGINE" in
+ opencode)
+ if ! command -v opencode &>/dev/null; then
+ log_error "OpenCode CLI not found."
+ log_info "Install from: https://opencode.ai/docs/"
+ exit 1
+ fi
+ ;;
+ codex)
+ if ! command -v codex &>/dev/null; then
+ log_error "Codex CLI not found."
+ log_info "Make sure 'codex' is in your PATH."
+ exit 1
+ fi
+ ;;
+ cursor)
+ if ! command -v agent &>/dev/null; then
+ log_error "Cursor agent CLI not found."
+ log_info "Make sure Cursor is installed and 'agent' is in your PATH."
+ exit 1
+ fi
+ ;;
+ qwen)
+ if ! command -v qwen &>/dev/null; then
+ log_error "Qwen-Code CLI not found."
+ log_info "Make sure 'qwen' is in your PATH."
+ exit 1
+ fi
+ ;;
+ droid)
+ if ! command -v droid &>/dev/null; then
+ log_error "Factory Droid CLI not found. Install from https://docs.factory.ai/cli/getting-started/quickstart"
+ exit 1
+ fi
+ ;;
+ *)
+ if ! command -v claude &>/dev/null; then
+ log_error "Claude Code CLI not found."
+ log_info "Install from: https://github.com/anthropics/claude-code"
+ log_info "Or use another engine: --cursor, --opencode, --codex, --qwen"
+ exit 1
+ fi
+ ;;
+ esac
+
+ # Check for jq (required for JSON parsing)
+ if ! command -v jq &>/dev/null; then
+ log_error "jq is required but not installed. On Linux, install with: apt-get install jq (Debian/Ubuntu) or yum install jq (RHEL/CentOS)"
+ exit 1
+ fi
+
+ # Check for gh if PR creation is requested
+ if [[ "$CREATE_PR" == true ]] && ! command -v gh &>/dev/null; then
+ log_error "GitHub CLI (gh) is required for --create-pr. Install from https://cli.github.com/"
+ exit 1
+ fi
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ log_warn "Missing optional dependencies: ${missing[*]}"
+ log_warn "Some features may not work properly"
+ fi
+
+ # Check for git
+ if ! command -v git &>/dev/null; then
+ log_error "git is required but not installed. Ralphy requires a git repository to track changes."
+ exit 1
+ fi
+
+ # Check if we're in a git repository
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
+ log_error "Not a git repository. Ralphy requires a git repository to track changes."
+ exit 1
+ fi
+
+ # Ensure .ralphy/ directory exists and create progress.txt if missing
+ mkdir -p "$RALPHY_DIR"
+ if [[ ! -f "$PROGRESS_FILE" ]]; then
+ log_info "Creating $PROGRESS_FILE..."
+ echo "# Ralphy Progress Log" > "$PROGRESS_FILE"
+ echo "" >> "$PROGRESS_FILE"
+ fi
+
+ # Set base branch if not specified
+ if [[ "$BRANCH_PER_TASK" == true ]] && [[ -z "$BASE_BRANCH" ]]; then
+ BASE_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
+ log_debug "Using base branch: $BASE_BRANCH"
+ fi
+}
+
+# ============================================
+# ARGUMENT PARSING END
+# ============================================
+}
+
+# ============================================
+# CLEANUP HANDLER
+# ============================================
+
+cleanup() {
+ local exit_code=$?
+
+ # Kill background processes
+ [[ -n "$monitor_pid" ]] && kill "$monitor_pid" 2>/dev/null || true
+ [[ -n "$ai_pid" ]] && kill "$ai_pid" 2>/dev/null || true
+
+ # Kill parallel processes
+ for pid in "${parallel_pids[@]+"${parallel_pids[@]}"}"; do
+ kill "$pid" 2>/dev/null || true
+ done
+
+ # Kill any remaining child processes
+ pkill -P $$ 2>/dev/null || true
+
+ # Remove temp file
+ [[ -n "$tmpfile" ]] && rm -f "$tmpfile"
+ [[ -n "$CODEX_LAST_MESSAGE_FILE" ]] && rm -f "$CODEX_LAST_MESSAGE_FILE"
+
+ # Cleanup parallel worktrees
+ if [[ -n "$WORKTREE_BASE" ]] && [[ -d "$WORKTREE_BASE" ]]; then
+ # Remove all worktrees we created
+ for dir in "$WORKTREE_BASE"/agent-*; do
+ if [[ -d "$dir" ]]; then
+ if git -C "$dir" status --porcelain 2>/dev/null | grep -q .; then
+ log_warn "Preserving dirty worktree: $dir"
+ continue
+ fi
+ git worktree remove "$dir" 2>/dev/null || true
+ fi
+ done
+ if ! find "$WORKTREE_BASE" -maxdepth 1 -type d -name 'agent-*' -print -quit 2>/dev/null | grep -q .; then
+ rm -rf "$WORKTREE_BASE" 2>/dev/null || true
+ else
+ log_warn "Preserving worktree base with dirty agents: $WORKTREE_BASE"
+ fi
+ fi
+
+ # Show message on interrupt
+ if [[ $exit_code -eq 130 ]]; then
+ printf "\n"
+ log_warn "Interrupted! Cleaned up."
+
+ # Show branches created if any
+ if [[ -n "${task_branches[*]+"${task_branches[*]}"}" ]]; then
+ log_info "Branches created: ${task_branches[*]}"
+ fi
+
+ # Show integration branches if any (for parallel group workflows)
+ if [[ -n "${integration_branches[*]+"${integration_branches[*]}"}" ]]; then
+ log_info "Integration branches: ${integration_branches[*]}"
+ if [[ -n "$ORIGINAL_BASE_BRANCH" ]]; then
+ log_info "To resume: merge integration branches into $ORIGINAL_BASE_BRANCH"
+ fi
+ fi
+ fi
+}
+
+# ============================================
+# TASK SOURCES - MARKDOWN
+# ============================================
+
+get_tasks_markdown() {
+ grep '^\- \[ \]' "$PRD_FILE" 2>/dev/null | sed 's/^- \[ \] //' || true
+}
+
+get_next_task_markdown() {
+ grep -m1 '^\- \[ \]' "$PRD_FILE" 2>/dev/null | sed 's/^- \[ \] //' | cut -c1-50 || echo ""
+}
+
+count_remaining_markdown() {
+ grep -c '^\- \[ \]' "$PRD_FILE" 2>/dev/null || echo "0"
+}
+
+count_completed_markdown() {
+ grep -c '^\- \[x\]' "$PRD_FILE" 2>/dev/null || echo "0"
+}
+
+mark_task_complete_markdown() {
+ local task=$1
+ # For macOS sed (BRE), we need to:
+ # - Escape: [ ] \ . * ^ $ /
+ # - NOT escape: { } ( ) + ? | (these are literal in BRE)
+ local escaped_task
+ escaped_task=$(printf '%s\n' "$task" | sed 's/[[\.*^$/]/\\&/g')
+ sed -i.bak "s/^- \[ \] ${escaped_task}/- [x] ${escaped_task}/" "$PRD_FILE"
+ rm -f "${PRD_FILE}.bak"
+}
+
+# ============================================
+# TASK SOURCES - YAML
+# ============================================
+
+get_tasks_yaml() {
+ yq -r '.tasks[] | select(.completed != true) | .title' "$PRD_FILE" 2>/dev/null || true
+}
+
+get_next_task_yaml() {
+ yq -r '.tasks[] | select(.completed != true) | .title' "$PRD_FILE" 2>/dev/null | head -1 | cut -c1-50 || echo ""
+}
+
+count_remaining_yaml() {
+ yq -r '[.tasks[] | select(.completed != true)] | length' "$PRD_FILE" 2>/dev/null || echo "0"
+}
+
+count_completed_yaml() {
+ yq -r '[.tasks[] | select(.completed == true)] | length' "$PRD_FILE" 2>/dev/null || echo "0"
+}
+
+mark_task_complete_yaml() {
+ local task=$1
+ yq -i "(.tasks[] | select(.title == \"$task\")).completed = true" "$PRD_FILE"
+}
+
+get_parallel_group_yaml() {
+ local task=$1
+ yq -r ".tasks[] | select(.title == \"$task\") | .parallel_group // 0" "$PRD_FILE" 2>/dev/null || echo "0"
+}
+
+get_tasks_in_group_yaml() {
+ local group=$1
+ yq -r ".tasks[] | select(.completed != true and (.parallel_group // 0) == $group) | .title" "$PRD_FILE" 2>/dev/null || true
+}
+
+# ============================================
+# TASK SOURCES - GITHUB ISSUES
+# ============================================
+
+get_tasks_github() {
+ local args=(--repo "$GITHUB_REPO" --state open --json number,title)
+ [[ -n "$GITHUB_LABEL" ]] && args+=(--label "$GITHUB_LABEL")
+
+ gh issue list "${args[@]}" \
+ --jq '.[] | "\(.number):\(.title)"' 2>/dev/null || true
+}
+
+get_next_task_github() {
+ local args=(--repo "$GITHUB_REPO" --state open --limit 1 --json number,title)
+ [[ -n "$GITHUB_LABEL" ]] && args+=(--label "$GITHUB_LABEL")
+
+ gh issue list "${args[@]}" \
+ --jq '.[0] | "\(.number):\(.title)"' 2>/dev/null | cut -c1-50 || echo ""
+}
+
+count_remaining_github() {
+ local args=(--repo "$GITHUB_REPO" --state open --json number)
+ [[ -n "$GITHUB_LABEL" ]] && args+=(--label "$GITHUB_LABEL")
+
+ gh issue list "${args[@]}" \
+ --jq 'length' 2>/dev/null || echo "0"
+}
+
+count_completed_github() {
+ local args=(--repo "$GITHUB_REPO" --state closed --json number)
+ [[ -n "$GITHUB_LABEL" ]] && args+=(--label "$GITHUB_LABEL")
+
+ gh issue list "${args[@]}" \
+ --jq 'length' 2>/dev/null || echo "0"
+}
+
+mark_task_complete_github() {
+ local task=$1
+ # Extract issue number from "number:title" format
+ local issue_num="${task%%:*}"
+ gh issue close "$issue_num" --repo "$GITHUB_REPO" 2>/dev/null || true
+}
+
+get_github_issue_body() {
+ local task=$1
+ local issue_num="${task%%:*}"
+ gh issue view "$issue_num" --repo "$GITHUB_REPO" --json body --jq '.body' 2>/dev/null || echo ""
+}
+
+# ============================================
+# UNIFIED TASK INTERFACE
+# ============================================
+
+get_next_task() {
+ case "$PRD_SOURCE" in
+ markdown) get_next_task_markdown ;;
+ yaml) get_next_task_yaml ;;
+ github) get_next_task_github ;;
+ esac
+}
+
+get_all_tasks() {
+ case "$PRD_SOURCE" in
+ markdown) get_tasks_markdown ;;
+ yaml) get_tasks_yaml ;;
+ github) get_tasks_github ;;
+ esac
+}
+
+count_remaining_tasks() {
+ case "$PRD_SOURCE" in
+ markdown) count_remaining_markdown ;;
+ yaml) count_remaining_yaml ;;
+ github) count_remaining_github ;;
+ esac
+}
+
+count_completed_tasks() {
+ case "$PRD_SOURCE" in
+ markdown) count_completed_markdown ;;
+ yaml) count_completed_yaml ;;
+ github) count_completed_github ;;
+ esac
+}
+
+mark_task_complete() {
+ local task=$1
+ case "$PRD_SOURCE" in
+ markdown) mark_task_complete_markdown "$task" ;;
+ yaml) mark_task_complete_yaml "$task" ;;
+ github) mark_task_complete_github "$task" ;;
+ esac
+}
+
+# ============================================
+# GIT BRANCH MANAGEMENT
+# ============================================
+
+create_task_branch() {
+ local task=$1
+ local branch_name="ralphy/$(slugify "$task")"
+
+ log_debug "Creating branch: $branch_name from $BASE_BRANCH"
+
+ # Stash any changes (only pop if a new stash was created)
+ local stash_before stash_after stashed=false
+ stash_before=$(git stash list -1 --format='%gd %s' 2>/dev/null || true)
+ git stash push -m "ralphy-autostash" >/dev/null 2>&1 || true
+ stash_after=$(git stash list -1 --format='%gd %s' 2>/dev/null || true)
+ if [[ -n "$stash_after" ]] && [[ "$stash_after" != "$stash_before" ]] && [[ "$stash_after" == *"ralphy-autostash"* ]]; then
+ stashed=true
+ fi
+
+ # Create and checkout new branch
+ git checkout "$BASE_BRANCH" 2>/dev/null || true
+ git pull origin "$BASE_BRANCH" 2>/dev/null || true
+ git checkout -b "$branch_name" 2>/dev/null || {
+ # Branch might already exist
+ git checkout "$branch_name" 2>/dev/null || true
+ }
+
+ # Pop stash if we stashed
+ if [[ "$stashed" == true ]]; then
+ git stash pop >/dev/null 2>&1 || true
+ fi
+
+ task_branches+=("$branch_name")
+ echo "$branch_name"
+}
+
+create_pull_request() {
+ local branch=$1
+ local task=$2
+ local body="${3:-Automated PR created by Ralphy}"
+
+ local draft_flag=""
+ [[ "$PR_DRAFT" == true ]] && draft_flag="--draft"
+
+ log_info "Creating pull request for $branch..."
+
+ # Push branch first
+ git push -u origin "$branch" 2>/dev/null || {
+ log_warn "Failed to push branch $branch"
+ return 1
+ }
+
+ # Create PR
+ local pr_url
+ pr_url=$(gh pr create \
+ --base "$BASE_BRANCH" \
+ --head "$branch" \
+ --title "$task" \
+ --body "$body" \
+ $draft_flag 2>/dev/null) || {
+ log_warn "Failed to create PR for $branch"
+ return 1
+ }
+
+ log_success "PR created: $pr_url"
+ echo "$pr_url"
+}
+
+return_to_base_branch() {
+ if [[ "$BRANCH_PER_TASK" == true ]]; then
+ git checkout "$BASE_BRANCH" 2>/dev/null || true
+ fi
+}
+
+# ============================================
+# PROGRESS MONITOR
+# ============================================
+
+monitor_progress() {
+ local file=$1
+ local task=$2
+ local start_time
+ start_time=$(date +%s)
+ local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
+ local spin_idx=0
+
+ task="${task:0:40}"
+
+ while true; do
+ local elapsed=$(($(date +%s) - start_time))
+ local mins=$((elapsed / 60))
+ local secs=$((elapsed % 60))
+
+ # Check latest output for step indicators
+ if [[ -f "$file" ]] && [[ -s "$file" ]]; then
+ local content
+ content=$(tail -c 5000 "$file" 2>/dev/null || true)
+
+ if echo "$content" | grep -qE 'git commit|"command":"git commit'; then
+ current_step="Committing"
+ elif echo "$content" | grep -qE 'git add|"command":"git add'; then
+ current_step="Staging"
+ elif echo "$content" | grep -qE 'progress\.txt'; then
+ current_step="Logging"
+ elif echo "$content" | grep -qE 'PRD\.md|tasks\.yaml'; then
+ current_step="Updating PRD"
+ elif echo "$content" | grep -qE 'lint|eslint|biome|prettier'; then
+ current_step="Linting"
+ elif echo "$content" | grep -qE 'vitest|jest|bun test|npm test|pytest|go test'; then
+ current_step="Testing"
+ elif echo "$content" | grep -qE '\.test\.|\.spec\.|__tests__|_test\.go'; then
+ current_step="Writing tests"
+ elif echo "$content" | grep -qE '"tool":"[Ww]rite"|"tool":"[Ee]dit"|"name":"write"|"name":"edit"'; then
+ current_step="Implementing"
+ elif echo "$content" | grep -qE '"tool":"[Rr]ead"|"tool":"[Gg]lob"|"tool":"[Gg]rep"|"name":"read"|"name":"glob"|"name":"grep"'; then
+ current_step="Reading code"
+ fi
+ fi
+
+ local spinner_char="${spinstr:$spin_idx:1}"
+ local step_color=""
+
+ # Color-code steps
+ case "$current_step" in
+ "Thinking"|"Reading code") step_color="$CYAN" ;;
+ "Implementing"|"Writing tests") step_color="$MAGENTA" ;;
+ "Testing"|"Linting") step_color="$YELLOW" ;;
+ "Staging"|"Committing") step_color="$GREEN" ;;
+ *) step_color="$BLUE" ;;
+ esac
+
+ # Use tput for cleaner line clearing
+ tput cr 2>/dev/null || printf "\r"
+ tput el 2>/dev/null || true
+ printf " %s ${step_color}%-16s${RESET} │ %s ${DIM}[%02d:%02d]${RESET}" "$spinner_char" "$current_step" "$task" "$mins" "$secs"
+
+ spin_idx=$(( (spin_idx + 1) % ${#spinstr} ))
+ sleep 0.12
+ done
+}
+
+# ============================================
+# NOTIFICATION (Cross-platform)
+# ============================================
+
+notify_done() {
+ local message="${1:-Ralphy has completed all tasks!}"
+
+ # macOS
+ if command -v afplay &>/dev/null; then
+ afplay /System/Library/Sounds/Glass.aiff 2>/dev/null &
+ fi
+
+ # macOS notification
+ if command -v osascript &>/dev/null; then
+ osascript -e "display notification \"$message\" with title \"Ralphy\"" 2>/dev/null || true
+ fi
+
+ # Linux (notify-send)
+ if command -v notify-send &>/dev/null; then
+ notify-send "Ralphy" "$message" 2>/dev/null || true
+ fi
+
+ # Linux (paplay for sound)
+ if command -v paplay &>/dev/null; then
+ paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null &
+ fi
+
+ # Windows (powershell)
+ if command -v powershell.exe &>/dev/null; then
+ powershell.exe -Command "[System.Media.SystemSounds]::Asterisk.Play()" 2>/dev/null || true
+ fi
+}
+
+notify_error() {
+ local message="${1:-Ralphy encountered an error}"
+
+ # macOS
+ if command -v osascript &>/dev/null; then
+ osascript -e "display notification \"$message\" with title \"Ralphy - Error\"" 2>/dev/null || true
+ fi
+
+ # Linux
+ if command -v notify-send &>/dev/null; then
+ notify-send -u critical "Ralphy - Error" "$message" 2>/dev/null || true
+ fi
+}
+
+# ============================================
+# PROMPT BUILDER
+# ============================================
+
+build_prompt() {
+ local task_override="${1:-}"
+ local prompt=""
+
+ # Add .ralphy/ config if available (works with PRD mode too)
+ if [[ -d "$RALPHY_DIR" ]]; then
+ # Add project context
+ local context
+ context=$(load_project_context)
+ if [[ -n "$context" ]]; then
+ prompt+="## Project Context
+$context
+
+"
+ fi
+
+ # Add rules
+ local rules
+ rules=$(load_ralphy_rules)
+ if [[ -n "$rules" ]]; then
+ prompt+="## Rules (you MUST follow these)
+$rules
+
+"
+ fi
+
+ # Add boundaries
+ local never_touch
+ never_touch=$(load_ralphy_boundaries "never_touch")
+ if [[ -n "$never_touch" ]]; then
+ prompt+="## Boundaries - Do NOT modify these files:
+$never_touch
+
+"
+ fi
+ fi
+
+ # Add context based on PRD source
+ case "$PRD_SOURCE" in
+ markdown)
+ prompt="@${PRD_FILE} @$PROGRESS_FILE"
+ ;;
+ yaml)
+ prompt="@${PRD_FILE} @$PROGRESS_FILE"
+ ;;
+ github)
+ # For GitHub issues, we include the issue body
+ local issue_body=""
+ if [[ -n "$task_override" ]]; then
+ issue_body=$(get_github_issue_body "$task_override")
+ fi
+ prompt="Task from GitHub Issue: $task_override
+
+Issue Description:
+$issue_body
+
+@$PROGRESS_FILE"
+ ;;
+ esac
+
+ prompt="$prompt
+1. Find the highest-priority incomplete task and implement it."
+
+ local step=2
+
+ if [[ "$SKIP_TESTS" == false ]]; then
+ prompt="$prompt
+$step. Write tests for the feature.
+$((step+1)). Run tests and ensure they pass before proceeding."
+ step=$((step+2))
+ fi
+
+ if [[ "$SKIP_LINT" == false ]]; then
+ prompt="$prompt
+$step. Run linting and ensure it passes before proceeding."
+ step=$((step+1))
+ fi
+
+ # Adjust completion step based on PRD source
+ case "$PRD_SOURCE" in
+ markdown)
+ prompt="$prompt
+$step. Update the PRD to mark the task as complete (change '- [ ]' to '- [x]')."
+ ;;
+ yaml)
+ prompt="$prompt
+$step. Update ${PRD_FILE} to mark the task as completed (set completed: true)."
+ ;;
+ github)
+ prompt="$prompt
+$step. The task will be marked complete automatically. Just note the completion in $PROGRESS_FILE."
+ ;;
+ esac
+
+ step=$((step+1))
+
+ prompt="$prompt
+$step. Append your progress to $PROGRESS_FILE.
+$((step+1)). Commit your changes with a descriptive message.
+ONLY WORK ON A SINGLE TASK."
+
+ if [[ "$SKIP_TESTS" == false ]]; then
+ prompt="$prompt Do not proceed if tests fail."
+ fi
+ if [[ "$SKIP_LINT" == false ]]; then
+ prompt="$prompt Do not proceed if linting fails."
+ fi
+
+ prompt="$prompt
+If ALL tasks in the PRD are complete, output COMPLETE."
+
+ echo "$prompt"
+}
+
+# ============================================
+# AI ENGINE ABSTRACTION
+# ============================================
+
+run_ai_command() {
+ local prompt=$1
+ local output_file=$2
+
+ case "$AI_ENGINE" in
+ opencode)
+ # OpenCode: use 'run' command with JSON format and permissive settings
+ OPENCODE_PERMISSION='{"*":"allow"}' opencode run \
+ --format json \
+ "$prompt" > "$output_file" 2>&1 &
+ ;;
+ cursor)
+ # Cursor agent: use --print for non-interactive, --force to allow all commands
+ agent --print --force \
+ --output-format stream-json \
+ "$prompt" > "$output_file" 2>&1 &
+ ;;
+ qwen)
+ # Qwen-Code: use CLI with JSON format and auto-approve tools
+ qwen --output-format stream-json \
+ --approval-mode yolo \
+ -p "$prompt" > "$output_file" 2>&1 &
+ ;;
+ droid)
+ # Droid: use exec with stream-json output and medium autonomy for development
+ droid exec --output-format stream-json \
+ --auto medium \
+ "$prompt" > "$output_file" 2>&1 &
+ ;;
+ codex)
+ CODEX_LAST_MESSAGE_FILE="${output_file}.last"
+ rm -f "$CODEX_LAST_MESSAGE_FILE"
+ codex exec --full-auto \
+ --json \
+ --output-last-message "$CODEX_LAST_MESSAGE_FILE" \
+ "$prompt" > "$output_file" 2>&1 &
+ ;;
+ *)
+ # Claude Code: use existing approach
+ claude --dangerously-skip-permissions \
+ --verbose \
+ --output-format stream-json \
+ -p "$prompt" > "$output_file" 2>&1 &
+ ;;
+ esac
+
+ ai_pid=$!
+}
+
+parse_ai_result() {
+ local result=$1
+ local response=""
+ local input_tokens=0
+ local output_tokens=0
+ local actual_cost="0"
+
+ case "$AI_ENGINE" in
+ opencode)
+ # OpenCode JSON format: uses step_finish for tokens and text events for response
+ local step_finish
+ step_finish=$(echo "$result" | grep '"type":"step_finish"' | tail -1 || echo "")
+
+ if [[ -n "$step_finish" ]]; then
+ input_tokens=$(echo "$step_finish" | jq -r '.part.tokens.input // 0' 2>/dev/null || echo "0")
+ output_tokens=$(echo "$step_finish" | jq -r '.part.tokens.output // 0' 2>/dev/null || echo "0")
+ # OpenCode provides actual cost directly
+ actual_cost=$(echo "$step_finish" | jq -r '.part.cost // 0' 2>/dev/null || echo "0")
+ fi
+
+ # Get text response from text events
+ response=$(echo "$result" | grep '"type":"text"' | jq -rs 'map(.part.text // "") | join("")' 2>/dev/null || echo "")
+
+ # If no text found, indicate task completed
+ if [[ -z "$response" ]]; then
+ response="Task completed"
+ fi
+ ;;
+ cursor)
+ # Cursor agent: parse stream-json output
+ # Cursor doesn't provide token counts, but does provide duration_ms
+
+ local result_line
+ result_line=$(echo "$result" | grep '"type":"result"' | tail -1)
+
+ if [[ -n "$result_line" ]]; then
+ response=$(echo "$result_line" | jq -r '.result // "Task completed"' 2>/dev/null || echo "Task completed")
+ # Cursor provides duration instead of tokens
+ local duration_ms
+ duration_ms=$(echo "$result_line" | jq -r '.duration_ms // 0' 2>/dev/null || echo "0")
+ # Store duration in output_tokens field for now (we'll handle it specially)
+ # Use negative value as marker that this is duration, not tokens
+ if [[ "$duration_ms" =~ ^[0-9]+$ ]] && [[ "$duration_ms" -gt 0 ]]; then
+ # Encode duration: store as-is, we track separately
+ actual_cost="duration:$duration_ms"
+ fi
+ fi
+
+ # Get response from assistant message if result is empty
+ if [[ -z "$response" ]] || [[ "$response" == "Task completed" ]]; then
+ local assistant_msg
+ assistant_msg=$(echo "$result" | grep '"type":"assistant"' | tail -1)
+ if [[ -n "$assistant_msg" ]]; then
+ response=$(echo "$assistant_msg" | jq -r '.message.content[0].text // .message.content // "Task completed"' 2>/dev/null || echo "Task completed")
+ fi
+ fi
+
+ # Tokens remain 0 for Cursor (not available)
+ input_tokens=0
+ output_tokens=0
+ ;;
+ qwen)
+ # Qwen-Code stream-json parsing (similar to Claude Code)
+ local result_line
+ result_line=$(echo "$result" | grep '"type":"result"' | tail -1)
+
+ if [[ -n "$result_line" ]]; then
+ response=$(echo "$result_line" | jq -r '.result // "No result text"' 2>/dev/null || echo "Could not parse result")
+ input_tokens=$(echo "$result_line" | jq -r '.usage.input_tokens // 0' 2>/dev/null || echo "0")
+ output_tokens=$(echo "$result_line" | jq -r '.usage.output_tokens // 0' 2>/dev/null || echo "0")
+ fi
+
+ # Fallback when no response text was parsed, similar to OpenCode behavior
+ if [[ -z "$response" ]]; then
+ response="Task completed"
+ fi
+ ;;
+ droid)
+ # Droid stream-json parsing
+ # Look for completion event which has the final result
+ local completion_line
+ completion_line=$(echo "$result" | grep '"type":"completion"' | tail -1)
+
+ if [[ -n "$completion_line" ]]; then
+ response=$(echo "$completion_line" | jq -r '.finalText // "Task completed"' 2>/dev/null || echo "Task completed")
+ # Droid provides duration_ms in completion event
+ local dur_ms
+ dur_ms=$(echo "$completion_line" | jq -r '.durationMs // 0' 2>/dev/null || echo "0")
+ if [[ "$dur_ms" =~ ^[0-9]+$ ]] && [[ "$dur_ms" -gt 0 ]]; then
+ # Store duration for tracking
+ actual_cost="duration:$dur_ms"
+ fi
+ fi
+
+ # Tokens remain 0 for Droid (not exposed in exec mode)
+ input_tokens=0
+ output_tokens=0
+ ;;
+ codex)
+ if [[ -n "$CODEX_LAST_MESSAGE_FILE" ]] && [[ -f "$CODEX_LAST_MESSAGE_FILE" ]]; then
+ response=$(cat "$CODEX_LAST_MESSAGE_FILE" 2>/dev/null || echo "")
+ # Codex sometimes prefixes a generic completion line; drop it for readability.
+ response=$(printf '%s' "$response" | sed '1{/^Task completed successfully\.[[:space:]]*$/d;}')
+ fi
+ input_tokens=0
+ output_tokens=0
+ ;;
+ *)
+ # Claude Code stream-json parsing
+ local result_line
+ result_line=$(echo "$result" | grep '"type":"result"' | tail -1)
+
+ if [[ -n "$result_line" ]]; then
+ response=$(echo "$result_line" | jq -r '.result // "No result text"' 2>/dev/null || echo "Could not parse result")
+ input_tokens=$(echo "$result_line" | jq -r '.usage.input_tokens // 0' 2>/dev/null || echo "0")
+ output_tokens=$(echo "$result_line" | jq -r '.usage.output_tokens // 0' 2>/dev/null || echo "0")
+ fi
+ ;;
+ esac
+
+ # Sanitize token counts
+ [[ "$input_tokens" =~ ^[0-9]+$ ]] || input_tokens=0
+ [[ "$output_tokens" =~ ^[0-9]+$ ]] || output_tokens=0
+
+ echo "$response"
+ echo "---TOKENS---"
+ echo "$input_tokens"
+ echo "$output_tokens"
+ echo "$actual_cost"
+}
+
+check_for_errors() {
+ local result=$1
+
+ if echo "$result" | grep -q '"type":"error"'; then
+ local error_msg
+ error_msg=$(echo "$result" | grep '"type":"error"' | head -1 | jq -r '.error.message // .message // .' 2>/dev/null || echo "Unknown error")
+ echo "$error_msg"
+ return 1
+ fi
+
+ return 0
+}
+
+# ============================================
+# COST CALCULATION
+# ============================================
+
+calculate_cost() {
+ local input=$1
+ local output=$2
+
+ if command -v bc &>/dev/null; then
+ echo "scale=4; ($input * 0.000003) + ($output * 0.000015)" | bc
+ else
+ echo "N/A"
+ fi
+}
+
+# ============================================
+# SINGLE TASK EXECUTION
+# ============================================
+
+run_single_task() {
+ local task_name="${1:-}"
+ local task_num="${2:-$iteration}"
+
+ retry_count=0
+
+ echo ""
+ echo "${BOLD}>>> Task $task_num${RESET}"
+
+ local remaining completed
+ remaining=$(count_remaining_tasks | tr -d '[:space:]')
+ completed=$(count_completed_tasks | tr -d '[:space:]')
+ remaining=${remaining:-0}
+ completed=${completed:-0}
+ echo "${DIM} Completed: $completed | Remaining: $remaining${RESET}"
+ echo "--------------------------------------------"
+
+ # Get current task for display
+ local current_task
+ if [[ -n "$task_name" ]]; then
+ current_task="$task_name"
+ else
+ current_task=$(get_next_task)
+ fi
+
+ if [[ -z "$current_task" ]]; then
+ log_info "No more tasks found"
+ return 2
+ fi
+
+ current_step="Thinking"
+
+ # Create branch if needed
+ local branch_name=""
+ if [[ "$BRANCH_PER_TASK" == true ]]; then
+ branch_name=$(create_task_branch "$current_task")
+ log_info "Working on branch: $branch_name"
+ fi
+
+ # Temp file for AI output
+ tmpfile=$(mktemp)
+
+ # Build the prompt
+ local prompt
+ prompt=$(build_prompt "$current_task")
+
+ if [[ "$DRY_RUN" == true ]]; then
+ log_info "DRY RUN - Would execute:"
+ echo "${DIM}$prompt${RESET}"
+ rm -f "$tmpfile"
+ tmpfile=""
+ return_to_base_branch
+ return 0
+ fi
+
+ # Run with retry logic
+ while [[ $retry_count -lt $MAX_RETRIES ]]; do
+ # Start AI command
+ run_ai_command "$prompt" "$tmpfile"
+
+ # Start progress monitor in background
+ monitor_progress "$tmpfile" "${current_task:0:40}" &
+ monitor_pid=$!
+
+ # Wait for AI to finish
+ wait "$ai_pid" 2>/dev/null || true
+
+ # Stop the monitor
+ kill "$monitor_pid" 2>/dev/null || true
+ wait "$monitor_pid" 2>/dev/null || true
+ monitor_pid=""
+
+ # Show completion
+ tput cr 2>/dev/null || printf "\r"
+ tput el 2>/dev/null || true
+
+ # Read result
+ local result
+ result=$(cat "$tmpfile" 2>/dev/null || echo "")
+
+ # Check for empty response
+ if [[ -z "$result" ]]; then
+ ((retry_count++)) || true
+ if [[ $retry_count -lt $MAX_RETRIES ]]; then
+ log_warn "Empty response, retrying... ($retry_count/$MAX_RETRIES)"
+ sleep "$RETRY_DELAY"
+ fi
+ continue
+ fi
+
+ # Check for errors
+ local error_msg
+ error_msg=$(check_for_errors "$result")
+ if [[ -n "$error_msg" ]]; then
+ ((retry_count++)) || true
+ if [[ $retry_count -lt $MAX_RETRIES ]]; then
+ log_warn "AI error: $error_msg"
+ log_warn "Retrying... ($retry_count/$MAX_RETRIES)"
+ sleep "$RETRY_DELAY"
+ fi
+ continue
+ fi
+
+ # Parse result
+ local parsed
+ parsed=$(parse_ai_result "$result")
+ local response
+ response=$(echo "$parsed" | sed '/^---TOKENS---$/,$d')
+ local token_data
+ token_data=$(echo "$parsed" | sed -n '/^---TOKENS---$/,$p' | tail -3)
+ local input_tokens
+ input_tokens=$(echo "$token_data" | sed -n '1p')
+ local output_tokens
+ output_tokens=$(echo "$token_data" | sed -n '2p')
+ local actual_cost
+ actual_cost=$(echo "$token_data" | sed -n '3p')
+
+ printf " ${GREEN}✓${RESET} %-16s │ %s\n" "Done" "${current_task:0:40}"
+
+ if [[ -n "$response" ]]; then
+ echo ""
+ echo "$response"
+ fi
+
+ # Sanitize values
+ [[ "$input_tokens" =~ ^[0-9]+$ ]] || input_tokens=0
+ [[ "$output_tokens" =~ ^[0-9]+$ ]] || output_tokens=0
+
+ # Update totals
+ total_input_tokens=$((total_input_tokens + input_tokens))
+ total_output_tokens=$((total_output_tokens + output_tokens))
+
+ # Track actual cost for OpenCode, or duration for Cursor
+ if [[ -n "$actual_cost" ]]; then
+ if [[ "$actual_cost" == duration:* ]]; then
+ # Cursor duration tracking
+ local dur_ms="${actual_cost#duration:}"
+ [[ "$dur_ms" =~ ^[0-9]+$ ]] && total_duration_ms=$((total_duration_ms + dur_ms))
+ elif [[ "$actual_cost" != "0" ]] && command -v bc &>/dev/null; then
+ # OpenCode cost tracking
+ total_actual_cost=$(echo "scale=6; $total_actual_cost + $actual_cost" | bc 2>/dev/null || echo "$total_actual_cost")
+ fi
+ fi
+
+ rm -f "$tmpfile"
+ tmpfile=""
+ if [[ "$AI_ENGINE" == "codex" ]] && [[ -n "$CODEX_LAST_MESSAGE_FILE" ]]; then
+ rm -f "$CODEX_LAST_MESSAGE_FILE"
+ CODEX_LAST_MESSAGE_FILE=""
+ fi
+
+ # Mark task complete for GitHub issues (since AI can't do it)
+ if [[ "$PRD_SOURCE" == "github" ]]; then
+ mark_task_complete "$current_task"
+ fi
+
+ # Create PR if requested
+ if [[ "$CREATE_PR" == true ]] && [[ -n "$branch_name" ]]; then
+ create_pull_request "$branch_name" "$current_task" "Automated implementation by Ralphy"
+ fi
+
+ # Return to base branch
+ return_to_base_branch
+
+ # Check for completion - verify by actually counting remaining tasks
+ local remaining_count
+ remaining_count=$(count_remaining_tasks | tr -d '[:space:]' | head -1)
+ remaining_count=${remaining_count:-0}
+ [[ "$remaining_count" =~ ^[0-9]+$ ]] || remaining_count=0
+
+ if [[ "$remaining_count" -eq 0 ]]; then
+ return 2 # All tasks actually complete
+ fi
+
+ # AI might claim completion but tasks remain - continue anyway
+ if [[ "$result" == *"COMPLETE"* ]]; then
+ log_debug "AI claimed completion but $remaining_count tasks remain, continuing..."
+ fi
+
+ return 0
+ done
+
+ return_to_base_branch
+ return 1
+}
+
+# ============================================
+# PARALLEL TASK EXECUTION
+# ============================================
+
+# Create an isolated worktree for a parallel agent
+create_agent_worktree() {
+ local task_name="$1"
+ local agent_num="$2"
+ local branch_name="ralphy/agent-${agent_num}-$(slugify "$task_name")"
+ local worktree_dir="${WORKTREE_BASE}/agent-${agent_num}"
+
+ # Run git commands from original directory
+ # All git output goes to stderr so it doesn't interfere with our return value
+ (
+ cd "$ORIGINAL_DIR" || { echo "Failed to cd to $ORIGINAL_DIR" >&2; exit 1; }
+
+ # Prune any stale worktrees first
+ git worktree prune >&2
+
+ # Delete branch if it exists (force)
+ git branch -D "$branch_name" >&2 2>/dev/null || true
+
+ # Create worktree
+ git worktree add "$worktree_dir" "$ORIGINAL_BASE_BRANCH" >&2 || {
+ echo "Failed to create worktree $worktree_dir" >&2
+ exit 1
+ }
+
+ # Create and checkout branch in the worktree
+ (
+ cd "$worktree_dir" || exit 1
+ git checkout -b "$branch_name" >&2 || {
+ echo "Failed to create branch $branch_name" >&2
+ exit 1
+ }
+ ) || exit 1
+ ) || return 1
+
+ echo "$worktree_dir"
+}
+
+# Run a single agent in parallel
+run_parallel_agent() {
+ local agent_num="$1"
+ local task_name="$2"
+ local worktree_dir="$3"
+
+ # Change to worktree directory
+ cd "$worktree_dir" || return 1
+
+ # Set up agent-specific state
+ local agent_iteration=0
+ local agent_tmpfile=""
+ local agent_monitor_pid=""
+ local agent_ai_pid=""
+
+ # Create agent-specific progress file
+ local agent_progress_file="$RALPHY_DIR/progress-agent-${agent_num}.txt"
+ echo "# Ralphy Agent $agent_num Progress Log" > "$agent_progress_file"
+ echo "" >> "$agent_progress_file"
+
+ log_info "Agent $agent_num: Starting task '$task_name'"
+
+ # Run single task with agent-specific settings
+ while true; do
+ ((agent_iteration++)) || true
+
+ # Create temp file for this agent's AI output
+ agent_tmpfile=$(mktemp)
+
+ # Build prompt for this specific task
+ local prompt
+ prompt=$(build_prompt "$task_name")
+
+ # Replace progress file reference with agent-specific one
+ prompt=$(echo "$prompt" | sed "s/@$PROGRESS_FILE/@$agent_progress_file/")
+
+ # Run AI command
+ run_ai_command "$prompt" "$agent_tmpfile"
+
+ # Start progress monitor
+ monitor_progress "$agent_tmpfile" "${task_name:0:30} (A$agent_num)" &
+ agent_monitor_pid=$!
+
+ # Wait for AI
+ wait "$agent_ai_pid" 2>/dev/null || true
+
+ # Stop monitor
+ kill "$agent_monitor_pid" 2>/dev/null || true
+ wait "$agent_monitor_pid" 2>/dev/null || true
+ agent_monitor_pid=""
+
+ # Show completion for this agent
+ tput cr 2>/dev/null || printf "\r"
+ tput el 2>/dev/null || true
+ printf " ${GREEN}✓${RESET} %-16s │ %s ${DIM}(Agent %d)${RESET}\n" "Done" "${task_name:0:30}" "$agent_num"
+
+ # Read result
+ local result
+ result=$(cat "$agent_tmpfile" 2>/dev/null || echo "")
+
+ # Parse result (similar to run_single_task)
+ if [[ -n "$result" ]]; then
+ local parsed
+ parsed=$(parse_ai_result "$result")
+ local response
+ response=$(echo "$parsed" | sed '/^---TOKENS---$/,$d')
+
+ # Update global token counts (simplified - just add to totals)
+ local token_data
+ token_data=$(echo "$parsed" | sed -n '/^---TOKENS---$/,$p' | tail -3)
+ local input_tokens
+ input_tokens=$(echo "$token_data" | sed -n '1p')
+ local output_tokens
+ output_tokens=$(echo "$token_data" | sed -n '2p')
+ local actual_cost
+ actual_cost=$(echo "$token_data" | sed -n '3p')
+
+ [[ "$input_tokens" =~ ^[0-9]+$ ]] && total_input_tokens=$((total_input_tokens + input_tokens))
+ [[ "$output_tokens" =~ ^[0-9]+$ ]] && total_output_tokens=$((total_output_tokens + output_tokens))
+
+ if [[ -n "$actual_cost" ]]; then
+ if [[ "$actual_cost" == duration:* ]]; then
+ local dur_ms="${actual_cost#duration:}"
+ [[ "$dur_ms" =~ ^[0-9]+$ ]] && total_duration_ms=$((total_duration_ms + dur_ms))
+ elif [[ "$actual_cost" != "0" ]] && command -v bc &>/dev/null; then
+ total_actual_cost=$(echo "scale=6; $total_actual_cost + $actual_cost" | bc 2>/dev/null || echo "$total_actual_cost")
+ fi
+ fi
+ fi
+
+ rm -f "$agent_tmpfile"
+ agent_tmpfile=""
+
+ # Check if task is complete (AI should mark it in the PRD file)
+ # For parallel execution, we assume the AI marks tasks complete appropriately
+ # We break after one successful run per agent
+ break
+ done
+
+ # Merge changes back to integration branch
+ merge_to_integration "$worktree_dir" "$task_name" "$agent_num"
+
+ log_info "Agent $agent_num: Completed task '$task_name'"
+}
+
+# Merge agent worktree back to integration branch
+merge_to_integration() {
+ local worktree_dir="$1"
+ local task_name="$2"
+ local agent_num="$3"
+
+ local branch_name="ralphy/agent-${agent_num}-$(slugify "$task_name")"
+ local integration_branch="ralphy/integration-$(slugify "$task_name")"
+
+ # Run git commands from original directory
+ (
+ cd "$ORIGINAL_DIR" || exit 1
+
+ # Create integration branch if it doesn't exist
+ if ! git show-ref --verify --quiet "refs/heads/$integration_branch"; then
+ git checkout -b "$integration_branch" "$ORIGINAL_BASE_BRANCH" >&2 || {
+ echo "Failed to create integration branch $integration_branch" >&2
+ exit 1
+ }
+ integration_branches+=("$integration_branch")
+ else
+ git checkout "$integration_branch" >&2 || exit 1
+ fi
+
+ # Merge agent branch (allow unrelated histories)
+ git merge "$branch_name" --allow-unrelated-histories --no-edit >&2 || {
+ echo "Failed to merge $branch_name into $integration_branch" >&2
+ # Try to resolve conflicts automatically
+ git merge --abort >&2 2>/dev/null || true
+ exit 1
+ }
+
+ # Push integration branch
+ git push -u origin "$integration_branch" >&2 2>/dev/null || true
+ ) || log_warn "Agent $agent_num: Failed to merge changes for '$task_name'"
+}
+
+# Run parallel execution
+run_parallel_tasks() {
+ # Set up worktree base
+ WORKTREE_BASE=$(mktemp -d)
+ ORIGINAL_DIR="$PWD"
+ ORIGINAL_BASE_BRANCH="$BASE_BRANCH"
+
+ log_info "Using worktree base: $WORKTREE_BASE"
+
+ # Get all tasks
+ local all_tasks
+ all_tasks=$(get_all_tasks)
+
+ if [[ -z "$all_tasks" ]]; then
+ log_warn "No tasks found"
+ return
+ fi
+
+ # Convert to array
+ local task_array=()
+ while IFS= read -r task; do
+ task_array+=("$task")
+ done <<< "$all_tasks"
+
+ local total_tasks=${#task_array[@]}
+ log_info "Running $total_tasks tasks in parallel (max $MAX_PARALLEL concurrent)"
+
+ local agent_num=0
+ local active_pids=()
+ local completed_count=0
+
+ for task in "${task_array[@]}"; do
+ ((agent_num++)) || true
+
+ # Create worktree for this agent
+ local worktree_dir
+ worktree_dir=$(create_agent_worktree "$task" "$agent_num")
+
+ if [[ -z "$worktree_dir" ]]; then
+ log_error "Failed to create worktree for agent $agent_num"
+ continue
+ fi
+
+ # Start agent in background
+ run_parallel_agent "$agent_num" "$task" "$worktree_dir" &
+ local pid=$!
+ parallel_pids+=("$pid")
+ active_pids+=("$pid")
+
+ log_debug "Started agent $agent_num (PID $pid) for task: $task"
+
+ # Wait if we've reached max parallel
+ while [[ ${#active_pids[@]} -ge $MAX_PARALLEL ]]; do
+ # Check which PIDs are still running
+ local still_active=()
+ for pid in "${active_pids[@]}"; do
+ if kill -0 "$pid" 2>/dev/null; then
+ still_active+=("$pid")
+ else
+ wait "$pid" 2>/dev/null || true
+ ((completed_count++)) || true
+ fi
+ done
+ active_pids=("${still_active[@]}")
+
+ # Brief sleep to avoid busy waiting
+ sleep 0.5
+ done
+ done
+
+ # Wait for remaining agents
+ for pid in "${active_pids[@]}"; do
+ wait "$pid" 2>/dev/null || true
+ ((completed_count++)) || true
+ done
+
+ log_success "All $completed_count parallel agents completed"
+
+ # Create PRs for integration branches if requested
+ if [[ "$CREATE_PR" == true ]] && [[ -n "${integration_branches[*]+"${integration_branches[*]}"}" ]]; then
+ for branch in "${integration_branches[@]}"; do
+ create_pull_request "$branch" "Integration: ${branch#ralphy/integration-}" "Automated integration by Ralphy parallel execution"
+ done
+ fi
+
+ # Cleanup worktrees
+ if [[ -d "$WORKTREE_BASE" ]]; then
+ for dir in "$WORKTREE_BASE"/agent-*; do
+ if [[ -d "$dir" ]]; then
+ git worktree remove "$dir" 2>/dev/null || true
+ fi
+ done
+ rm -rf "$WORKTREE_BASE" 2>/dev/null || true
+ fi
+}
+
+# ============================================
+# SUMMARY & REPORTING
+# ============================================
+
+show_summary() {
+ local completed
+ completed=$(count_completed_tasks | tr -d '[:space:]')
+ completed=${completed:-0}
+
+ echo ""
+ echo "${BOLD}============================================${RESET}"
+ echo "${BOLD}SUMMARY${RESET}"
+ echo "${BOLD}============================================${RESET}"
+ echo "Tasks completed: $completed"
+
+ # Show token usage
+ if [[ $total_input_tokens -gt 0 ]] || [[ $total_output_tokens -gt 0 ]]; then
+ echo "Input tokens: $total_input_tokens"
+ echo "Output tokens: $total_output_tokens"
+
+ if [[ -n "$total_actual_cost" ]] && [[ "$total_actual_cost" != "0" ]]; then
+ echo "Actual cost: $$total_actual_cost"
+ else
+ local estimated_cost
+ estimated_cost=$(calculate_cost "$total_input_tokens" "$total_output_tokens")
+ echo "Est. cost: $$estimated_cost"
+ fi
+ fi
+
+ # Show duration if available
+ if [[ $total_duration_ms -gt 0 ]]; then
+ local total_seconds=$((total_duration_ms / 1000))
+ local mins=$((total_seconds / 60))
+ local secs=$((total_seconds % 60))
+ echo "Duration: ${mins}m ${secs}s"
+ fi
+
+ echo ""
+ echo "${GREEN}All tasks completed!${RESET}"
+ echo ""
+}
+
+# ============================================
+# MAIN
+# ============================================
+
+main() {
+ parse_args "$@"
+
+ # Handle --init mode
+ if [[ "$INIT_MODE" == true ]]; then
+ init_ralphy_config
+ exit 0
+ fi
+
+ # Handle --config mode
+ if [[ "$SHOW_CONFIG" == true ]]; then
+ show_ralphy_config
+ exit 0
+ fi
+
+ # Handle --add-rule
+ if [[ -n "$ADD_RULE" ]]; then
+ add_ralphy_rule "$ADD_RULE"
+ exit 0
+ fi
+
+ # Handle single-task (brownfield) mode
+ if [[ -n "$SINGLE_TASK" ]]; then
+ # Set up cleanup trap
+ trap cleanup EXIT
+ trap 'exit 130' INT TERM HUP
+
+ # Check basic requirements (AI engine, git)
+ case "$AI_ENGINE" in
+ claude) command -v claude &>/dev/null || { log_error "Claude Code CLI not found"; exit 1; } ;;
+ opencode) command -v opencode &>/dev/null || { log_error "OpenCode CLI not found"; exit 1; } ;;
+ cursor) command -v agent &>/dev/null || { log_error "Cursor agent CLI not found"; exit 1; } ;;
+ codex) command -v codex &>/dev/null || { log_error "Codex CLI not found"; exit 1; } ;;
+ qwen) command -v qwen &>/dev/null || { log_error "Qwen-Code CLI not found"; exit 1; } ;;
+ droid) command -v droid &>/dev/null || { log_error "Factory Droid CLI not found"; exit 1; } ;;
+ esac
+
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
+ log_error "Not a git repository"
+ exit 1
+ fi
+
+ # Show brownfield banner
+ echo "${BOLD}============================================${RESET}"
+ echo "${BOLD}Ralphy${RESET} - Single Task Mode"
+ local engine_display
+ case "$AI_ENGINE" in
+ opencode) engine_display="${CYAN}OpenCode${RESET}" ;;
+ cursor) engine_display="${YELLOW}Cursor Agent${RESET}" ;;
+ codex) engine_display="${BLUE}Codex${RESET}" ;;
+ qwen) engine_display="${GREEN}Qwen-Code${RESET}" ;;
+ droid) engine_display="${MAGENTA}Factory Droid${RESET}" ;;
+ *) engine_display="${MAGENTA}Claude Code${RESET}" ;;
+ esac
+ echo "Engine: $engine_display"
+ if [[ -d "$RALPHY_DIR" ]]; then
+ echo "Config: ${GREEN}$RALPHY_DIR/${RESET}"
+ else
+ echo "Config: ${DIM}none (run --init to configure)${RESET}"
+ fi
+ echo "${BOLD}============================================${RESET}"
+
+ run_brownfield_task "$SINGLE_TASK"
+ exit $?
+ fi
+
+ if [[ "$DRY_RUN" == true ]] && [[ "$MAX_ITERATIONS" -eq 0 ]]; then
+ MAX_ITERATIONS=1
+ fi
+
+ # Set up cleanup trap
+ trap cleanup EXIT
+ trap 'exit 130' INT TERM HUP
+
+ # Check requirements
+ check_requirements
+
+ # Show banner
+ echo "${BOLD}============================================${RESET}"
+ echo "${BOLD}Ralphy${RESET} - Running until PRD is complete"
+ local engine_display
+ case "$AI_ENGINE" in
+ opencode) engine_display="${CYAN}OpenCode${RESET}" ;;
+ cursor) engine_display="${YELLOW}Cursor Agent${RESET}" ;;
+ codex) engine_display="${BLUE}Codex${RESET}" ;;
+ qwen) engine_display="${GREEN}Qwen-Code${RESET}" ;;
+ droid) engine_display="${MAGENTA}Factory Droid${RESET}" ;;
+ *) engine_display="${MAGENTA}Claude Code${RESET}" ;;
+ esac
+ echo "Engine: $engine_display"
+ echo "Source: ${CYAN}$PRD_SOURCE${RESET} (${PRD_FILE:-$GITHUB_REPO})"
+ if [[ -d "$RALPHY_DIR" ]]; then
+ echo "Config: ${GREEN}$RALPHY_DIR/${RESET} (rules loaded)"
+ fi
+
+ local mode_parts=()
+ [[ "$SKIP_TESTS" == true ]] && mode_parts+=("no-tests")
+ [[ "$SKIP_LINT" == true ]] && mode_parts+=("no-lint")
+ [[ "$DRY_RUN" == true ]] && mode_parts+=("dry-run")
+ [[ "$PARALLEL" == true ]] && mode_parts+=("parallel:$MAX_PARALLEL")
+ [[ "$BRANCH_PER_TASK" == true ]] && mode_parts+=("branch-per-task")
+ [[ "$CREATE_PR" == true ]] && mode_parts+=("create-pr")
+ [[ $MAX_ITERATIONS -gt 0 ]] && mode_parts+=("max:$MAX_ITERATIONS")
+
+ if [[ ${#mode_parts[@]} -gt 0 ]]; then
+ echo "Mode: ${YELLOW}${mode_parts[*]}${RESET}"
+ fi
+ echo "${BOLD}============================================${RESET}"
+
+ # Run in parallel or sequential mode
+ if [[ "$PARALLEL" == true ]]; then
+ run_parallel_tasks
+ show_summary
+ notify_done
+ exit 0
+ fi
+
+ # Sequential main loop
+ while true; do
+ ((iteration++)) || true
+ local result_code=0
+ run_single_task "" "$iteration" || result_code=$?
+
+ case $result_code in
+ 0)
+ # Success, continue
+ ;;
+ 1)
+ # Error, but continue to next task
+ log_warn "Task failed after $MAX_RETRIES attempts, continuing..."
+ ;;
+ 2)
+ # All tasks complete
+ show_summary
+ notify_done
+ exit 0
+ ;;
+ esac
+
+ # Check max iterations
+ if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $iteration -ge $MAX_ITERATIONS ]]; then
+ log_warn "Reached max iterations ($MAX_ITERATIONS)"
+ show_summary
+ notify_done "Ralphy stopped after $MAX_ITERATIONS iterations"
+ exit 0
+ fi
+
+ # Small delay between iterations
+ sleep 1
+ done
+}
+
+# Run main
+main "$@"