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 "$@"