diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml new file mode 100644 index 0000000..217a4fa --- /dev/null +++ b/.github/workflows/validate-plugin.yml @@ -0,0 +1,34 @@ +name: Validate Plugin + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Validate plugin (versions, schema, skills, docs) + run: bun run validate + + - name: Validate shell scripts syntax + run: | + for script in scripts/*.sh; do + echo "Checking $script..." + bash -n "$script" + done + echo "All shell scripts valid" diff --git a/scripts/ci-ralph.sh b/scripts/ci-ralph.sh index 4637b3b..b523f64 100755 --- a/scripts/ci-ralph.sh +++ b/scripts/ci-ralph.sh @@ -1,33 +1,82 @@ #!/usr/bin/env bash # -# ci-ralph.sh - Autonomous CI resolution loop +# ci-ralph.sh - Autonomous CI resolution loop (AFK Ralph pattern) # # Continuously monitors and fixes CI failures until success or max iterations. # Called by workflow-ralph.sh after PR creation. # +# Features: +# - Docker sandbox mode for AFK safety (USE_SANDBOX=true) +# - YOLO mode to skip permission prompts (YOLO_MODE=true) +# - jq streaming for real-time output +# - Stuck detection (same errors 2x) +# # Usage: # ./scripts/ci-ralph.sh # -# Flow: -# 1. Parse and validate PR number argument -# 2. Poll CI status until complete -# 3. If failing, invoke /github:fix-ci -# 4. Push changes and wait for new CI run -# 5. Repeat up to MAX_CI_ITERATIONS times -# 6. Detect stuck state (same errors 2x) -# 7. Exit with success (0) or failure (1) +# # Full AFK mode (Docker + YOLO) - default +# ./ci-ralph.sh 123 +# +# # No sandbox (faster, less safe) +# USE_SANDBOX=false ./ci-ralph.sh 123 +# +# # Interactive mode (keep permission prompts) +# YOLO_MODE=false ./ci-ralph.sh 123 set -euo pipefail +# ============================================================================ +# AFK Ralph Configuration +# ============================================================================ + +# Docker sandbox mode (default: enabled for AFK safety) +USE_SANDBOX="${USE_SANDBOX:-true}" + +# YOLO mode - skip all permission prompts +YOLO_MODE="${YOLO_MODE:-true}" + +# Temp files for signal capture +TMPDIR="${TMPDIR:-/tmp}" +SIGNAL_LOG="" + +# jq filter for streaming assistant text +JQ_STREAM='select(.type == "assistant").message.content[]? | select(.type == "text").text // empty' + +# jq filter for final result +JQ_RESULT='select(.type == "result").result // empty' + # ============================================================================ # Configuration # ============================================================================ readonly NOTIFICATION_LOG="$HOME/.workflow-notifications.log" -readonly MAX_CI_ITERATIONS=10 -readonly CI_START_TIMEOUT=120 # 2 minutes for CI to start -readonly CI_RUN_TIMEOUT=1800 # 30 minutes for CI to complete -readonly POLL_INTERVAL=30 # Poll every 30 seconds +readonly MAX_CI_ITERATIONS="${MAX_CI_ITERATIONS:-10}" +readonly CI_START_TIMEOUT="${CI_START_TIMEOUT:-120}" # 2 minutes for CI to start +readonly CI_RUN_TIMEOUT="${CI_RUN_TIMEOUT:-1800}" # 30 minutes for CI to complete +readonly POLL_INTERVAL="${POLL_INTERVAL:-30}" # Poll every 30 seconds +readonly VERBOSE="${VERBOSE:-false}" + +# ============================================================================ +# Cleanup and Trap Handler +# ============================================================================ + +cleanup() { + local exit_code=$? + echo "" + echo "Cleaning up..." + + # Remove temp files + [ -n "$SIGNAL_LOG" ] && [ -f "$SIGNAL_LOG" ] && rm -f "$SIGNAL_LOG" + + # Stop Docker sandbox if running + if [ "$USE_SANDBOX" = "true" ]; then + docker sandbox stop claude 2>/dev/null || true + fi + + exit $exit_code +} + +trap cleanup EXIT SIGINT SIGTERM # ============================================================================ # Utilities @@ -64,6 +113,13 @@ Arguments: Example: $0 123 +Environment Variables: + USE_SANDBOX Docker sandbox mode (default: true) + YOLO_MODE Skip permission prompts (default: true) + MAX_CI_ITERATIONS Max fix iterations (default: 10) + CI_RUN_TIMEOUT CI run timeout in seconds (default: 1800) + VERBOSE Show raw jq output (default: false) + The script will: 1. Poll CI status for the PR 2. Fix failures with /github:fix-ci @@ -71,49 +127,80 @@ The script will: 4. Repeat up to $MAX_CI_ITERATIONS times 5. Abort if stuck (same errors 2x) -Configuration: - MAX_CI_ITERATIONS=$MAX_CI_ITERATIONS - CI_TIMEOUT=${CI_RUN_TIMEOUT}s per iteration - CI_START_TIMEOUT=${CI_START_TIMEOUT}s wait for CI start - EOF exit 1 } # ============================================================================ -# Argument Parsing +# Claude Invocation Function # ============================================================================ -if [ $# -eq 0 ]; then - usage -fi +run_claude() { + local prompt="$1" + local capture_file="${2:-}" -PR_NUMBER="$1" + # Build command + local cmd="" + if [ "$USE_SANDBOX" = "true" ]; then + cmd="docker sandbox run --credentials host claude" + else + cmd="claude" + fi -# Validate PR number is a positive integer -if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || [ "$PR_NUMBER" -le 0 ]; then - error "Invalid PR number: $PR_NUMBER (must be positive integer)" -fi + # Add YOLO mode + if [ "$YOLO_MODE" = "true" ]; then + cmd="$cmd --dangerously-skip-permissions" + fi -WORKFLOW_ID="pr-$PR_NUMBER" + # Add output format + cmd="$cmd --print --output-format stream-json" + + # Create temp file for capture + SIGNAL_LOG=$(mktemp "$TMPDIR/ralph-signal.XXXXXX") + + # Execute with streaming + capture + if [ "$VERBOSE" = "true" ]; then + eval "$cmd \"$prompt\"" 2>&1 \ + | grep --line-buffered '^{' \ + | tee "$SIGNAL_LOG" \ + | jq --unbuffered -rj "$JQ_STREAM" + else + eval "$cmd \"$prompt\"" 2>&1 \ + | grep --line-buffered '^{' \ + | tee "$SIGNAL_LOG" \ + | jq --unbuffered -rj "$JQ_STREAM" 2>/dev/null || true + fi -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "CI Ralph - Autonomous CI Resolution" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "PR Number: #$PR_NUMBER" -echo "Max Iterations: $MAX_CI_ITERATIONS" -echo "CI Timeout: ${CI_RUN_TIMEOUT}s" -echo "" + local exit_code=${PIPESTATUS[0]} + + # Extract final result if capture file specified + if [ -n "$capture_file" ]; then + jq -r "$JQ_RESULT" "$SIGNAL_LOG" > "$capture_file" 2>/dev/null || true + fi -notify "STARTED" "$WORKFLOW_ID" "CI_RESOLUTION" "Beginning CI fix loop for PR #$PR_NUMBER" + return $exit_code +} # ============================================================================ -# Utility Functions +# Signal Detection +# ============================================================================ + +check_completion() { + local log_file="$1" + + if grep -q 'COMPLETE' "$log_file" 2>/dev/null; then + return 0 # Complete + elif grep -q 'FAILED' "$log_file" 2>/dev/null; then + return 1 # Failed + fi + return 2 # Continue +} + +# ============================================================================ +# CI Utility Functions # ============================================================================ # Wait for CI to complete -# Args: pr_number, timeout_seconds -# Returns: 0 if complete, 1 if timeout wait_for_ci_complete() { local pr="$1" local timeout="$2" @@ -149,8 +236,6 @@ wait_for_ci_complete() { } # Check CI status -# Args: pr_number -# Returns: "passing", "failing", "pending", or "error" get_ci_status() { local pr="$1" @@ -183,9 +268,7 @@ get_ci_status() { echo "passing" } -# Get CI error summary -# Args: pr_number -# Returns: SHA256 hash of error messages (for detecting stuck state) +# Get CI error summary hash (for detecting stuck state) get_ci_errors_hash() { local pr="$1" @@ -203,11 +286,8 @@ get_ci_errors_hash() { } # Wait for new CI run to start after push -# Args: pr_number, push_timestamp -# Returns: 0 if started, 1 if timeout wait_for_ci_start() { local pr="$1" - local push_time="$2" local timeout="$CI_START_TIMEOUT" local start_time start_time=$(date +%s) @@ -219,11 +299,9 @@ wait_for_ci_start() { if [ $elapsed -ge $timeout ]; then echo "Warning: No new CI run detected after ${timeout}s" - echo "CI may not be configured for this PR" return 1 fi - # Check if CI has started running local ci_status ci_status=$(get_ci_status "$pr") @@ -237,17 +315,45 @@ wait_for_ci_start() { done } +# ============================================================================ +# Argument Parsing +# ============================================================================ + +if [ $# -eq 0 ]; then + usage +fi + +PR_NUMBER="$1" + +# Validate PR number is a positive integer +if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || [ "$PR_NUMBER" -le 0 ]; then + error "Invalid PR number: $PR_NUMBER (must be positive integer)" +fi + +WORKFLOW_ID="pr-$PR_NUMBER" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "CI Ralph - AFK Autonomous CI Resolution" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "PR Number: #$PR_NUMBER" +echo "Sandbox: $USE_SANDBOX" +echo "YOLO Mode: $YOLO_MODE" +echo "Max Iterations: $MAX_CI_ITERATIONS" +echo "CI Timeout: ${CI_RUN_TIMEOUT}s" +echo "" + +notify "STARTED" "$WORKFLOW_ID" "CI_RESOLUTION" "Beginning CI fix loop (sandbox=$USE_SANDBOX, yolo=$YOLO_MODE)" + # ============================================================================ # Main CI Fix Loop # ============================================================================ -ITERATION=1 LAST_ERROR_HASH="" CONSECUTIVE_SAME_ERRORS=0 -while [ $ITERATION -le $MAX_CI_ITERATIONS ]; do +for ((i=1; i<=MAX_CI_ITERATIONS; i++)); do echo "" - echo "━━━ CI Fix Iteration $ITERATION/$MAX_CI_ITERATIONS ━━━" + echo "━━━ CI Fix Iteration $i/$MAX_CI_ITERATIONS ━━━" echo "" # Check current CI status @@ -260,14 +366,14 @@ while [ $ITERATION -le $MAX_CI_ITERATIONS ]; do passing) echo "" echo "✓ CI is passing!" - notify "SUCCESS" "$WORKFLOW_ID" "CI_COMPLETE" "CI checks passed after $ITERATION iteration(s)" + notify "SUCCESS" "$WORKFLOW_ID" "CI_COMPLETE" "CI checks passed after $i iteration(s)" exit 0 ;; pending) echo "CI is still running, waiting for completion..." if ! wait_for_ci_complete "$PR_NUMBER" "$CI_RUN_TIMEOUT"; then - notify "ERROR" "$WORKFLOW_ID" "CI_TIMEOUT" "CI run timeout at iteration $ITERATION" + notify "ERROR" "$WORKFLOW_ID" "CI_TIMEOUT" "CI run timeout at iteration $i" error "CI run timed out after ${CI_RUN_TIMEOUT}s" fi # After wait completes, loop will re-check status @@ -280,13 +386,11 @@ while [ $ITERATION -le $MAX_CI_ITERATIONS ]; do # Get error hash for stuck detection CURRENT_ERROR_HASH=$(get_ci_errors_hash "$PR_NUMBER") - # Check if we're stuck on same errors (abort if same errors 2x in a row) + # Check if stuck on same errors if [ -n "$LAST_ERROR_HASH" ] && [ "$CURRENT_ERROR_HASH" = "$LAST_ERROR_HASH" ]; then CONSECUTIVE_SAME_ERRORS=$((CONSECUTIVE_SAME_ERRORS + 1)) echo "Warning: Same errors detected ($CONSECUTIVE_SAME_ERRORS consecutive time(s))" - # Abort if same errors detected twice in a row (CONSECUTIVE_SAME_ERRORS starts at 0, - # increments when same error seen, so >= 1 means second consecutive occurrence) if [ $CONSECUTIVE_SAME_ERRORS -ge 1 ]; then echo "" echo "ERROR: Stuck on same errors after 2 fix attempts" @@ -295,11 +399,10 @@ while [ $ITERATION -le $MAX_CI_ITERATIONS ]; do echo "Failed checks:" gh pr checks "$PR_NUMBER" 2>/dev/null | grep -E "(fail|×)" || true echo "" - notify "ERROR" "$WORKFLOW_ID" "CI_STUCK" "Stuck on same errors after $ITERATION iterations" + notify "ERROR" "$WORKFLOW_ID" "CI_STUCK" "Stuck on same errors after $i iterations" error "CI fix loop stuck - same errors detected twice in a row" fi else - # Different errors, reset counter CONSECUTIVE_SAME_ERRORS=0 fi @@ -308,65 +411,53 @@ while [ $ITERATION -le $MAX_CI_ITERATIONS ]; do # Invoke fix-ci command echo "" echo "Invoking /github:fix-ci to resolve failures..." - notify "PROGRESS" "$WORKFLOW_ID" "CI_FIX" "Attempting fix $ITERATION/$MAX_CI_ITERATIONS" + notify "PROGRESS" "$WORKFLOW_ID" "CI_FIX" "Attempting fix $i/$MAX_CI_ITERATIONS" - if ! claude -p "/github:fix-ci" --output-format stream-json 2>&1; then - echo "Warning: /github:fix-ci command failed" - notify "ERROR" "$WORKFLOW_ID" "CI_FIX_FAILED" "Fix command failed at iteration $ITERATION" - # Continue to next iteration anyway - fi + run_claude "/github:fix-ci" || true - # Stage and commit changes made by fix-ci + # Commit and push if changes echo "" echo "Staging and committing fixes..." - if git diff --quiet && git diff --staged --quiet; then - echo "No changes to commit (fix-ci may not have made changes)" - else + if ! git diff --quiet || ! git diff --staged --quiet; then git add . - if ! git commit -m "fix(ci): automated fix attempt $ITERATION + git commit -m "fix(ci): automated fix attempt $i -Applied fixes from /github:fix-ci (iteration $ITERATION/$MAX_CI_ITERATIONS)"; then - echo "Warning: git commit failed (may have no changes)" - fi - fi +Applied fixes from /github:fix-ci (iteration $i/$MAX_CI_ITERATIONS)" || echo "Warning: commit failed" - # Push changes - echo "" - echo "Pushing fixes to trigger new CI run..." - PUSH_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + # Push changes + echo "" + echo "Pushing fixes to trigger new CI run..." - if ! git push origin HEAD 2>&1; then - echo "Warning: git push failed" - notify "ERROR" "$WORKFLOW_ID" "PUSH_FAILED" "Push failed at iteration $ITERATION" - error "Failed to push changes for CI re-run" - fi + if ! git push origin HEAD 2>&1; then + notify "ERROR" "$WORKFLOW_ID" "PUSH_FAILED" "Push failed at iteration $i" + error "Failed to push changes for CI re-run" + fi - echo "Changes pushed at $PUSH_TIME" + echo "Changes pushed" - # Wait for new CI run to start - if ! wait_for_ci_start "$PR_NUMBER" "$PUSH_TIME"; then - echo "Warning: Could not detect new CI run" - echo "Waiting $POLL_INTERVAL seconds before checking status..." - sleep "$POLL_INTERVAL" - fi + # Wait for new CI run to start + if ! wait_for_ci_start "$PR_NUMBER"; then + echo "Warning: Could not detect new CI run" + sleep "$POLL_INTERVAL" + fi - # Wait for CI run to complete - echo "" - if ! wait_for_ci_complete "$PR_NUMBER" "$CI_RUN_TIMEOUT"; then - notify "ERROR" "$WORKFLOW_ID" "CI_TIMEOUT" "CI run timeout at iteration $ITERATION" - error "CI run timed out after ${CI_RUN_TIMEOUT}s" + # Wait for CI run to complete + if ! wait_for_ci_complete "$PR_NUMBER" "$CI_RUN_TIMEOUT"; then + notify "ERROR" "$WORKFLOW_ID" "CI_TIMEOUT" "CI run timeout at iteration $i" + error "CI run timed out after ${CI_RUN_TIMEOUT}s" + fi + else + echo "No changes to commit (fix-ci may not have made changes)" fi ;; error) - echo "Error: Could not get CI status (PR may not exist or no checks configured)" + echo "Error: Could not get CI status" notify "ERROR" "$WORKFLOW_ID" "CI_STATUS_ERROR" "Could not get CI status for PR #$PR_NUMBER" error "Failed to get CI status for PR #$PR_NUMBER" ;; esac - - ITERATION=$((ITERATION + 1)) done # Max iterations reached diff --git a/scripts/comments-ralph.sh b/scripts/comments-ralph.sh index 5ed52ff..013fab6 100755 --- a/scripts/comments-ralph.sh +++ b/scripts/comments-ralph.sh @@ -1,24 +1,49 @@ #!/usr/bin/env bash # -# comments-ralph.sh - Autonomous comment resolution loop +# comments-ralph.sh - Autonomous comment resolution loop (AFK Ralph pattern) # # Continuously monitors and resolves PR review comments until all resolved or max iterations. # Called by workflow-ralph.sh after CI passes. # +# Features: +# - Docker sandbox mode for AFK safety (USE_SANDBOX=true) +# - YOLO mode to skip permission prompts (YOLO_MODE=true) +# - jq streaming for real-time output +# # Usage: # ./scripts/comments-ralph.sh # -# Flow: -# 1. Parse and validate PR number argument -# 2. Count pending/unresolved comments -# 3. If pending comments exist, invoke /workflows:resolve-comments -# 4. Stage, commit, and push changes -# 5. Wait for reviewer response (configurable interval) -# 6. Repeat up to MAX_COMMENT_ITERATIONS times -# 7. Exit with success when all resolved, failure on max iterations +# # Full AFK mode (Docker + YOLO) - default +# ./comments-ralph.sh 123 +# +# # No sandbox (faster, less safe) +# USE_SANDBOX=false ./comments-ralph.sh 123 +# +# # Interactive mode (keep permission prompts) +# YOLO_MODE=false ./comments-ralph.sh 123 set -euo pipefail +# ============================================================================ +# AFK Ralph Configuration +# ============================================================================ + +# Docker sandbox mode (default: enabled for AFK safety) +USE_SANDBOX="${USE_SANDBOX:-true}" + +# YOLO mode - skip all permission prompts +YOLO_MODE="${YOLO_MODE:-true}" + +# Temp files for signal capture +TMPDIR="${TMPDIR:-/tmp}" +SIGNAL_LOG="" + +# jq filter for streaming assistant text +JQ_STREAM='select(.type == "assistant").message.content[]? | select(.type == "text").text // empty' + +# jq filter for final result +JQ_RESULT='select(.type == "result").result // empty' + # ============================================================================ # Configuration # ============================================================================ @@ -26,6 +51,29 @@ set -euo pipefail readonly NOTIFICATION_LOG="$HOME/.workflow-notifications.log" readonly MAX_COMMENT_ITERATIONS="${MAX_COMMENT_ITERATIONS:-10}" readonly REVIEWER_WAIT_TIME="${REVIEWER_WAIT_TIME:-300}" # 5 minutes default +readonly VERBOSE="${VERBOSE:-false}" + +# ============================================================================ +# Cleanup and Trap Handler +# ============================================================================ + +cleanup() { + local exit_code=$? + echo "" + echo "Cleaning up..." + + # Remove temp files + [ -n "$SIGNAL_LOG" ] && [ -f "$SIGNAL_LOG" ] && rm -f "$SIGNAL_LOG" + + # Stop Docker sandbox if running + if [ "$USE_SANDBOX" = "true" ]; then + docker sandbox stop claude 2>/dev/null || true + fi + + exit $exit_code +} + +trap cleanup EXIT SIGINT SIGTERM # ============================================================================ # Utilities @@ -62,6 +110,13 @@ Arguments: Example: $0 123 +Environment Variables: + USE_SANDBOX Docker sandbox mode (default: true) + YOLO_MODE Skip permission prompts (default: true) + MAX_COMMENT_ITERATIONS Max resolution cycles (default: 10) + REVIEWER_WAIT_TIME Wait time between cycles in seconds (default: 300) + VERBOSE Show raw jq output (default: false) + The script will: 1. Count pending/unresolved PR comments 2. Invoke /workflows:resolve-comments if needed @@ -70,53 +125,81 @@ The script will: 5. Repeat up to $MAX_COMMENT_ITERATIONS times 6. Exit success when all comments resolved -Environment Variables: - MAX_COMMENT_ITERATIONS Max resolution cycles (default: 10) - REVIEWER_WAIT_TIME Wait time between cycles in seconds (default: 300) - -Configuration: - MAX_COMMENT_ITERATIONS=$MAX_COMMENT_ITERATIONS - REVIEWER_WAIT_TIME=${REVIEWER_WAIT_TIME}s between resolution cycles - EOF exit 1 } # ============================================================================ -# Argument Parsing +# Claude Invocation Function # ============================================================================ -if [ $# -eq 0 ]; then - usage -fi +run_claude() { + local prompt="$1" + local capture_file="${2:-}" -PR_NUMBER="$1" + # Build command + local cmd="" + if [ "$USE_SANDBOX" = "true" ]; then + cmd="docker sandbox run --credentials host claude" + else + cmd="claude" + fi -# Validate PR number is a positive integer -if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || [ "$PR_NUMBER" -le 0 ]; then - error "Invalid PR number: $PR_NUMBER (must be positive integer)" -fi + # Add YOLO mode + if [ "$YOLO_MODE" = "true" ]; then + cmd="$cmd --dangerously-skip-permissions" + fi -WORKFLOW_ID="pr-$PR_NUMBER" + # Add output format + cmd="$cmd --print --output-format stream-json" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "Comments Ralph - Autonomous Comment Resolution" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "PR Number: #$PR_NUMBER" -echo "Max Iterations: $MAX_COMMENT_ITERATIONS" -echo "Reviewer Wait Time: ${REVIEWER_WAIT_TIME}s" -echo "" + # Create temp file for capture + SIGNAL_LOG=$(mktemp "$TMPDIR/ralph-signal.XXXXXX") + + # Execute with streaming + capture + if [ "$VERBOSE" = "true" ]; then + eval "$cmd \"$prompt\"" 2>&1 \ + | grep --line-buffered '^{' \ + | tee "$SIGNAL_LOG" \ + | jq --unbuffered -rj "$JQ_STREAM" + else + eval "$cmd \"$prompt\"" 2>&1 \ + | grep --line-buffered '^{' \ + | tee "$SIGNAL_LOG" \ + | jq --unbuffered -rj "$JQ_STREAM" 2>/dev/null || true + fi + + local exit_code=${PIPESTATUS[0]} + + # Extract final result if capture file specified + if [ -n "$capture_file" ]; then + jq -r "$JQ_RESULT" "$SIGNAL_LOG" > "$capture_file" 2>/dev/null || true + fi -notify "STARTED" "$WORKFLOW_ID" "COMMENT_RESOLUTION" "Beginning comment resolution loop for PR #$PR_NUMBER" + return $exit_code +} # ============================================================================ -# Utility Functions +# Signal Detection +# ============================================================================ + +check_completion() { + local log_file="$1" + + if grep -q 'COMPLETE' "$log_file" 2>/dev/null; then + return 0 # Complete + elif grep -q 'FAILED' "$log_file" 2>/dev/null; then + return 1 # Failed + fi + return 2 # Continue +} + +# ============================================================================ +# Comment Utility Functions # ============================================================================ # Get repository info (owner/repo) -# Returns: "owner/repo" format get_repo_info() { - # Try to get from gh CLI local repo repo=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null) @@ -133,23 +216,15 @@ get_repo_info() { } # Count pending PR comments -# Args: pr_number -# Returns: integer count of unresolved comments count_pending_comments() { local pr="$1" local repo repo=$(get_repo_info) - # Get PR review comments and regular comments - # Review comments can have an in_reply_to_id (threaded) - # We'll count top-level unresolved review comments + # Get PR review comments (top-level only) local review_comments review_comments=$(gh api "repos/$repo/pulls/$pr/comments" --jq '[.[] | select(.in_reply_to_id == null)] | length' 2>/dev/null || echo "0") - # Get PR issue comments (general comments not tied to code) - local issue_comments - issue_comments=$(gh api "repos/$repo/issues/$pr/comments" --jq 'length' 2>/dev/null || echo "0") - # Get review status local reviews_json reviews_json=$(gh pr view "$pr" --json reviews -q '.reviews' 2>/dev/null || echo "[]") @@ -158,116 +233,112 @@ count_pending_comments() { local changes_requested changes_requested=$(echo "$reviews_json" | jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0") - # Total pending = review comments + issue comments + changes requested reviews - # Note: This is a simplistic count. In real scenarios, we'd filter for resolved vs unresolved. - # For MVP, we'll count all review comments + changes_requested reviews + # Total pending = review comments + changes_requested reviews local total=$((review_comments + changes_requested)) echo "$total" } -# Check if PR has pending comments or requested changes -# Args: pr_number -# Returns: 0 if comments exist, 1 if none -has_pending_comments() { - local pr="$1" - local count - count=$(count_pending_comments "$pr") +# ============================================================================ +# Argument Parsing +# ============================================================================ - if [ "$count" -gt 0 ]; then - return 0 - else - return 1 - fi -} +if [ $# -eq 0 ]; then + usage +fi + +PR_NUMBER="$1" + +# Validate PR number is a positive integer +if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || [ "$PR_NUMBER" -le 0 ]; then + error "Invalid PR number: $PR_NUMBER (must be positive integer)" +fi + +WORKFLOW_ID="pr-$PR_NUMBER" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Comments Ralph - AFK Autonomous Comment Resolution" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "PR Number: #$PR_NUMBER" +echo "Sandbox: $USE_SANDBOX" +echo "YOLO Mode: $YOLO_MODE" +echo "Max Iterations: $MAX_COMMENT_ITERATIONS" +echo "Reviewer Wait Time: ${REVIEWER_WAIT_TIME}s" +echo "" + +notify "STARTED" "$WORKFLOW_ID" "COMMENT_RESOLUTION" "Beginning comment resolution loop (sandbox=$USE_SANDBOX, yolo=$YOLO_MODE)" # ============================================================================ # Main Comment Resolution Loop # ============================================================================ -ITERATION=1 - -while [ $ITERATION -le $MAX_COMMENT_ITERATIONS ]; do +for ((i=1; i<=MAX_COMMENT_ITERATIONS; i++)); do echo "" - echo "━━━ Comment Resolution Iteration $ITERATION/$MAX_COMMENT_ITERATIONS ━━━" + echo "━━━ Comment Resolution $i/$MAX_COMMENT_ITERATIONS ━━━" echo "" # Count pending comments echo "Checking for pending comments on PR #$PR_NUMBER..." - PENDING_COUNT=$(count_pending_comments "$PR_NUMBER") + PENDING=$(count_pending_comments "$PR_NUMBER") - echo "Pending comments/reviews: $PENDING_COUNT" + echo "Pending comments/reviews: $PENDING" - if [ "$PENDING_COUNT" -eq 0 ]; then + if [ "$PENDING" -eq 0 ]; then echo "" - echo "✓ No pending comments or requested changes!" - notify "SUCCESS" "$WORKFLOW_ID" "COMMENTS_RESOLVED" "All comments resolved after $ITERATION iteration(s)" + echo "✓ All comments resolved!" + notify "SUCCESS" "$WORKFLOW_ID" "COMMENTS_RESOLVED" "All comments resolved after $i iteration(s)" exit 0 fi # Comments exist, invoke resolve-comments command echo "" - echo "Found $PENDING_COUNT pending comment(s)/review(s)" - echo "Invoking /workflows:resolve-comments to address feedback..." - notify "PROGRESS" "$WORKFLOW_ID" "RESOLVE_COMMENTS" "Resolving comments (iteration $ITERATION/$MAX_COMMENT_ITERATIONS)" - - if ! claude -p "/workflows:resolve-comments $PR_NUMBER" --output-format stream-json 2>&1; then - echo "Warning: /workflows:resolve-comments command failed" - notify "ERROR" "$WORKFLOW_ID" "RESOLVE_FAILED" "Command failed at iteration $ITERATION" - # Continue to next iteration anyway - fi + echo "Resolving $PENDING pending comments..." + notify "PROGRESS" "$WORKFLOW_ID" "RESOLVE_COMMENTS" "Resolving comments (iteration $i/$MAX_COMMENT_ITERATIONS)" + + run_claude "/workflows:resolve-comments $PR_NUMBER --all" || true - # Stage and commit changes made by resolve-comments + # Commit and push if changes echo "" echo "Staging and committing resolution changes..." - if git diff --quiet && git diff --staged --quiet; then - echo "No changes to commit (resolve-comments may not have made changes)" - echo "This could indicate:" - echo " - Comments are discussion-only (no code changes needed)" - echo " - Comments were already resolved" - echo " - Resolution requires manual intervention" - echo "" - echo "Continuing to next iteration..." - else + if ! git diff --quiet || ! git diff --staged --quiet; then git add . - if ! git commit -m "fix(review): address review comments (iteration $ITERATION) + git commit -m "fix(review): address comments (attempt $i) -Applied changes from /workflows:resolve-comments (iteration $ITERATION/$MAX_COMMENT_ITERATIONS)"; then - echo "Warning: git commit failed" - fi +Applied changes from /workflows:resolve-comments (iteration $i/$MAX_COMMENT_ITERATIONS)" || echo "Warning: commit failed" # Push changes echo "" echo "Pushing resolution changes..." - PUSH_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") if ! git push origin HEAD 2>&1; then - echo "Warning: git push failed" - notify "ERROR" "$WORKFLOW_ID" "PUSH_FAILED" "Push failed at iteration $ITERATION" + notify "ERROR" "$WORKFLOW_ID" "PUSH_FAILED" "Push failed at iteration $i" error "Failed to push changes" fi - echo "Changes pushed at $PUSH_TIME" + echo "Changes pushed" + else + echo "No changes to commit (resolve-comments may not have made changes)" + echo "This could indicate:" + echo " - Comments are discussion-only" + echo " - Comments were already resolved" + echo " - Resolution requires manual intervention" fi # Wait for reviewer response before checking again - # Only wait if we haven't reached max iterations - if [ $ITERATION -lt $MAX_COMMENT_ITERATIONS ]; then + if [ $i -lt $MAX_COMMENT_ITERATIONS ]; then echo "" - echo "Waiting ${REVIEWER_WAIT_TIME}s for reviewer response..." - notify "PROGRESS" "$WORKFLOW_ID" "WAITING" "Waiting for reviewer feedback (iteration $ITERATION)" + echo "Waiting ${REVIEWER_WAIT_TIME}s for reviewer..." + notify "PROGRESS" "$WORKFLOW_ID" "WAITING" "Waiting for reviewer feedback (iteration $i)" # Show countdown in minutes for long waits if [ $REVIEWER_WAIT_TIME -ge 60 ]; then - local minutes=$((REVIEWER_WAIT_TIME / 60)) + minutes=$((REVIEWER_WAIT_TIME / 60)) echo "Waiting $minutes minute(s)..." fi sleep "$REVIEWER_WAIT_TIME" fi - - ITERATION=$((ITERATION + 1)) done # Max iterations reached diff --git a/scripts/workflow-ralph.sh b/scripts/workflow-ralph.sh index e2ba534..6a0c989 100755 --- a/scripts/workflow-ralph.sh +++ b/scripts/workflow-ralph.sh @@ -1,31 +1,84 @@ #!/usr/bin/env bash # -# workflow-ralph.sh - Autonomous workflow orchestrator +# workflow-ralph.sh - Autonomous workflow orchestrator (AFK Ralph pattern) # # The "Ralph Wiggum" pattern: Run and walk away while the workflow completes. # This script orchestrates the full SDLC pipeline from research to PR submission, # handling CI failures and comment resolution autonomously. # +# Features: +# - Docker sandbox mode for AFK safety (USE_SANDBOX=true) +# - YOLO mode to skip permission prompts (YOLO_MODE=true) +# - jq streaming for real-time output +# - Signal-based completion detection +# # Usage: # ./scripts/workflow-ralph.sh research/my-feature.md # -# Flow: -# 1. Parse arguments and validate research file -# 2. Loop /workflows:build until PR_CREATED signal -# 3. Call ci-ralph.sh for CI resolution -# 4. Call comments-ralph.sh for comment resolution -# 5. Log success/failure to notification log +# # Full AFK mode (Docker + YOLO) - default +# ./workflow-ralph.sh research/my-feature.md +# +# # No sandbox (faster, less safe) +# USE_SANDBOX=false ./workflow-ralph.sh research/my-feature.md +# +# # Interactive mode (keep permission prompts) +# YOLO_MODE=false ./workflow-ralph.sh research/my-feature.md set -euo pipefail +# ============================================================================ +# AFK Ralph Configuration +# ============================================================================ + +# Docker sandbox mode (default: enabled for AFK safety) +USE_SANDBOX="${USE_SANDBOX:-true}" + +# YOLO mode - skip all permission prompts +YOLO_MODE="${YOLO_MODE:-true}" + +# Temp files for signal capture +TMPDIR="${TMPDIR:-/tmp}" +SIGNAL_LOG="" +RESULT_FILE="" + +# jq filter for streaming assistant text +JQ_STREAM='select(.type == "assistant").message.content[]? | select(.type == "text").text // empty' + +# jq filter for final result +JQ_RESULT='select(.type == "result").result // empty' + # ============================================================================ # Configuration # ============================================================================ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly NOTIFICATION_LOG="$HOME/.workflow-notifications.log" -readonly MAX_BUILD_ITERATIONS=10 +readonly MAX_BUILD_ITERATIONS="${MAX_BUILD_ITERATIONS:-10}" readonly PROGRESS_FILE=".workflow-progress.txt" +readonly VERBOSE="${VERBOSE:-false}" + +# ============================================================================ +# Cleanup and Trap Handler +# ============================================================================ + +cleanup() { + local exit_code=$? + echo "" + echo "Cleaning up..." + + # Remove temp files + [ -n "$SIGNAL_LOG" ] && [ -f "$SIGNAL_LOG" ] && rm -f "$SIGNAL_LOG" + [ -n "$RESULT_FILE" ] && [ -f "$RESULT_FILE" ] && rm -f "$RESULT_FILE" + + # Stop Docker sandbox if running + if [ "$USE_SANDBOX" = "true" ]; then + docker sandbox stop claude 2>/dev/null || true + fi + + exit $exit_code +} + +trap cleanup EXIT SIGINT SIGTERM # ============================================================================ # Utilities @@ -62,6 +115,12 @@ Arguments: Example: $0 research/my-feature.md +Environment Variables: + USE_SANDBOX Docker sandbox mode (default: true) + YOLO_MODE Skip permission prompts (default: true) + MAX_BUILD_ITERATIONS Max build iterations (default: 10) + VERBOSE Show raw jq output (default: false) + The script will: 1. Run /workflows:build in a loop until PR is created 2. Resolve CI failures with ci-ralph.sh @@ -72,6 +131,80 @@ EOF exit 1 } +# ============================================================================ +# Claude Invocation Function +# ============================================================================ + +run_claude() { + local prompt="$1" + local capture_file="${2:-}" + + # Build command + local cmd="" + if [ "$USE_SANDBOX" = "true" ]; then + cmd="docker sandbox run --credentials host claude" + else + cmd="claude" + fi + + # Add YOLO mode + if [ "$YOLO_MODE" = "true" ]; then + cmd="$cmd --dangerously-skip-permissions" + fi + + # Add output format + cmd="$cmd --print --output-format stream-json" + + # Create temp file for capture + SIGNAL_LOG=$(mktemp "$TMPDIR/ralph-signal.XXXXXX") + + # Execute with streaming + capture + if [ "$VERBOSE" = "true" ]; then + eval "$cmd \"$prompt\"" 2>&1 \ + | grep --line-buffered '^{' \ + | tee "$SIGNAL_LOG" \ + | jq --unbuffered -rj "$JQ_STREAM" + else + eval "$cmd \"$prompt\"" 2>&1 \ + | grep --line-buffered '^{' \ + | tee "$SIGNAL_LOG" \ + | jq --unbuffered -rj "$JQ_STREAM" 2>/dev/null || true + fi + + local exit_code=${PIPESTATUS[0]} + + # Extract final result if capture file specified + if [ -n "$capture_file" ]; then + jq -r "$JQ_RESULT" "$SIGNAL_LOG" > "$capture_file" 2>/dev/null || true + fi + + return $exit_code +} + +# ============================================================================ +# Signal Detection +# ============================================================================ + +check_completion() { + local log_file="$1" + + if grep -q 'COMPLETE' "$log_file" 2>/dev/null; then + return 0 # Complete + elif grep -q 'FAILED' "$log_file" 2>/dev/null; then + return 1 # Failed + fi + return 2 # Continue +} + +check_pr_created() { + local log_file="$1" + + if grep -qE 'PR_CREATED|pull request.*created|PR_CREATED' "$log_file" 2>/dev/null; then + return 0 + fi + return 1 +} + # ============================================================================ # Argument Parsing # ============================================================================ @@ -88,19 +221,19 @@ if [ ! -f "$RESEARCH_FILE" ]; then fi # Extract workflow ID from research filename -# Example: research/my-feature.md -> my-feature -# If filename contains timestamp, extract that for uniqueness WORKFLOW_ID=$(basename "$RESEARCH_FILE" .md) echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "Workflow Ralph - Autonomous Orchestrator" +echo "Workflow Ralph - AFK Autonomous Orchestrator" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Research: $RESEARCH_FILE" echo "Workflow ID: $WORKFLOW_ID" +echo "Sandbox: $USE_SANDBOX" +echo "YOLO Mode: $YOLO_MODE" echo "Notifications: $NOTIFICATION_LOG" echo "" -notify "STARTED" "$WORKFLOW_ID" "INIT" "Beginning workflow execution" +notify "STARTED" "$WORKFLOW_ID" "INIT" "Beginning workflow execution (sandbox=$USE_SANDBOX, yolo=$YOLO_MODE)" # ============================================================================ # Phase 1: Build Loop (until PR created) @@ -110,86 +243,52 @@ echo "Phase 1: Build Loop" echo "Running /workflows:build until PR is created..." echo "" -BUILD_ITERATION=1 -PR_NUMBER="" BUILD_COMPLETE=false -while [ $BUILD_ITERATION -le $MAX_BUILD_ITERATIONS ]; do - echo "━━━ Build Iteration $BUILD_ITERATION/$MAX_BUILD_ITERATIONS ━━━" - - # Run claude with /workflows:build and parse signals - # Using -p (print mode) for non-interactive execution - # Using --output-format stream-json for signal parsing - # Signals are emitted as: SIGNAL_NAME - - if claude -p "/workflows:build \"$RESEARCH_FILE\"" --output-format stream-json 2>&1 | while IFS= read -r line; do - # Echo line for visibility - echo "$line" - - # Parse phase signals using grep/sed - # Signal format: SIGNAL_NAME - if echo "$line" | grep -q '.*'; then - SIGNAL=$(echo "$line" | sed -n 's/.*\(.*\)<\/phase>.*/\1/p') - - case "$SIGNAL" in - SETUP_COMPLETE) - notify "PROGRESS" "$WORKFLOW_ID" "SETUP" "Worktree and dependencies initialized" - ;; - PLANNING_COMPLETE) - notify "PROGRESS" "$WORKFLOW_ID" "PLANNING" "Implementation plans generated" - ;; - IMPLEMENTATION_COMPLETE) - notify "PROGRESS" "$WORKFLOW_ID" "IMPLEMENTATION" "All plans implemented" - ;; - SUBMISSION_COMPLETE) - notify "PROGRESS" "$WORKFLOW_ID" "SUBMISSION" "PR created and pushed" - ;; - PR_CREATED) - # This is our signal to exit the build loop - notify "PROGRESS" "$WORKFLOW_ID" "PR_CREATED" "Pull request created successfully" - echo "PR_CREATED signal detected - exiting build loop" - exit 0 # Exit the while read loop - ;; - WORKFLOW_COMPLETE) - notify "SUCCESS" "$WORKFLOW_ID" "COMPLETE" "Workflow finished successfully" - echo "WORKFLOW_COMPLETE signal detected" - exit 0 - ;; - ERROR:*) - # Parse error signal: ERROR:phase:message - ERROR_PHASE=$(echo "$SIGNAL" | cut -d: -f2) - ERROR_MSG=$(echo "$SIGNAL" | cut -d: -f3-) - notify "ERROR" "$WORKFLOW_ID" "$ERROR_PHASE" "$ERROR_MSG" - echo "ERROR signal detected: $ERROR_MSG" - exit 1 - ;; - esac - fi - done; then - # Pipe succeeded (got PR_CREATED or WORKFLOW_COMPLETE) +for ((i=1; i<=MAX_BUILD_ITERATIONS; i++)); do + echo "" + echo "━━━ Build Iteration $i/$MAX_BUILD_ITERATIONS ━━━" + + RESULT_FILE=$(mktemp "$TMPDIR/ralph-result.XXXXXX") + + run_claude "/workflows:build \"$RESEARCH_FILE\" --continue" "$RESULT_FILE" || true + + # Check signals in captured output + check_completion "$SIGNAL_LOG" + completion_status=$? + + if [ $completion_status -eq 0 ]; then + echo "" + echo "Ralph complete after $i iterations." + notify "SUCCESS" "$WORKFLOW_ID" "COMPLETE" "Completed in $i iterations" BUILD_COMPLETE=true break - else - # Pipe failed (error occurred) - notify "ERROR" "$WORKFLOW_ID" "BUILD" "Build command failed at iteration $BUILD_ITERATION" - error "Build loop failed at iteration $BUILD_ITERATION" + elif [ $completion_status -eq 1 ]; then + echo "" + echo "Workflow failed signal detected." + notify "ERROR" "$WORKFLOW_ID" "BUILD" "Workflow failed at iteration $i" + error "Workflow failed - check logs for details" fi - # Safety check: if we're still here, increment and continue - BUILD_ITERATION=$((BUILD_ITERATION + 1)) - - if [ $BUILD_ITERATION -gt $MAX_BUILD_ITERATIONS ]; then - notify "ERROR" "$WORKFLOW_ID" "BUILD" "Max build iterations reached ($MAX_BUILD_ITERATIONS)" - error "Maximum build iterations ($MAX_BUILD_ITERATIONS) reached without PR creation" + # Check for PR_CREATED in output + if check_pr_created "$SIGNAL_LOG"; then + echo "" + echo "PR created - moving to CI resolution..." + BUILD_COMPLETE=true + break fi + # Clean up temp files from this iteration + [ -f "$RESULT_FILE" ] && rm -f "$RESULT_FILE" + echo "" - echo "Build iteration complete, checking if PR was created..." + echo "Build iteration $i complete, continuing..." sleep 2 done if [ "$BUILD_COMPLETE" = false ]; then - error "Build loop did not complete successfully" + notify "ERROR" "$WORKFLOW_ID" "BUILD" "Max build iterations reached ($MAX_BUILD_ITERATIONS)" + error "Maximum build iterations ($MAX_BUILD_ITERATIONS) reached without PR creation" fi echo "" @@ -205,7 +304,6 @@ echo "Reading $PROGRESS_FILE for PR information..." echo "" # Find the worktree directory from progress file or use default location -# Progress file should be at worktree root, need to locate it WORKTREE_DIR="" # Try common locations for progress file @@ -225,7 +323,6 @@ if [ -z "$WORKTREE_DIR" ] || [ ! -f "$WORKTREE_DIR/$PROGRESS_FILE" ]; then fi # Extract PR number from progress file -# Format: "number: 123" in the PR section PR_NUMBER=$(awk '/^## PR/,/^##/ {if (/^number:/) print $2}' "$WORKTREE_DIR/$PROGRESS_FILE" | head -1) if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then @@ -249,10 +346,11 @@ CI_SCRIPT="$SCRIPT_DIR/ci-ralph.sh" if [ ! -f "$CI_SCRIPT" ]; then echo "Warning: ci-ralph.sh not found at $CI_SCRIPT" - echo "Skipping CI resolution phase (will be implemented in future)" - notify "SKIPPED" "$WORKFLOW_ID" "CI_RESOLUTION" "ci-ralph.sh not yet implemented" + echo "Skipping CI resolution phase" + notify "SKIPPED" "$WORKFLOW_ID" "CI_RESOLUTION" "ci-ralph.sh not found" else - if "$CI_SCRIPT" "$PR_NUMBER"; then + # Pass environment variables to child script + if USE_SANDBOX="$USE_SANDBOX" YOLO_MODE="$YOLO_MODE" "$CI_SCRIPT" "$PR_NUMBER"; then notify "PROGRESS" "$WORKFLOW_ID" "CI_RESOLUTION" "CI checks passed for PR #$PR_NUMBER" echo "CI resolution complete" else @@ -275,10 +373,11 @@ COMMENTS_SCRIPT="$SCRIPT_DIR/comments-ralph.sh" if [ ! -f "$COMMENTS_SCRIPT" ]; then echo "Warning: comments-ralph.sh not found at $COMMENTS_SCRIPT" - echo "Skipping comment resolution phase (will be implemented in future)" - notify "SKIPPED" "$WORKFLOW_ID" "COMMENT_RESOLUTION" "comments-ralph.sh not yet implemented" + echo "Skipping comment resolution phase" + notify "SKIPPED" "$WORKFLOW_ID" "COMMENT_RESOLUTION" "comments-ralph.sh not found" else - if "$COMMENTS_SCRIPT" "$PR_NUMBER"; then + # Pass environment variables to child script + if USE_SANDBOX="$USE_SANDBOX" YOLO_MODE="$YOLO_MODE" "$COMMENTS_SCRIPT" "$PR_NUMBER"; then notify "PROGRESS" "$WORKFLOW_ID" "COMMENT_RESOLUTION" "Comments resolved for PR #$PR_NUMBER" echo "Comment resolution complete" else