diff --git a/.agents/scripts/archived/quality-loop-helper.sh b/.agents/scripts/archived/quality-loop-helper.sh index 6eb2909069..8b118bdc88 100755 --- a/.agents/scripts/archived/quality-loop-helper.sh +++ b/.agents/scripts/archived/quality-loop-helper.sh @@ -42,8 +42,8 @@ readonly LEGACY_STATE_DIR=".claude" # Default settings readonly DEFAULT_MAX_ITERATIONS=10 -readonly DEFAULT_MONITOR_DURATION=300 # 5 minutes in seconds -readonly DEFAULT_REVIEW_STALE_THRESHOLD=300 # 5 minutes - trigger re-review if no activity +readonly DEFAULT_MONITOR_DURATION=300 # 5 minutes in seconds +readonly DEFAULT_REVIEW_STALE_THRESHOLD=300 # 5 minutes - trigger re-review if no activity # ============================================================================= # CI/CD Service Timing Constants (Evidence-Based from PR #19 Analysis) @@ -95,9 +95,9 @@ readonly BACKOFF_MULTIPLIER=2 # Arguments: $1 - Step message # Returns: 0 print_step() { - local message="$1" - echo -e "${CYAN}[quality-loop]${NC} ${message}" - return 0 + local message="$1" + echo -e "${CYAN}[quality-loop]${NC} ${message}" + return 0 } # ============================================================================= @@ -110,23 +110,23 @@ print_step() { # Returns: 0 # Output: Recommended wait time in seconds calculate_adaptive_wait() { - local pending_services="$1" - local max_wait=0 - - # Check for slow services first (they dominate wait time) - if echo "$pending_services" | grep -qiE "$SLOW_SERVICES"; then - max_wait=$WAIT_SLOW - elif echo "$pending_services" | grep -qiE "$MEDIUM_SERVICES"; then - max_wait=$WAIT_MEDIUM - elif echo "$pending_services" | grep -qiE "$FAST_SERVICES"; then - max_wait=$WAIT_FAST - else - # Unknown service, use medium as default - max_wait=$WAIT_MEDIUM - fi - - echo "$max_wait" - return 0 + local pending_services="$1" + local max_wait=0 + + # Check for slow services first (they dominate wait time) + if echo "$pending_services" | grep -qiE "$SLOW_SERVICES"; then + max_wait=$WAIT_SLOW + elif echo "$pending_services" | grep -qiE "$MEDIUM_SERVICES"; then + max_wait=$WAIT_MEDIUM + elif echo "$pending_services" | grep -qiE "$FAST_SERVICES"; then + max_wait=$WAIT_FAST + else + # Unknown service, use medium as default + max_wait=$WAIT_MEDIUM + fi + + echo "$max_wait" + return 0 } # Calculate poll interval based on pending services @@ -135,19 +135,19 @@ calculate_adaptive_wait() { # Returns: 0 # Output: Recommended poll interval in seconds calculate_poll_interval() { - local pending_services="$1" - local poll_interval=$POLL_MEDIUM - - if echo "$pending_services" | grep -qiE "$SLOW_SERVICES"; then - poll_interval=$POLL_SLOW - elif echo "$pending_services" | grep -qiE "$MEDIUM_SERVICES"; then - poll_interval=$POLL_MEDIUM - elif echo "$pending_services" | grep -qiE "$FAST_SERVICES"; then - poll_interval=$POLL_FAST - fi - - echo "$poll_interval" - return 0 + local pending_services="$1" + local poll_interval=$POLL_MEDIUM + + if echo "$pending_services" | grep -qiE "$SLOW_SERVICES"; then + poll_interval=$POLL_SLOW + elif echo "$pending_services" | grep -qiE "$MEDIUM_SERVICES"; then + poll_interval=$POLL_MEDIUM + elif echo "$pending_services" | grep -qiE "$FAST_SERVICES"; then + poll_interval=$POLL_FAST + fi + + echo "$poll_interval" + return 0 } # Calculate exponential backoff wait time @@ -156,22 +156,22 @@ calculate_poll_interval() { # Returns: 0 # Output: Wait time in seconds (capped at BACKOFF_MAX) calculate_backoff_wait() { - local iteration="$1" - local wait_time=$BACKOFF_BASE - - # Calculate: base * multiplier^(iteration-1), capped at max - local i=1 - while [[ $i -lt $iteration ]]; do - wait_time=$((wait_time * BACKOFF_MULTIPLIER)) - if [[ $wait_time -ge $BACKOFF_MAX ]]; then - wait_time=$BACKOFF_MAX - break - fi - ((i++)) - done - - echo "$wait_time" - return 0 + local iteration="$1" + local wait_time=$BACKOFF_BASE + + # Calculate: base * multiplier^(iteration-1), capped at max + local i=1 + while [[ $i -lt $iteration ]]; do + wait_time=$((wait_time * BACKOFF_MULTIPLIER)) + if [[ $wait_time -ge $BACKOFF_MAX ]]; then + wait_time=$BACKOFF_MAX + break + fi + ((i++)) + done + + echo "$wait_time" + return 0 } # Get list of pending CI check names from PR @@ -180,16 +180,16 @@ calculate_backoff_wait() { # Returns: 0 # Output: Comma-separated list of pending check names (lowercase) get_pending_checks() { - local pr_number="$1" - - local pr_info - pr_info=$(gh pr view "$pr_number" --json statusCheckRollup || echo '{"statusCheckRollup":[]}') - - local pending - pending=$(echo "$pr_info" | jq -r '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS") | .name] | join(",")' 2>/dev/null | tr '[:upper:]' '[:lower:]') - - echo "$pending" - return 0 + local pr_number="$1" + + local pr_info + pr_info=$(gh pr view "$pr_number" --json statusCheckRollup || echo '{"statusCheckRollup":[]}') + + local pending + pending=$(echo "$pr_info" | jq -r '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS") | .name] | join(",")' 2>/dev/null | tr '[:upper:]' '[:lower:]') + + echo "$pending" + return 0 } # ============================================================================= @@ -204,28 +204,28 @@ get_pending_checks() { # Returns: 0 # Side effects: Creates .agents/loop-state/quality-loop.local.state create_state() { - local loop_type="$1" - local max_iterations="$2" - local options_str="$3" - - mkdir -p "$STATE_DIR" - - # Convert options string to YAML object format - # Input: "auto_fix=true,wait_for_ci=false" -> " auto_fix: true\n wait_for_ci: false" - local options_yaml="" - if [[ -n "$options_str" ]]; then - options_yaml=$(echo "$options_str" | tr ',' '\n' | while IFS='=' read -r key value; do - [[ -z "$key" ]] && continue - # Handle boolean and numeric values without quotes - if [[ "$value" == "true" || "$value" == "false" || "$value" =~ ^[0-9]+$ ]]; then - echo " $key: $value" - else - echo " $key: \"$value\"" - fi - done) - fi - - cat > "$STATE_FILE" << EOF + local loop_type="$1" + local max_iterations="$2" + local options_str="$3" + + mkdir -p "$STATE_DIR" + + # Convert options string to YAML object format + # Input: "auto_fix=true,wait_for_ci=false" -> " auto_fix: true\n wait_for_ci: false" + local options_yaml="" + if [[ -n "$options_str" ]]; then + options_yaml=$(echo "$options_str" | tr ',' '\n' | while IFS='=' read -r key value; do + [[ -z "$key" ]] && continue + # Handle boolean and numeric values without quotes + if [[ "$value" == "true" || "$value" == "false" || "$value" =~ ^[0-9]+$ ]]; then + echo " $key: $value" + else + echo " $key: \"$value\"" + fi + done) + fi + + cat >"$STATE_FILE" < "$temp_file" - mv "$temp_file" "$STATE_FILE" - return 0 + local field="$1" + local value="$2" + + if [[ ! -f "$STATE_FILE" ]]; then + return 1 + fi + + local temp_file="${STATE_FILE}.tmp.$$" + sed "s/^${field}: .*/${field}: ${value}/" "$STATE_FILE" >"$temp_file" + mv "$temp_file" "$STATE_FILE" + return 0 } # Get a field value from the state file @@ -266,17 +266,17 @@ update_state() { # Returns: 0 # Output: Field value to stdout (empty if not found) get_state_field() { - local field="$1" - - if [[ ! -f "$STATE_FILE" ]]; then - echo "" - return 0 - fi - - local frontmatter - frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE") - echo "$frontmatter" | grep "^${field}:" | sed "s/${field}: *//" | sed 's/^"\(.*\)"$/\1/' - return 0 + local field="$1" + + if [[ ! -f "$STATE_FILE" ]]; then + echo "" + return 0 + fi + + local frontmatter + frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE") + echo "$frontmatter" | grep "^${field}:" | sed "s/${field}: *//" | sed 's/^"\(.*\)"$/\1/' + return 0 } # Increment iteration counter in state file @@ -284,17 +284,17 @@ get_state_field() { # Returns: 0 # Output: New iteration number to stdout increment_iteration() { - local current - current=$(get_state_field "iteration") - - if [[ ! "$current" =~ ^[0-9]+$ ]]; then - current=0 - fi - - local next=$((current + 1)) - update_state "iteration" "$next" - echo "$next" - return 0 + local current + current=$(get_state_field "iteration") + + if [[ ! "$current" =~ ^[0-9]+$ ]]; then + current=0 + fi + + local next=$((current + 1)) + update_state "iteration" "$next" + echo "$next" + return 0 } # Increment fixes applied counter in state file @@ -302,17 +302,17 @@ increment_iteration() { # Returns: 0 # Output: New fixes count to stdout increment_fixes() { - local current - current=$(get_state_field "fixes_applied") - - if [[ ! "$current" =~ ^[0-9]+$ ]]; then - current=0 - fi - - local next=$((current + 1)) - update_state "fixes_applied" "$next" - echo "$next" - return 0 + local current + current=$(get_state_field "fixes_applied") + + if [[ ! "$current" =~ ^[0-9]+$ ]]; then + current=0 + fi + + local next=$((current + 1)) + update_state "fixes_applied" "$next" + echo "$next" + return 0 } # Cancel the active quality loop @@ -320,19 +320,19 @@ increment_fixes() { # Returns: 0 # Side effects: Removes state file if exists cancel_loop() { - if [[ ! -f "$STATE_FILE" ]]; then - print_warning "No active quality loop found." - return 0 - fi - - local loop_type - local iteration - loop_type=$(get_state_field "type") - iteration=$(get_state_field "iteration") - - rm -f "$STATE_FILE" - print_success "Cancelled ${loop_type} loop (was at iteration ${iteration})" - return 0 + if [[ ! -f "$STATE_FILE" ]]; then + print_warning "No active quality loop found." + return 0 + fi + + local loop_type + local iteration + loop_type=$(get_state_field "type") + iteration=$(get_state_field "iteration") + + rm -f "$STATE_FILE" + print_success "Cancelled ${loop_type} loop (was at iteration ${iteration})" + return 0 } # Display current quality loop status @@ -340,31 +340,31 @@ cancel_loop() { # Returns: 0 # Output: Status information to stdout show_status() { - if [[ ! -f "$STATE_FILE" ]]; then - echo "No active quality loop." - return 0 - fi - - echo "Quality Loop Status" - echo "===================" - echo "" - - local loop_type iteration max_iterations status started_at fixes_applied - loop_type=$(get_state_field "type") - iteration=$(get_state_field "iteration") - max_iterations=$(get_state_field "max_iterations") - status=$(get_state_field "status") - started_at=$(get_state_field "started_at") - fixes_applied=$(get_state_field "fixes_applied") - - echo "Type: $loop_type" - echo "Status: $status" - echo "Iteration: $iteration / $max_iterations" - echo "Fixes applied: $fixes_applied" - echo "Started: $started_at" - echo "" - echo "State file: $STATE_FILE" - return 0 + if [[ ! -f "$STATE_FILE" ]]; then + echo "No active quality loop." + return 0 + fi + + echo "Quality Loop Status" + echo "===================" + echo "" + + local loop_type iteration max_iterations status started_at fixes_applied + loop_type=$(get_state_field "type") + iteration=$(get_state_field "iteration") + max_iterations=$(get_state_field "max_iterations") + status=$(get_state_field "status") + started_at=$(get_state_field "started_at") + fixes_applied=$(get_state_field "fixes_applied") + + echo "Type: $loop_type" + echo "Status: $status" + echo "Iteration: $iteration / $max_iterations" + echo "Fixes applied: $fixes_applied" + echo "Started: $started_at" + echo "" + echo "State file: $STATE_FILE" + return 0 } # ============================================================================= @@ -376,95 +376,94 @@ show_status() { # Returns: 0 # Output: "PASS" or "FAIL" to stdout run_preflight_checks() { - local auto_fix="$1" - local results="" - local all_passed=true - - print_step "Running preflight checks..." - - # Check 1: ShellCheck - print_info " Checking ShellCheck..." - # Keep this aligned with linters-local.sh which checks warnings+errors. - # Otherwise, info-level shellcheck findings can fail preflight even though - # the repo's accepted local-linter gate passes. - if find .agents/scripts -name "*.sh" -exec shellcheck --severity=warning {} \; >/dev/null 2>&1; then - results="${results}shellcheck:pass\n" - print_success " ShellCheck: PASS" - else - results="${results}shellcheck:fail\n" - print_warning " ShellCheck: FAIL" - all_passed=false - - if [[ "$auto_fix" == "true" ]]; then - print_info " Auto-fix not available for ShellCheck (manual fixes required)" - fi - fi - - - # Check 2: Secretlint (skip if not installed) - print_info " Checking secrets..." - if command -v secretlint &>/dev/null; then - if secretlint "**/*" --no-terminalLink 2>/dev/null; then - results="${results}secretlint:pass\n" - print_success " Secretlint: PASS" - else - results="${results}secretlint:fail\n" - print_warning " Secretlint: FAIL" - all_passed=false - fi - else - results="${results}secretlint:skip\n" - print_info " Secretlint: SKIPPED (not installed)" - fi - - # Check 3: Markdown formatting - print_info " Checking markdown..." - if command -v markdownlint &>/dev/null || command -v markdownlint-cli2 &>/dev/null; then - local md_cmd="markdownlint" - command -v markdownlint-cli2 &>/dev/null && md_cmd="markdownlint-cli2" - - if $md_cmd "**/*.md" --ignore node_modules 2>/dev/null; then - results="${results}markdown:pass\n" - print_success " Markdown: PASS" - else - results="${results}markdown:fail\n" - print_warning " Markdown: FAIL" - all_passed=false - - if [[ "$auto_fix" == "true" ]]; then - print_info " Attempting auto-fix..." - $md_cmd "**/*.md" --fix --ignore node_modules 2>/dev/null || true - increment_fixes > /dev/null - fi - fi - else - results="${results}markdown:skip\n" - print_info " Markdown: SKIPPED (markdownlint not installed)" - fi - - # Check 4: Version consistency - print_info " Checking version consistency..." - if [[ -x "${SCRIPT_DIR}/version-manager.sh" ]]; then - if "${SCRIPT_DIR}/version-manager.sh" validate &>/dev/null; then - results="${results}version:pass\n" - print_success " Version: PASS" - else - results="${results}version:fail\n" - print_warning " Version: FAIL" - all_passed=false - fi - else - results="${results}version:skip\n" - print_info " Version: SKIPPED (version-manager.sh not found)" - fi - - # Return results (stdout only) - if [[ "$all_passed" == "true" ]]; then - echo "PASS" - else - echo "FAIL" - fi - return 0 + local auto_fix="$1" + local results="" + local all_passed=true + + print_step "Running preflight checks..." + + # Check 1: ShellCheck + print_info " Checking ShellCheck..." + # Keep this aligned with linters-local.sh which checks warnings+errors. + # Otherwise, info-level shellcheck findings can fail preflight even though + # the repo's accepted local-linter gate passes. + if find .agents/scripts -name "*.sh" -exec shellcheck --severity=warning {} \; >/dev/null 2>&1; then + results="${results}shellcheck:pass\n" + print_success " ShellCheck: PASS" + else + results="${results}shellcheck:fail\n" + print_warning " ShellCheck: FAIL" + all_passed=false + + if [[ "$auto_fix" == "true" ]]; then + print_info " Auto-fix not available for ShellCheck (manual fixes required)" + fi + fi + + # Check 2: Secretlint (skip if not installed) + print_info " Checking secrets..." + if command -v secretlint &>/dev/null; then + if secretlint "**/*" --no-terminalLink 2>/dev/null; then + results="${results}secretlint:pass\n" + print_success " Secretlint: PASS" + else + results="${results}secretlint:fail\n" + print_warning " Secretlint: FAIL" + all_passed=false + fi + else + results="${results}secretlint:skip\n" + print_info " Secretlint: SKIPPED (not installed)" + fi + + # Check 3: Markdown formatting + print_info " Checking markdown..." + if command -v markdownlint &>/dev/null || command -v markdownlint-cli2 &>/dev/null; then + local md_cmd="markdownlint" + command -v markdownlint-cli2 &>/dev/null && md_cmd="markdownlint-cli2" + + if $md_cmd "**/*.md" --ignore node_modules 2>/dev/null; then + results="${results}markdown:pass\n" + print_success " Markdown: PASS" + else + results="${results}markdown:fail\n" + print_warning " Markdown: FAIL" + all_passed=false + + if [[ "$auto_fix" == "true" ]]; then + print_info " Attempting auto-fix..." + $md_cmd "**/*.md" --fix --ignore node_modules 2>/dev/null || true + increment_fixes >/dev/null + fi + fi + else + results="${results}markdown:skip\n" + print_info " Markdown: SKIPPED (markdownlint not installed)" + fi + + # Check 4: Version consistency + print_info " Checking version consistency..." + if [[ -x "${SCRIPT_DIR}/version-manager.sh" ]]; then + if "${SCRIPT_DIR}/version-manager.sh" validate &>/dev/null; then + results="${results}version:pass\n" + print_success " Version: PASS" + else + results="${results}version:fail\n" + print_warning " Version: FAIL" + all_passed=false + fi + else + results="${results}version:skip\n" + print_info " Version: SKIPPED (version-manager.sh not found)" + fi + + # Return results (stdout only) + if [[ "$all_passed" == "true" ]]; then + echo "PASS" + else + echo "FAIL" + fi + return 0 } # Run preflight checks in a loop until all pass or max iterations @@ -472,69 +471,77 @@ run_preflight_checks() { # Returns: 0 on success, 1 if max iterations reached # Output: PREFLIGHT_PASS on success preflight_loop() { - local auto_fix=false - local max_iterations=$DEFAULT_MAX_ITERATIONS - - # Parse arguments - while [[ $# -gt 0 ]]; do - case $1 in - --auto-fix) - auto_fix=true - shift - ;; - --max-iterations) - max_iterations="$2" - shift 2 - ;; - *) - shift - ;; - esac - done - - print_info "Starting preflight loop (max iterations: $max_iterations, auto-fix: $auto_fix)" - - create_state "preflight" "$max_iterations" "auto_fix=$auto_fix" - - local iteration=1 - while [[ $iteration -le $max_iterations ]]; do - echo "" - print_info "=== Preflight Iteration $iteration / $max_iterations ===" - - local result_status - result_status=$(run_preflight_checks "$auto_fix" 2>/dev/null | tail -n 1 | tr -d '\r') - - if [[ "$result_status" == "PASS" ]]; then - echo "" - print_success "All preflight checks passed!" - update_state "status" "completed" - rm -f "$STATE_FILE" - - # Output completion promise for Ralph integration - echo "" - echo "PREFLIGHT_PASS" - return 0 - fi - - if [[ $iteration -ge $max_iterations ]]; then - echo "" - print_warning "Max iterations ($max_iterations) reached. Some checks still failing." - update_state "status" "max_iterations_reached" - return 1 - fi - - iteration=$(increment_iteration) - - if [[ "$auto_fix" == "true" ]]; then - print_info "Fixes applied, re-running checks..." - sleep 1 - else - print_warning "Checks failed. Enable --auto-fix or fix manually." - return 1 - fi - done - - return 1 + local auto_fix=false + local max_iterations=$DEFAULT_MAX_ITERATIONS + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --auto-fix) + auto_fix=true + shift + ;; + --max-iterations) + if [[ -z "${2:-}" ]]; then + print_error "--max-iterations requires a number argument" + return 1 + fi + if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -eq 0 ]]; then + print_error "--max-iterations must be a positive integer, got: $2" + return 1 + fi + max_iterations="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + print_info "Starting preflight loop (max iterations: $max_iterations, auto-fix: $auto_fix)" + + create_state "preflight" "$max_iterations" "auto_fix=$auto_fix" + + local iteration=1 + while [[ $iteration -le $max_iterations ]]; do + echo "" + print_info "=== Preflight Iteration $iteration / $max_iterations ===" + + local result_status + result_status=$(run_preflight_checks "$auto_fix" 2>/dev/null | tail -n 1 | tr -d '\r') + + if [[ "$result_status" == "PASS" ]]; then + echo "" + print_success "All preflight checks passed!" + update_state "status" "completed" + rm -f "$STATE_FILE" + + # Output completion promise for Ralph integration + echo "" + echo "PREFLIGHT_PASS" + return 0 + fi + + if [[ $iteration -ge $max_iterations ]]; then + echo "" + print_warning "Max iterations ($max_iterations) reached. Some checks still failing." + update_state "status" "max_iterations_reached" + return 1 + fi + + iteration=$(increment_iteration) + + if [[ "$auto_fix" == "true" ]]; then + print_info "Fixes applied, re-running checks..." + sleep 1 + else + print_warning "Checks failed. Enable --auto-fix or fix manually." + return 1 + fi + done + + return 1 } # ============================================================================= @@ -548,61 +555,61 @@ preflight_loop() { # Returns: 0 on success, 1 on error # Output: Status string (MERGED, READY, PENDING, CI_FAILED, CHANGES_REQUESTED, WAITING) check_pr_status() { - local pr_number="$1" - local wait_for_ci="$2" - - print_step "Checking PR #${pr_number} status..." - - # Get PR info - local pr_info - if ! pr_info=$(gh pr view "$pr_number" --json state,mergeable,reviewDecision,statusCheckRollup); then - print_error "Failed to get PR info for #$pr_number" - return 1 - fi - - local state mergeable review_decision - state=$(echo "$pr_info" | jq -r '.state') - mergeable=$(echo "$pr_info" | jq -r '.mergeable') - review_decision=$(echo "$pr_info" | jq -r '.reviewDecision // "NONE"') - - print_info " State: $state" - print_info " Mergeable: $mergeable" - print_info " Review: $review_decision" - - # Check CI status - local checks_pending=false - local checks_failed=false - - local check_count - check_count=$(echo "$pr_info" | jq '.statusCheckRollup | length') - - if [[ "$check_count" -gt 0 ]]; then - local pending_count failed_count action_required_count - pending_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS")] | length') - failed_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.conclusion == "FAILURE")] | length') - action_required_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.conclusion == "ACTION_REQUIRED")] | length') - - print_info " CI Checks: $check_count total, $pending_count pending, $failed_count failed, $action_required_count action required" - - [[ "$pending_count" -gt 0 ]] && checks_pending=true - [[ "$failed_count" -gt 0 || "$action_required_count" -gt 0 ]] && checks_failed=true - fi - - # Determine overall status - if [[ "$state" == "MERGED" ]]; then - echo "MERGED" - elif [[ "$review_decision" == "APPROVED" ]] && [[ "$checks_failed" == "false" ]] && [[ "$checks_pending" == "false" ]]; then - echo "READY" - elif [[ "$checks_pending" == "true" ]] && [[ "$wait_for_ci" == "true" ]]; then - echo "PENDING" - elif [[ "$checks_failed" == "true" ]]; then - echo "CI_FAILED" - elif [[ "$review_decision" == "CHANGES_REQUESTED" ]]; then - echo "CHANGES_REQUESTED" - else - echo "WAITING" - fi - return 0 + local pr_number="$1" + local wait_for_ci="$2" + + print_step "Checking PR #${pr_number} status..." + + # Get PR info + local pr_info + if ! pr_info=$(gh pr view "$pr_number" --json state,mergeable,reviewDecision,statusCheckRollup); then + print_error "Failed to get PR info for #$pr_number" + return 1 + fi + + local state mergeable review_decision + state=$(echo "$pr_info" | jq -r '.state') + mergeable=$(echo "$pr_info" | jq -r '.mergeable') + review_decision=$(echo "$pr_info" | jq -r '.reviewDecision // "NONE"') + + print_info " State: $state" + print_info " Mergeable: $mergeable" + print_info " Review: $review_decision" + + # Check CI status + local checks_pending=false + local checks_failed=false + + local check_count + check_count=$(echo "$pr_info" | jq '.statusCheckRollup | length') + + if [[ "$check_count" -gt 0 ]]; then + local pending_count failed_count action_required_count + pending_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS")] | length') + failed_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.conclusion == "FAILURE")] | length') + action_required_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.conclusion == "ACTION_REQUIRED")] | length') + + print_info " CI Checks: $check_count total, $pending_count pending, $failed_count failed, $action_required_count action required" + + [[ "$pending_count" -gt 0 ]] && checks_pending=true + [[ "$failed_count" -gt 0 || "$action_required_count" -gt 0 ]] && checks_failed=true + fi + + # Determine overall status + if [[ "$state" == "MERGED" ]]; then + echo "MERGED" + elif [[ "$review_decision" == "APPROVED" ]] && [[ "$checks_failed" == "false" ]] && [[ "$checks_pending" == "false" ]]; then + echo "READY" + elif [[ "$checks_pending" == "true" ]] && [[ "$wait_for_ci" == "true" ]]; then + echo "PENDING" + elif [[ "$checks_failed" == "true" ]]; then + echo "CI_FAILED" + elif [[ "$review_decision" == "CHANGES_REQUESTED" ]]; then + echo "CHANGES_REQUESTED" + else + echo "WAITING" + fi + return 0 } # Get feedback from PR reviews and CI annotations @@ -610,42 +617,42 @@ check_pr_status() { # Returns: 0 # Output: Feedback text to stdout get_pr_feedback() { - local pr_number="$1" - - print_step "Getting PR feedback..." - - # Get AI reviewer comments (CodeRabbit, Gemini Code Assist, Augment Code, Copilot) - local ai_review_comments api_response - api_response=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/comments") - - if [[ -z "$api_response" ]]; then - print_warning "Failed to fetch PR comments from GitHub API" - else - ai_review_comments=$(printf '%s' "$api_response" | jq -r --arg bots "$AI_REVIEWERS" \ - '.[] | select(.user.login | test($bots; "i")) | "\(.user.login): \(.body)"' \ - 2>/dev/null | head -20) - - if [[ -n "$ai_review_comments" ]]; then - print_info "AI reviewer feedback found" - echo "$ai_review_comments" - fi - fi - - # Get check run annotations - local head_sha - head_sha=$(gh pr view "$pr_number" --json headRefOid -q .headRefOid || echo "") - - if [[ -n "$head_sha" ]]; then - local annotations - annotations=$(gh api "repos/{owner}/{repo}/commits/${head_sha}/check-runs" --jq '.check_runs[].output.annotations[]? | "\(.path):\(.start_line) - \(.message)"' | head -20 || echo "") - - if [[ -n "$annotations" ]]; then - print_info "CI annotations found:" - echo "$annotations" - fi - fi - - return 0 + local pr_number="$1" + + print_step "Getting PR feedback..." + + # Get AI reviewer comments (CodeRabbit, Gemini Code Assist, Augment Code, Copilot) + local ai_review_comments api_response + api_response=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/comments") + + if [[ -z "$api_response" ]]; then + print_warning "Failed to fetch PR comments from GitHub API" + else + ai_review_comments=$(printf '%s' "$api_response" | jq -r --arg bots "$AI_REVIEWERS" \ + '.[] | select(.user.login | test($bots; "i")) | "\(.user.login): \(.body)"' \ + 2>/dev/null | head -20) + + if [[ -n "$ai_review_comments" ]]; then + print_info "AI reviewer feedback found" + echo "$ai_review_comments" + fi + fi + + # Get check run annotations + local head_sha + head_sha=$(gh pr view "$pr_number" --json headRefOid -q .headRefOid || echo "") + + if [[ -n "$head_sha" ]]; then + local annotations + annotations=$(gh api "repos/{owner}/{repo}/commits/${head_sha}/check-runs" --jq '.check_runs[].output.annotations[]? | "\(.path):\(.start_line) - \(.message)"' | head -20 || echo "") + + if [[ -n "$annotations" ]]; then + print_info "CI annotations found:" + echo "$annotations" + fi + fi + + return 0 } # Check if review is stale and trigger re-review if needed @@ -655,65 +662,65 @@ get_pr_feedback() { # Returns: 0 if re-review triggered, 1 if not needed # Side effects: Posts @coderabbitai review comment if stale check_and_trigger_review() { - local pr_number="$1" - local stale_threshold="${2:-$DEFAULT_REVIEW_STALE_THRESHOLD}" - - # Get last push time - local last_push_time - last_push_time=$(gh pr view "$pr_number" --json commits --jq '.commits[-1].committedDate' || echo "") - - if [[ -z "$last_push_time" ]]; then - print_warning "Could not determine last push time" - return 1 - fi - - # Get last AI reviewer review time (any supported reviewer) - local last_review_time api_response - api_response=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews") - - if [[ -n "$api_response" ]]; then - last_review_time=$(printf '%s' "$api_response" | jq -r --arg bots "$AI_REVIEWERS" \ - '[.[] | select(.user.login | test($bots; "i"))] | sort_by(.submitted_at) | last | .submitted_at // ""' 2>/dev/null) - else - last_review_time="" - fi - - # Convert times to epoch for comparison - local now_epoch last_push_epoch last_review_epoch - now_epoch=$(date +%s) - - # Parse ISO 8601 dates (works on macOS and Linux) - if [[ -n "$last_push_time" ]]; then - last_push_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_push_time" +%s 2>/dev/null || \ - date -d "$last_push_time" +%s 2>/dev/null || echo "0") - else - last_push_epoch=0 - fi - - if [[ -n "$last_review_time" ]]; then - last_review_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_review_time" +%s 2>/dev/null || \ - date -d "$last_review_time" +%s 2>/dev/null || echo "0") - else - last_review_epoch=0 - fi - - # Check if review is stale (push happened after last review, and threshold exceeded) - local time_since_push=$((now_epoch - last_push_epoch)) - - if [[ $last_push_epoch -gt $last_review_epoch ]] && [[ $time_since_push -ge $stale_threshold ]]; then - print_info "Review appears stale (${time_since_push}s since push, no review since)" - print_info "Triggering CodeRabbit re-review..." - - if gh pr comment "$pr_number" --body "@coderabbitai review" 2>/dev/null; then - print_success "Re-review triggered for PR #${pr_number}" - return 0 - else - print_warning "Failed to trigger re-review" - return 1 - fi - fi - - return 1 + local pr_number="$1" + local stale_threshold="${2:-$DEFAULT_REVIEW_STALE_THRESHOLD}" + + # Get last push time + local last_push_time + last_push_time=$(gh pr view "$pr_number" --json commits --jq '.commits[-1].committedDate' || echo "") + + if [[ -z "$last_push_time" ]]; then + print_warning "Could not determine last push time" + return 1 + fi + + # Get last AI reviewer review time (any supported reviewer) + local last_review_time api_response + api_response=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews") + + if [[ -n "$api_response" ]]; then + last_review_time=$(printf '%s' "$api_response" | jq -r --arg bots "$AI_REVIEWERS" \ + '[.[] | select(.user.login | test($bots; "i"))] | sort_by(.submitted_at) | last | .submitted_at // ""' 2>/dev/null) + else + last_review_time="" + fi + + # Convert times to epoch for comparison + local now_epoch last_push_epoch last_review_epoch + now_epoch=$(date +%s) + + # Parse ISO 8601 dates (works on macOS and Linux) + if [[ -n "$last_push_time" ]]; then + last_push_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_push_time" +%s 2>/dev/null || + date -d "$last_push_time" +%s 2>/dev/null || echo "0") + else + last_push_epoch=0 + fi + + if [[ -n "$last_review_time" ]]; then + last_review_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_review_time" +%s 2>/dev/null || + date -d "$last_review_time" +%s 2>/dev/null || echo "0") + else + last_review_epoch=0 + fi + + # Check if review is stale (push happened after last review, and threshold exceeded) + local time_since_push=$((now_epoch - last_push_epoch)) + + if [[ $last_push_epoch -gt $last_review_epoch ]] && [[ $time_since_push -ge $stale_threshold ]]; then + print_info "Review appears stale (${time_since_push}s since push, no review since)" + print_info "Triggering CodeRabbit re-review..." + + if gh pr comment "$pr_number" --body "@coderabbitai review" 2>/dev/null; then + print_success "Re-review triggered for PR #${pr_number}" + return 0 + else + print_warning "Failed to trigger re-review" + return 1 + fi + fi + + return 1 } # Check for unresolved review threads on a PR using GraphQL @@ -721,19 +728,19 @@ check_and_trigger_review() { # Returns: 0 if no unresolved threads, 1 if unresolved threads exist, 2 on API error # Output: Warning message if unresolved threads found check_unresolved_review_comments() { - local pr_number="$1" - - local repo_owner repo_name api_response unresolved_count - repo_owner=$(gh repo view --json owner -q '.owner.login' 2>/dev/null || echo "") - repo_name=$(gh repo view --json name -q '.name' 2>/dev/null || echo "") - - if [[ -z "$repo_owner" || -z "$repo_name" ]]; then - print_error "Failed to resolve repo owner/name - cannot verify review status" - return 2 - fi - - # shellcheck disable=SC2016 # GraphQL variables, not shell - single quotes intentional - api_response=$(gh api graphql -f query=' + local pr_number="$1" + + local repo_owner repo_name api_response unresolved_count + repo_owner=$(gh repo view --json owner -q '.owner.login' 2>/dev/null || echo "") + repo_name=$(gh repo view --json name -q '.name' 2>/dev/null || echo "") + + if [[ -z "$repo_owner" || -z "$repo_name" ]]; then + print_error "Failed to resolve repo owner/name - cannot verify review status" + return 2 + fi + + # shellcheck disable=SC2016 # GraphQL variables, not shell - single quotes intentional + api_response=$(gh api graphql -f query=' query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { pullRequest(number:$number) { @@ -741,25 +748,25 @@ check_unresolved_review_comments() { } } }' -f owner="$repo_owner" -f repo="$repo_name" -F number="$pr_number" 2>/dev/null) - - if [[ -z "$api_response" ]]; then - print_error "Failed to fetch PR review threads from GitHub API - cannot verify review status" - return 2 - fi - - unresolved_count=$(printf '%s' "$api_response" | jq -r \ - '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length' 2>/dev/null) - - if ! [[ "$unresolved_count" =~ ^[0-9]+$ ]]; then - print_error "Failed to parse unresolved thread count - cannot proceed safely" - return 2 - fi - - if [[ "$unresolved_count" -gt 0 ]]; then - print_warning "Found $unresolved_count unresolved review threads" - return 1 - fi - return 0 + + if [[ -z "$api_response" ]]; then + print_error "Failed to fetch PR review threads from GitHub API - cannot verify review status" + return 2 + fi + + unresolved_count=$(printf '%s' "$api_response" | jq -r \ + '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length' 2>/dev/null) + + if ! [[ "$unresolved_count" =~ ^[0-9]+$ ]]; then + print_error "Failed to parse unresolved thread count - cannot proceed safely" + return 2 + fi + + if [[ "$unresolved_count" -gt 0 ]]; then + print_warning "Found $unresolved_count unresolved review threads" + return 1 + fi + return 0 } # Monitor PR until approved or merged @@ -767,174 +774,182 @@ check_unresolved_review_comments() { # Returns: 0 on approval/merge, 1 if max iterations reached # Output: PR_APPROVED or PR_MERGED pr_review_loop() { - local wait_for_ci=false - local auto_trigger_review=true - local max_iterations=$DEFAULT_MAX_ITERATIONS - local pr_number="" - - # Parse arguments - while [[ $# -gt 0 ]]; do - case $1 in - --wait-for-ci) - wait_for_ci=true - shift - ;; - --max-iterations) - max_iterations="$2" - shift 2 - ;; - --pr) - pr_number="$2" - shift 2 - ;; - --no-auto-trigger) - auto_trigger_review=false - shift - ;; - --auto-trigger-review) - auto_trigger_review=true - shift - ;; - *) - # Assume it's the PR number - if [[ "$1" =~ ^[0-9]+$ ]]; then - pr_number="$1" - fi - shift - ;; - esac - done - - # Auto-detect PR number if not provided - if [[ -z "$pr_number" ]]; then - pr_number=$(gh pr view --json number -q .number 2>/dev/null || echo "") - - if [[ -z "$pr_number" ]]; then - print_error "No PR number provided and no PR found for current branch" - echo "Usage: quality-loop-helper.sh pr-review [--pr NUMBER] [--wait-for-ci] [--max-iterations N] [--no-auto-trigger]" - return 1 - fi - fi - - print_info "Starting PR review loop for PR #${pr_number} (max iterations: $max_iterations, auto-trigger: $auto_trigger_review)" - - create_state "pr-review" "$max_iterations" "pr=$pr_number,wait_for_ci=$wait_for_ci,auto_trigger=$auto_trigger_review" - - local iteration=1 - while [[ $iteration -le $max_iterations ]]; do - echo "" - print_info "=== PR Review Iteration $iteration / $max_iterations ===" - - local status - status=$(check_pr_status "$pr_number" "$wait_for_ci" | tail -n 1 | tr -d '\r') - - case "$status" in - MERGED) - print_success "PR has been merged!" - rm -f "$STATE_FILE" - echo "PR_MERGED" - return 0 - ;; - READY) - # Check for unresolved AI review comments before declaring ready - local unresolved_check_result - check_unresolved_review_comments "$pr_number" - unresolved_check_result=$? - - if [[ $unresolved_check_result -eq 2 ]]; then - print_warning "Could not verify AI review status (API error) - proceeding with caution" - elif [[ $unresolved_check_result -eq 1 ]]; then - print_warning "PR approved but has unresolved AI review comments" - get_pr_feedback "$pr_number" - print_info "Address the AI reviewer feedback and push updates." - else - print_success "PR is approved and ready to merge!" - rm -f "$STATE_FILE" - echo "PR_APPROVED" - return 0 - fi - ;; - PENDING) - # Get pending checks and calculate adaptive wait - local pending_checks - pending_checks=$(get_pending_checks "$pr_number") - local wait_time - wait_time=$(calculate_adaptive_wait "$pending_checks") - - if [[ -n "$pending_checks" ]]; then - print_info "CI checks still running: $pending_checks" - print_info "Waiting ${wait_time}s (adaptive based on slowest pending check)..." - else - print_info "CI checks still running, waiting ${wait_time}s..." - fi - sleep "$wait_time" - ;; - CI_FAILED) - print_warning "CI checks failed. Getting feedback..." - get_pr_feedback "$pr_number" - print_info "Fix the issues and push updates." - ;; - CHANGES_REQUESTED) - print_warning "Changes requested. Getting feedback..." - get_pr_feedback "$pr_number" - print_warning "IMPORTANT: Verify AI bot suggestions before implementing — reviewers can hallucinate. Check claims against runtime/docs first." - print_info "Address verified feedback and push updates." - ;; - WAITING) - # Check for unresolved AI review threads (e.g., Gemini posts as - # COMMENTED, not CHANGES_REQUESTED, so reviewDecision stays NONE - # but feedback still needs addressing) - local waiting_unresolved_result - check_unresolved_review_comments "$pr_number" - waiting_unresolved_result=$? - - if [[ $waiting_unresolved_result -eq 2 ]]; then - print_warning "Could not verify AI review status (API error) - proceeding with caution" - elif [[ $waiting_unresolved_result -eq 1 ]]; then - print_warning "AI reviewers left unresolved feedback (review posted as COMMENTED, not CHANGES_REQUESTED)" - get_pr_feedback "$pr_number" - print_warning "IMPORTANT: Verify AI bot suggestions before implementing — reviewers can hallucinate. Check claims against runtime/docs first." - print_info "Address verified feedback and push updates." - else - print_info "Waiting for review..." - # Check if review is stale and trigger re-review if enabled - if [[ "$auto_trigger_review" == "true" ]] && check_and_trigger_review "$pr_number"; then - print_info "Re-review triggered, waiting for response..." - fi - fi - ;; - *) - print_warning "Unknown PR status: $status" - ;; - esac - - iteration=$(increment_iteration) - - if [[ $iteration -le $max_iterations ]]; then - # Use exponential backoff for general waiting - local backoff_wait - backoff_wait=$(calculate_backoff_wait "$iteration") - - # But also consider pending checks for smarter waiting - local pending_checks - pending_checks=$(get_pending_checks "$pr_number") - local adaptive_wait - adaptive_wait=$(calculate_adaptive_wait "$pending_checks") - - # Use the larger of backoff or adaptive wait - local final_wait=$backoff_wait - if [[ $adaptive_wait -gt $backoff_wait ]]; then - final_wait=$adaptive_wait - fi - - print_info "Waiting ${final_wait}s before next check (iteration $iteration, backoff: ${backoff_wait}s, adaptive: ${adaptive_wait}s)..." - sleep "$final_wait" - fi - done - - print_warning "Max iterations reached. PR not yet approved." - update_state "status" "max_iterations_reached" - return 1 + local wait_for_ci=false + local auto_trigger_review=true + local max_iterations=$DEFAULT_MAX_ITERATIONS + local pr_number="" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --wait-for-ci) + wait_for_ci=true + shift + ;; + --max-iterations) + if [[ -z "${2:-}" ]]; then + print_error "--max-iterations requires a number argument" + return 1 + fi + if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -eq 0 ]]; then + print_error "--max-iterations must be a positive integer, got: $2" + return 1 + fi + max_iterations="$2" + shift 2 + ;; + --pr) + pr_number="$2" + shift 2 + ;; + --no-auto-trigger) + auto_trigger_review=false + shift + ;; + --auto-trigger-review) + auto_trigger_review=true + shift + ;; + *) + # Assume it's the PR number + if [[ "$1" =~ ^[0-9]+$ ]]; then + pr_number="$1" + fi + shift + ;; + esac + done + + # Auto-detect PR number if not provided + if [[ -z "$pr_number" ]]; then + pr_number=$(gh pr view --json number -q .number 2>/dev/null || echo "") + + if [[ -z "$pr_number" ]]; then + print_error "No PR number provided and no PR found for current branch" + echo "Usage: quality-loop-helper.sh pr-review [--pr NUMBER] [--wait-for-ci] [--max-iterations N] [--no-auto-trigger]" + return 1 + fi + fi + + print_info "Starting PR review loop for PR #${pr_number} (max iterations: $max_iterations, auto-trigger: $auto_trigger_review)" + + create_state "pr-review" "$max_iterations" "pr=$pr_number,wait_for_ci=$wait_for_ci,auto_trigger=$auto_trigger_review" + + local iteration=1 + while [[ $iteration -le $max_iterations ]]; do + echo "" + print_info "=== PR Review Iteration $iteration / $max_iterations ===" + + local status + status=$(check_pr_status "$pr_number" "$wait_for_ci" | tail -n 1 | tr -d '\r') + + case "$status" in + MERGED) + print_success "PR has been merged!" + rm -f "$STATE_FILE" + echo "PR_MERGED" + return 0 + ;; + READY) + # Check for unresolved AI review comments before declaring ready + local unresolved_check_result + check_unresolved_review_comments "$pr_number" + unresolved_check_result=$? + + if [[ $unresolved_check_result -eq 2 ]]; then + print_warning "Could not verify AI review status (API error) - proceeding with caution" + elif [[ $unresolved_check_result -eq 1 ]]; then + print_warning "PR approved but has unresolved AI review comments" + get_pr_feedback "$pr_number" + print_info "Address the AI reviewer feedback and push updates." + else + print_success "PR is approved and ready to merge!" + rm -f "$STATE_FILE" + echo "PR_APPROVED" + return 0 + fi + ;; + PENDING) + # Get pending checks and calculate adaptive wait + local pending_checks + pending_checks=$(get_pending_checks "$pr_number") + local wait_time + wait_time=$(calculate_adaptive_wait "$pending_checks") + + if [[ -n "$pending_checks" ]]; then + print_info "CI checks still running: $pending_checks" + print_info "Waiting ${wait_time}s (adaptive based on slowest pending check)..." + else + print_info "CI checks still running, waiting ${wait_time}s..." + fi + sleep "$wait_time" + ;; + CI_FAILED) + print_warning "CI checks failed. Getting feedback..." + get_pr_feedback "$pr_number" + print_info "Fix the issues and push updates." + ;; + CHANGES_REQUESTED) + print_warning "Changes requested. Getting feedback..." + get_pr_feedback "$pr_number" + print_warning "IMPORTANT: Verify AI bot suggestions before implementing — reviewers can hallucinate. Check claims against runtime/docs first." + print_info "Address verified feedback and push updates." + ;; + WAITING) + # Check for unresolved AI review threads (e.g., Gemini posts as + # COMMENTED, not CHANGES_REQUESTED, so reviewDecision stays NONE + # but feedback still needs addressing) + local waiting_unresolved_result + check_unresolved_review_comments "$pr_number" + waiting_unresolved_result=$? + + if [[ $waiting_unresolved_result -eq 2 ]]; then + print_warning "Could not verify AI review status (API error) - proceeding with caution" + elif [[ $waiting_unresolved_result -eq 1 ]]; then + print_warning "AI reviewers left unresolved feedback (review posted as COMMENTED, not CHANGES_REQUESTED)" + get_pr_feedback "$pr_number" + print_warning "IMPORTANT: Verify AI bot suggestions before implementing — reviewers can hallucinate. Check claims against runtime/docs first." + print_info "Address verified feedback and push updates." + else + print_info "Waiting for review..." + # Check if review is stale and trigger re-review if enabled + if [[ "$auto_trigger_review" == "true" ]] && check_and_trigger_review "$pr_number"; then + print_info "Re-review triggered, waiting for response..." + fi + fi + ;; + *) + print_warning "Unknown PR status: $status" + ;; + esac + + iteration=$(increment_iteration) + + if [[ $iteration -le $max_iterations ]]; then + # Use exponential backoff for general waiting + local backoff_wait + backoff_wait=$(calculate_backoff_wait "$iteration") + + # But also consider pending checks for smarter waiting + local pending_checks + pending_checks=$(get_pending_checks "$pr_number") + local adaptive_wait + adaptive_wait=$(calculate_adaptive_wait "$pending_checks") + + # Use the larger of backoff or adaptive wait + local final_wait=$backoff_wait + if [[ $adaptive_wait -gt $backoff_wait ]]; then + final_wait=$adaptive_wait + fi + + print_info "Waiting ${final_wait}s before next check (iteration $iteration, backoff: ${backoff_wait}s, adaptive: ${adaptive_wait}s)..." + sleep "$final_wait" + fi + done + + print_warning "Max iterations reached. PR not yet approved." + update_state "status" "max_iterations_reached" + return 1 } # ============================================================================= @@ -946,58 +961,58 @@ pr_review_loop() { # Returns: 0 # Output: "HEALTHY" or "UNHEALTHY" to stdout check_release_health() { - print_step "Checking release health..." - - local all_healthy=true - - # Check 1: Latest workflow run status - print_info " Checking CI status..." - local latest_run - latest_run=$(gh run list --limit 1 --json conclusion,status -q '.[0]' 2>/dev/null || echo '{}') - - local run_status run_conclusion - run_status=$(echo "$latest_run" | jq -r '.status // "unknown"') - run_conclusion=$(echo "$latest_run" | jq -r '.conclusion // "unknown"') - - if [[ "$run_status" == "completed" ]] && [[ "$run_conclusion" == "success" ]]; then - print_success " CI: PASS (latest run succeeded)" - elif [[ "$run_status" == "in_progress" ]]; then - print_info " CI: PENDING (run in progress)" - all_healthy=false - else - print_warning " CI: FAIL (conclusion: $run_conclusion)" - all_healthy=false - fi - - # Check 2: Latest release exists - print_info " Checking latest release..." - local latest_release - latest_release=$(gh release view --json tagName,publishedAt -q '.tagName' 2>/dev/null || echo "") - - if [[ -n "$latest_release" ]]; then - print_success " Release: $latest_release exists" - else - print_warning " Release: No releases found" - fi - - # Check 3: Tag matches VERSION - print_info " Checking version consistency..." - local current_version - current_version=$(cat VERSION 2>/dev/null || echo "unknown") - - if [[ "$latest_release" == "v${current_version}" ]] || [[ "$latest_release" == "$current_version" ]]; then - print_success " Version: Matches ($current_version)" - else - print_warning " Version: Mismatch (VERSION=$current_version, release=$latest_release)" - all_healthy=false - fi - - if [[ "$all_healthy" == "true" ]]; then - echo "HEALTHY" - else - echo "UNHEALTHY" - fi - return 0 + print_step "Checking release health..." + + local all_healthy=true + + # Check 1: Latest workflow run status + print_info " Checking CI status..." + local latest_run + latest_run=$(gh run list --limit 1 --json conclusion,status -q '.[0]' 2>/dev/null || echo '{}') + + local run_status run_conclusion + run_status=$(echo "$latest_run" | jq -r '.status // "unknown"') + run_conclusion=$(echo "$latest_run" | jq -r '.conclusion // "unknown"') + + if [[ "$run_status" == "completed" ]] && [[ "$run_conclusion" == "success" ]]; then + print_success " CI: PASS (latest run succeeded)" + elif [[ "$run_status" == "in_progress" ]]; then + print_info " CI: PENDING (run in progress)" + all_healthy=false + else + print_warning " CI: FAIL (conclusion: $run_conclusion)" + all_healthy=false + fi + + # Check 2: Latest release exists + print_info " Checking latest release..." + local latest_release + latest_release=$(gh release view --json tagName,publishedAt -q '.tagName' 2>/dev/null || echo "") + + if [[ -n "$latest_release" ]]; then + print_success " Release: $latest_release exists" + else + print_warning " Release: No releases found" + fi + + # Check 3: Tag matches VERSION + print_info " Checking version consistency..." + local current_version + current_version=$(cat VERSION 2>/dev/null || echo "unknown") + + if [[ "$latest_release" == "v${current_version}" ]] || [[ "$latest_release" == "$current_version" ]]; then + print_success " Version: Matches ($current_version)" + else + print_warning " Version: Mismatch (VERSION=$current_version, release=$latest_release)" + all_healthy=false + fi + + if [[ "$all_healthy" == "true" ]]; then + echo "HEALTHY" + else + echo "UNHEALTHY" + fi + return 0 } # Monitor release health for a specified duration @@ -1005,82 +1020,90 @@ check_release_health() { # Returns: 0 on healthy, 0 on timeout (with warning) # Output: RELEASE_HEALTHY on success postflight_loop() { - local monitor_duration=$DEFAULT_MONITOR_DURATION - local max_iterations=5 - - # Parse arguments - while [[ $# -gt 0 ]]; do - case $1 in - --monitor-duration) - # Parse duration (e.g., 5m, 10m, 1h, or raw seconds) - local duration_str="$2" - if [[ "$duration_str" =~ ^([0-9]+)m$ ]]; then - monitor_duration=$((BASH_REMATCH[1] * 60)) - elif [[ "$duration_str" =~ ^([0-9]+)h$ ]]; then - monitor_duration=$((BASH_REMATCH[1] * 3600)) - elif [[ "$duration_str" =~ ^([0-9]+)s$ ]]; then - monitor_duration="${BASH_REMATCH[1]}" - elif [[ "$duration_str" =~ ^([0-9]+)$ ]]; then - monitor_duration="$duration_str" - else - print_warning "Unrecognized duration format: '$duration_str'. Expected: Nm (minutes), Nh (hours), Ns (seconds), or N (seconds). Using default: ${DEFAULT_MONITOR_DURATION}s" - fi - shift 2 - ;; - --max-iterations) - max_iterations="$2" - shift 2 - ;; - *) - shift - ;; - esac - done - - print_info "Starting postflight monitoring (duration: ${monitor_duration}s, max iterations: $max_iterations)" - - create_state "postflight" "$max_iterations" "monitor_duration=$monitor_duration" - - local start_time - start_time=$(date +%s) - local iteration=1 - - while [[ $iteration -le $max_iterations ]]; do - local current_time - current_time=$(date +%s) - local elapsed=$((current_time - start_time)) - - if [[ $elapsed -ge $monitor_duration ]]; then - print_info "Monitor duration reached." - break - fi - - echo "" - print_info "=== Postflight Check $iteration / $max_iterations (${elapsed}s / ${monitor_duration}s) ===" - - local status - status=$(check_release_health) - - if [[ "$status" == "HEALTHY" ]]; then - print_success "Release is healthy!" - rm -f "$STATE_FILE" - echo "RELEASE_HEALTHY" - return 0 - fi - - iteration=$(increment_iteration) - - if [[ $iteration -le $max_iterations ]]; then - local wait_time=$((monitor_duration / max_iterations)) - print_info "Waiting ${wait_time}s before next check..." - sleep "$wait_time" - fi - done - - print_warning "Postflight monitoring complete. Some issues may remain." - update_state "status" "monitoring_complete" - rm -f "$STATE_FILE" - return 0 + local monitor_duration=$DEFAULT_MONITOR_DURATION + local max_iterations=5 + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --monitor-duration) + # Parse duration (e.g., 5m, 10m, 1h, or raw seconds) + local duration_str="$2" + if [[ "$duration_str" =~ ^([0-9]+)m$ ]]; then + monitor_duration=$((BASH_REMATCH[1] * 60)) + elif [[ "$duration_str" =~ ^([0-9]+)h$ ]]; then + monitor_duration=$((BASH_REMATCH[1] * 3600)) + elif [[ "$duration_str" =~ ^([0-9]+)s$ ]]; then + monitor_duration="${BASH_REMATCH[1]}" + elif [[ "$duration_str" =~ ^([0-9]+)$ ]]; then + monitor_duration="$duration_str" + else + print_warning "Unrecognized duration format: '$duration_str'. Expected: Nm (minutes), Nh (hours), Ns (seconds), or N (seconds). Using default: ${DEFAULT_MONITOR_DURATION}s" + fi + shift 2 + ;; + --max-iterations) + if [[ -z "${2:-}" ]]; then + print_error "--max-iterations requires a number argument" + return 1 + fi + if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -eq 0 ]]; then + print_error "--max-iterations must be a positive integer, got: $2" + return 1 + fi + max_iterations="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + print_info "Starting postflight monitoring (duration: ${monitor_duration}s, max iterations: $max_iterations)" + + create_state "postflight" "$max_iterations" "monitor_duration=$monitor_duration" + + local start_time + start_time=$(date +%s) + local iteration=1 + + while [[ $iteration -le $max_iterations ]]; do + local current_time + current_time=$(date +%s) + local elapsed=$((current_time - start_time)) + + if [[ $elapsed -ge $monitor_duration ]]; then + print_info "Monitor duration reached." + break + fi + + echo "" + print_info "=== Postflight Check $iteration / $max_iterations (${elapsed}s / ${monitor_duration}s) ===" + + local status + status=$(check_release_health) + + if [[ "$status" == "HEALTHY" ]]; then + print_success "Release is healthy!" + rm -f "$STATE_FILE" + echo "RELEASE_HEALTHY" + return 0 + fi + + iteration=$(increment_iteration) + + if [[ $iteration -le $max_iterations ]]; then + local wait_time=$((monitor_duration / max_iterations)) + print_info "Waiting ${wait_time}s before next check..." + sleep "$wait_time" + fi + done + + print_warning "Postflight monitoring complete. Some issues may remain." + update_state "status" "monitoring_complete" + rm -f "$STATE_FILE" + return 0 } # ============================================================================= @@ -1088,7 +1111,7 @@ postflight_loop() { # ============================================================================= show_help() { - cat << 'EOF' + cat <<'EOF' Quality Loop Helper - Iterative Quality Workflows USAGE: @@ -1149,7 +1172,7 @@ ADAPTIVE TIMING (PR Review): - Waiting too long for fast checks - Polling too frequently for slow checks (wastes API calls) EOF - return 0 + return 0 } # ============================================================================= @@ -1157,35 +1180,35 @@ EOF # ============================================================================= main() { - local command="${1:-help}" - shift || true - - case "$command" in - preflight) - preflight_loop "$@" - ;; - pr-review|pr) - pr_review_loop "$@" - ;; - postflight) - postflight_loop "$@" - ;; - status) - show_status - ;; - cancel) - cancel_loop - ;; - help|--help|-h) - show_help - ;; - *) - print_error "Unknown command: $command" - echo "" - show_help - return 1 - ;; - esac + local command="${1:-help}" + shift || true + + case "$command" in + preflight) + preflight_loop "$@" + ;; + pr-review | pr) + pr_review_loop "$@" + ;; + postflight) + postflight_loop "$@" + ;; + status) + show_status + ;; + cancel) + cancel_loop + ;; + help | --help | -h) + show_help + ;; + *) + print_error "Unknown command: $command" + echo "" + show_help + return 1 + ;; + esac } main "$@" diff --git a/.agents/scripts/archived/ralph-loop-helper.sh b/.agents/scripts/archived/ralph-loop-helper.sh index 2726989275..982194d97a 100755 --- a/.agents/scripts/archived/ralph-loop-helper.sh +++ b/.agents/scripts/archived/ralph-loop-helper.sh @@ -41,7 +41,7 @@ readonly SCRIPT_NAME="ralph-loop-helper.sh" # Source shared loop infrastructure # shellcheck source=loop-common.sh if [[ -f "$SCRIPT_DIR/loop-common.sh" ]]; then - source "$SCRIPT_DIR/loop-common.sh" + source "$SCRIPT_DIR/loop-common.sh" fi # State directories @@ -73,13 +73,13 @@ output_file="" # ============================================================================= print_step() { - local message="$1" - echo -e "${CYAN}[ralph]${NC} ${message}" - return 0 + local message="$1" + echo -e "${CYAN}[ralph]${NC} ${message}" + return 0 } show_help() { - cat << 'EOF' + cat <<'EOF' Ralph Loop Helper v2 - Cross-Tool Iterative AI Development USAGE: @@ -144,7 +144,7 @@ LEARN MORE: flow-next reference: https://github.com/gmickel/gmickel-claude-marketplace Documentation: ~/.aidevops/agents/workflows/ralph-loop.md EOF - return 0 + return 0 } # ============================================================================= @@ -156,213 +156,234 @@ EOF # $@ - Prompt and options # Returns: 0 on completion, 1 on error/max iterations run_v2_loop() { - local prompt="" - local max_iterations=$DEFAULT_MAX_ITERATIONS - local completion_promise="TASK_COMPLETE" - local tool="opencode" - local max_attempts=$DEFAULT_MAX_ATTEMPTS - local task_id="" - local prompt_parts=() - - # Parse arguments - while [[ $# -gt 0 ]]; do - case $1 in - --max-iterations) - max_iterations="$2" - shift 2 - ;; - --completion-promise) - completion_promise="$2" - shift 2 - ;; - --tool) - tool="$2" - shift 2 - ;; - --max-attempts) - max_attempts="$2" - shift 2 - ;; - --task-id) - task_id="$2" - shift 2 - ;; - *) - prompt_parts+=("$1") - shift - ;; - esac - done - - prompt="${prompt_parts[*]}" - - if [[ -z "$prompt" ]]; then - print_error "No prompt provided" - echo "Usage: $SCRIPT_NAME run \"\" --tool [options]" - return 1 - fi - - # Validate tool - if ! command -v "$tool" &>/dev/null; then - print_error "Tool '$tool' not found. Install it or use --tool to specify another." - print_info "Available tools: opencode, claude, aider" - return 1 - fi - - # Check for jq (required for v2) - if ! command -v jq &>/dev/null; then - print_error "jq is required for v2 loops. Install with: brew install jq" - return 1 - fi - - # Initialize state using shared infrastructure - if type loop_create_state &>/dev/null; then - loop_create_state "ralph" "$prompt" "$max_iterations" "$completion_promise" "$task_id" - else - print_warning "loop-common.sh not loaded, using basic state" - mkdir -p "$RALPH_STATE_DIR" - fi - - print_info "Starting Ralph loop v2 with $tool" - print_info "Architecture: Fresh context per iteration" - echo "" - echo "Prompt: $prompt" - echo "Max iterations: $max_iterations" - echo "Completion promise: $completion_promise" - echo "Max attempts before block: $max_attempts" - echo "" - - local iteration=1 - output_file="$(mktemp)" - local output_sizes_file - output_sizes_file="$(mktemp)" - _save_cleanup_scope; trap '_run_cleanups' RETURN - push_cleanup "rm -f '${output_file}'" - push_cleanup "rm -f '${output_sizes_file}'" - - while [[ $iteration -le $max_iterations ]]; do - print_step "=== Iteration $iteration/$max_iterations ===" - - # Update state - if type loop_set_state &>/dev/null; then - loop_set_state ".iteration" "$iteration" - loop_set_state ".last_iteration_at" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - fi - - # Generate re-anchor prompt (key v2 feature) - local reanchor_prompt="" - if type loop_generate_reanchor &>/dev/null; then - print_info "Generating re-anchor context..." - reanchor_prompt=$(loop_generate_reanchor "$prompt") - else - # Fallback: basic prompt with iteration - reanchor_prompt="[Ralph iteration $iteration/$max_iterations] + local prompt="" + local max_iterations=$DEFAULT_MAX_ITERATIONS + local completion_promise="TASK_COMPLETE" + local tool="opencode" + local max_attempts=$DEFAULT_MAX_ATTEMPTS + local task_id="" + local prompt_parts=() + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --max-iterations) + if [[ -z "${2:-}" ]]; then + print_error "--max-iterations requires a number argument" + return 1 + fi + if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -eq 0 ]]; then + print_error "--max-iterations must be a positive integer, got: $2" + return 1 + fi + max_iterations="$2" + shift 2 + ;; + --completion-promise) + if [[ -z "${2:-}" ]]; then + print_error "--completion-promise requires a non-empty text argument" + return 1 + fi + completion_promise="$2" + shift 2 + ;; + --tool) + tool="$2" + shift 2 + ;; + --max-attempts) + if [[ -z "${2:-}" ]]; then + print_error "--max-attempts requires a number argument" + return 1 + fi + if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -eq 0 ]]; then + print_error "--max-attempts must be a positive integer, got: $2" + return 1 + fi + max_attempts="$2" + shift 2 + ;; + --task-id) + task_id="$2" + shift 2 + ;; + *) + prompt_parts+=("$1") + shift + ;; + esac + done + + prompt="${prompt_parts[*]}" + + if [[ -z "$prompt" ]]; then + print_error "No prompt provided" + echo "Usage: $SCRIPT_NAME run \"\" --tool [options]" + return 1 + fi + + # Validate tool + if ! command -v "$tool" &>/dev/null; then + print_error "Tool '$tool' not found. Install it or use --tool to specify another." + print_info "Available tools: opencode, claude, aider" + return 1 + fi + + # Check for jq (required for v2) + if ! command -v jq &>/dev/null; then + print_error "jq is required for v2 loops. Install with: brew install jq" + return 1 + fi + + # Initialize state using shared infrastructure + if type loop_create_state &>/dev/null; then + loop_create_state "ralph" "$prompt" "$max_iterations" "$completion_promise" "$task_id" + else + print_warning "loop-common.sh not loaded, using basic state" + mkdir -p "$RALPH_STATE_DIR" + fi + + print_info "Starting Ralph loop v2 with $tool" + print_info "Architecture: Fresh context per iteration" + echo "" + echo "Prompt: $prompt" + echo "Max iterations: $max_iterations" + echo "Completion promise: $completion_promise" + echo "Max attempts before block: $max_attempts" + echo "" + + local iteration=1 + output_file="$(mktemp)" + local output_sizes_file + output_sizes_file="$(mktemp)" + _save_cleanup_scope + trap '_run_cleanups' RETURN + push_cleanup "rm -f '${output_file}'" + push_cleanup "rm -f '${output_sizes_file}'" + + while [[ $iteration -le $max_iterations ]]; do + print_step "=== Iteration $iteration/$max_iterations ===" + + # Update state + if type loop_set_state &>/dev/null; then + loop_set_state ".iteration" "$iteration" + loop_set_state ".last_iteration_at" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + fi + + # Generate re-anchor prompt (key v2 feature) + local reanchor_prompt="" + if type loop_generate_reanchor &>/dev/null; then + print_info "Generating re-anchor context..." + reanchor_prompt=$(loop_generate_reanchor "$prompt") + else + # Fallback: basic prompt with iteration + reanchor_prompt="[Ralph iteration $iteration/$max_iterations] $prompt To complete, output: $completion_promise (ONLY when TRUE)" - fi - - # Run tool with fresh session - local exit_code=0 - print_info "Spawning fresh $tool session..." - - case "$tool" in - opencode) - local opencode_args=("run" "$reanchor_prompt" "--format" "json") - if [[ -n "${RALPH_MODEL:-}" ]]; then - opencode_args+=("--model" "$RALPH_MODEL") - fi - opencode "${opencode_args[@]}" > "$output_file" 2>&1 || exit_code=$? - ;; - claude) - echo "$reanchor_prompt" | claude --print > "$output_file" 2>&1 || exit_code=$? - ;; - aider) - aider --yes --message "$reanchor_prompt" > "$output_file" 2>&1 || exit_code=$? - ;; - *) - print_error "Unknown tool: $tool" - return 1 - ;; - esac - - if [[ $exit_code -ne 0 ]]; then - print_warning "Tool exited with code $exit_code (continuing)" - if [[ -s "$output_file" ]]; then - print_warning "Tool output (last 20 lines):" - tail -n 20 "$output_file" - fi - fi - - # Check for completion promise - if grep -q "$completion_promise" "$output_file" 2>/dev/null; then - # opencode run emits JSON events; grep still works on raw output - print_success "Completion promise detected!" - - # Create success receipt - if type loop_create_receipt &>/dev/null; then - loop_create_receipt "task" "success" '{"promise_fulfilled": true}' - fi - - # Store success in memory - if type loop_store_success &>/dev/null; then - loop_store_success "Task completed after $iteration iterations" - fi - - print_success "Ralph loop completed successfully after $iteration iterations" - return 0 - fi - - # Context-remaining guard (t247.1): detect approaching context - # exhaustion and proactively signal + push before silent exit. - if type loop_context_guard &>/dev/null && \ - loop_context_guard "$output_file" "$iteration" "$max_iterations" "$output_sizes_file"; then - print_success "Context guard: work preserved, signal emitted" - return 0 - fi - - # Track attempt and check for blocking - if type loop_track_attempt &>/dev/null; then - local attempts - attempts=$(loop_track_attempt) - - if type loop_should_block &>/dev/null && loop_should_block "$max_attempts"; then - print_error "Task blocked after $attempts failed attempts" - - if type loop_block_task &>/dev/null; then - loop_block_task "Max attempts ($max_attempts) reached without completion" - fi - - if type loop_store_failure &>/dev/null; then - loop_store_failure "Task blocked" "Exceeded $max_attempts attempts" - fi - - return 1 - fi - fi - - # Create retry receipt - if type loop_create_receipt &>/dev/null; then - loop_create_receipt "task" "retry" "{\"iteration\": $iteration, \"exit_code\": $exit_code}" - fi - - iteration=$((iteration + 1)) - - # Adaptive delay - local delay - delay=$(calculate_delay "$iteration") - print_info "Waiting ${delay}s before next iteration..." - sleep "$delay" - done - - print_warning "Max iterations ($max_iterations) reached without completion" - - if type loop_block_task &>/dev/null; then - loop_block_task "Max iterations reached" - fi - - return 1 + fi + + # Run tool with fresh session + local exit_code=0 + print_info "Spawning fresh $tool session..." + + case "$tool" in + opencode) + local opencode_args=("run" "$reanchor_prompt" "--format" "json") + if [[ -n "${RALPH_MODEL:-}" ]]; then + opencode_args+=("--model" "$RALPH_MODEL") + fi + opencode "${opencode_args[@]}" >"$output_file" 2>&1 || exit_code=$? + ;; + claude) + echo "$reanchor_prompt" | claude --print >"$output_file" 2>&1 || exit_code=$? + ;; + aider) + aider --yes --message "$reanchor_prompt" >"$output_file" 2>&1 || exit_code=$? + ;; + *) + print_error "Unknown tool: $tool" + return 1 + ;; + esac + + if [[ $exit_code -ne 0 ]]; then + print_warning "Tool exited with code $exit_code (continuing)" + if [[ -s "$output_file" ]]; then + print_warning "Tool output (last 20 lines):" + tail -n 20 "$output_file" + fi + fi + + # Check for completion promise + if grep -q "$completion_promise" "$output_file" 2>/dev/null; then + # opencode run emits JSON events; grep still works on raw output + print_success "Completion promise detected!" + + # Create success receipt + if type loop_create_receipt &>/dev/null; then + loop_create_receipt "task" "success" '{"promise_fulfilled": true}' + fi + + # Store success in memory + if type loop_store_success &>/dev/null; then + loop_store_success "Task completed after $iteration iterations" + fi + + print_success "Ralph loop completed successfully after $iteration iterations" + return 0 + fi + + # Context-remaining guard (t247.1): detect approaching context + # exhaustion and proactively signal + push before silent exit. + if type loop_context_guard &>/dev/null && + loop_context_guard "$output_file" "$iteration" "$max_iterations" "$output_sizes_file"; then + print_success "Context guard: work preserved, signal emitted" + return 0 + fi + + # Track attempt and check for blocking + if type loop_track_attempt &>/dev/null; then + local attempts + attempts=$(loop_track_attempt) + + if type loop_should_block &>/dev/null && loop_should_block "$max_attempts"; then + print_error "Task blocked after $attempts failed attempts" + + if type loop_block_task &>/dev/null; then + loop_block_task "Max attempts ($max_attempts) reached without completion" + fi + + if type loop_store_failure &>/dev/null; then + loop_store_failure "Task blocked" "Exceeded $max_attempts attempts" + fi + + return 1 + fi + fi + + # Create retry receipt + if type loop_create_receipt &>/dev/null; then + loop_create_receipt "task" "retry" "{\"iteration\": $iteration, \"exit_code\": $exit_code}" + fi + + iteration=$((iteration + 1)) + + # Adaptive delay + local delay + delay=$(calculate_delay "$iteration") + print_info "Waiting ${delay}s before next iteration..." + sleep "$delay" + done + + print_warning "Max iterations ($max_iterations) reached without completion" + + if type loop_block_task &>/dev/null; then + loop_block_task "Max iterations reached" + fi + + return 1 } # Calculate adaptive delay with exponential backoff @@ -371,26 +392,26 @@ To complete, output: $completion_promise (ONLY when TRUE)" # Returns: 0 # Output: delay in seconds calculate_delay() { - local iteration="$1" - local delay - - if command -v bc &>/dev/null; then - delay=$(echo "scale=0; $RALPH_DELAY_BASE * ($RALPH_DELAY_MULTIPLIER ^ ($iteration - 1))" | bc 2>/dev/null || echo "$RALPH_DELAY_BASE") - if [[ $(echo "$delay > $RALPH_DELAY_MAX" | bc 2>/dev/null || echo "0") -eq 1 ]]; then - delay=$RALPH_DELAY_MAX - fi - else - delay=$RALPH_DELAY_BASE - local i=1 - while [[ $i -lt $iteration ]] && [[ $delay -lt $RALPH_DELAY_MAX ]]; do - delay=$((delay * 2)) - ((i++)) - done - [[ $delay -gt $RALPH_DELAY_MAX ]] && delay=$RALPH_DELAY_MAX - fi - - echo "$delay" - return 0 + local iteration="$1" + local delay + + if command -v bc &>/dev/null; then + delay=$(echo "scale=0; $RALPH_DELAY_BASE * ($RALPH_DELAY_MULTIPLIER ^ ($iteration - 1))" | bc 2>/dev/null || echo "$RALPH_DELAY_BASE") + if [[ $(echo "$delay > $RALPH_DELAY_MAX" | bc 2>/dev/null || echo "0") -eq 1 ]]; then + delay=$RALPH_DELAY_MAX + fi + else + delay=$RALPH_DELAY_BASE + local i=1 + while [[ $i -lt $iteration ]] && [[ $delay -lt $RALPH_DELAY_MAX ]]; do + delay=$((delay * 2)) + ((i++)) + done + [[ $delay -gt $RALPH_DELAY_MAX ]] && delay=$RALPH_DELAY_MAX + fi + + echo "$delay" + return 0 } # ============================================================================= @@ -399,46 +420,58 @@ calculate_delay() { # Setup a new Ralph loop (legacy mode - same session) setup_loop() { - local prompt="" - local max_iterations=0 - local completion_promise="null" - local prompt_parts=() - - while [[ $# -gt 0 ]]; do - case $1 in - --max-iterations) - max_iterations="$2" - shift 2 - ;; - --completion-promise) - completion_promise="$2" - shift 2 - ;; - *) - prompt_parts+=("$1") - shift - ;; - esac - done - - prompt="${prompt_parts[*]}" - - if [[ -z "$prompt" ]]; then - print_error "No prompt provided" - echo "Usage: $SCRIPT_NAME setup \"\" [--max-iterations N] [--completion-promise \"TEXT\"]" - return 1 - fi - - mkdir -p "$RALPH_STATE_DIR" - - local completion_promise_yaml - if [[ -n "$completion_promise" ]] && [[ "$completion_promise" != "null" ]]; then - completion_promise_yaml="\"$completion_promise\"" - else - completion_promise_yaml="null" - fi - - cat > "$RALPH_STATE_FILE" << EOF + local prompt="" + local max_iterations=0 + local completion_promise="null" + local prompt_parts=() + + while [[ $# -gt 0 ]]; do + case $1 in + --max-iterations) + if [[ -z "${2:-}" ]]; then + print_error "--max-iterations requires a number argument" + return 1 + fi + if ! [[ "$2" =~ ^[0-9]+$ ]]; then + print_error "--max-iterations must be a non-negative integer, got: $2" + return 1 + fi + max_iterations="$2" + shift 2 + ;; + --completion-promise) + if [[ -z "${2:-}" ]]; then + print_error "--completion-promise requires a non-empty text argument" + return 1 + fi + completion_promise="$2" + shift 2 + ;; + *) + prompt_parts+=("$1") + shift + ;; + esac + done + + prompt="${prompt_parts[*]}" + + if [[ -z "$prompt" ]]; then + print_error "No prompt provided" + echo "Usage: $SCRIPT_NAME setup \"\" [--max-iterations N] [--completion-promise \"TEXT\"]" + return 1 + fi + + mkdir -p "$RALPH_STATE_DIR" + + local completion_promise_yaml + if [[ -n "$completion_promise" ]] && [[ "$completion_promise" != "null" ]]; then + completion_promise_yaml="\"$completion_promise\"" + else + completion_promise_yaml="null" + fi + + cat >"$RALPH_STATE_FILE" <$completion_promise" - echo "================================================================" - fi - - echo "" - echo "$prompt" - - return 0 + check_other_loops + + echo "" + print_success "Ralph loop activated (legacy mode)" + print_warning "Note: Consider using 'run' command for v2 fresh-context architecture" + echo "" + echo "Iteration: 1" + echo "Max iterations: $(if [[ $max_iterations -gt 0 ]]; then echo "$max_iterations"; else echo "unlimited"; fi)" + echo "Completion promise: $(if [[ "$completion_promise" != "null" ]]; then echo "$completion_promise"; else echo "none"; fi)" + echo "" + echo "State file: $RALPH_STATE_FILE" + + if [[ "$completion_promise" != "null" ]]; then + echo "" + echo "================================================================" + echo "To complete this loop, output: $completion_promise" + echo "================================================================" + fi + + echo "" + echo "$prompt" + + return 0 } # Cancel the active Ralph loop cancel_loop() { - local cancelled=false - - # Cancel v2 state - if type loop_cancel &>/dev/null && [[ -f "${RALPH_STATE_DIR}/loop-state.json" ]]; then - loop_cancel - cancelled=true - fi - - # Cancel legacy state - if [[ -f "$RALPH_STATE_FILE" ]]; then - local iteration - iteration=$(grep '^iteration:' "$RALPH_STATE_FILE" | sed 's/iteration: *//' || echo "unknown") - rm "$RALPH_STATE_FILE" - print_success "Cancelled legacy Ralph loop (was at iteration $iteration)" - cancelled=true - fi - - if [[ "$cancelled" == "false" ]]; then - print_warning "No active Ralph loop found" - fi - - return 0 + local cancelled=false + + # Cancel v2 state + if type loop_cancel &>/dev/null && [[ -f "${RALPH_STATE_DIR}/loop-state.json" ]]; then + loop_cancel + cancelled=true + fi + + # Cancel legacy state + if [[ -f "$RALPH_STATE_FILE" ]]; then + local iteration + iteration=$(grep '^iteration:' "$RALPH_STATE_FILE" | sed 's/iteration: *//' || echo "unknown") + rm "$RALPH_STATE_FILE" + print_success "Cancelled legacy Ralph loop (was at iteration $iteration)" + cancelled=true + fi + + if [[ "$cancelled" == "false" ]]; then + print_warning "No active Ralph loop found" + fi + + return 0 } # Show status show_status() { - local show_all=false - - while [[ $# -gt 0 ]]; do - case $1 in - --all|-a) - show_all=true - shift - ;; - *) - shift - ;; - esac - done - - if [[ "$show_all" == "true" ]]; then - show_status_all - return 0 - fi - - # Check v2 state first - if type loop_show_status &>/dev/null && [[ -f "${RALPH_STATE_DIR}/loop-state.json" ]]; then - echo "=== Ralph Loop v2 Status ===" - loop_show_status - return 0 - fi - - # Fall back to legacy state - if [[ -f "$RALPH_STATE_FILE" ]]; then - echo "=== Ralph Loop Status (Legacy) ===" - echo "" - - local frontmatter - frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE") - - local iteration max_iterations completion_promise started_at - iteration=$(echo "$frontmatter" | grep '^iteration:' | sed 's/iteration: *//') - max_iterations=$(echo "$frontmatter" | grep '^max_iterations:' | sed 's/max_iterations: *//') - completion_promise=$(echo "$frontmatter" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/') - started_at=$(echo "$frontmatter" | grep '^started_at:' | sed 's/started_at: *//' | sed 's/^"\(.*\)"$/\1/') - - echo "Mode: legacy (same-session)" - echo "Active: yes" - echo "Iteration: $iteration" - echo "Max iterations: $(if [[ "$max_iterations" == "0" ]]; then echo "unlimited"; else echo "$max_iterations"; fi)" - echo "Completion promise: $(if [[ "$completion_promise" == "null" ]]; then echo "none"; else echo "$completion_promise"; fi)" - echo "Started: $started_at" - echo "" - print_warning "Consider migrating to v2: ralph-loop-helper.sh run ..." - return 0 - fi - - echo "No active Ralph loop in current directory." - echo "" - echo "Tip: Use 'status --all' to check all worktrees" - return 0 + local show_all=false + + while [[ $# -gt 0 ]]; do + case $1 in + --all | -a) + show_all=true + shift + ;; + *) + shift + ;; + esac + done + + if [[ "$show_all" == "true" ]]; then + show_status_all + return 0 + fi + + # Check v2 state first + if type loop_show_status &>/dev/null && [[ -f "${RALPH_STATE_DIR}/loop-state.json" ]]; then + echo "=== Ralph Loop v2 Status ===" + loop_show_status + return 0 + fi + + # Fall back to legacy state + if [[ -f "$RALPH_STATE_FILE" ]]; then + echo "=== Ralph Loop Status (Legacy) ===" + echo "" + + local frontmatter + frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE") + + local iteration max_iterations completion_promise started_at + iteration=$(echo "$frontmatter" | grep '^iteration:' | sed 's/iteration: *//') + max_iterations=$(echo "$frontmatter" | grep '^max_iterations:' | sed 's/max_iterations: *//') + completion_promise=$(echo "$frontmatter" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/') + started_at=$(echo "$frontmatter" | grep '^started_at:' | sed 's/started_at: *//' | sed 's/^"\(.*\)"$/\1/') + + echo "Mode: legacy (same-session)" + echo "Active: yes" + echo "Iteration: $iteration" + echo "Max iterations: $(if [[ "$max_iterations" == "0" ]]; then echo "unlimited"; else echo "$max_iterations"; fi)" + echo "Completion promise: $(if [[ "$completion_promise" == "null" ]]; then echo "none"; else echo "$completion_promise"; fi)" + echo "Started: $started_at" + echo "" + print_warning "Consider migrating to v2: ralph-loop-helper.sh run ..." + return 0 + fi + + echo "No active Ralph loop in current directory." + echo "" + echo "Tip: Use 'status --all' to check all worktrees" + return 0 } # Show status across all worktrees show_status_all() { - echo "Ralph Loop Status - All Worktrees" - echo "==================================" - echo "" - - if ! git rev-parse --git-dir &>/dev/null; then - print_error "Not in a git repository" - return 1 - fi - - local found_any=false - local current_dir - current_dir=$(pwd) - - while IFS= read -r line; do - if [[ "$line" =~ ^worktree\ (.+)$ ]]; then - local worktree_path="${BASH_REMATCH[1]}" - - # Check for v2 state (new location first, then legacy) - local v2_state="$worktree_path/.agents/loop-state/loop-state.json" - local v2_state_legacy="$worktree_path/.claude/loop-state.json" - local legacy_state="$worktree_path/.agents/loop-state/ralph-loop.local.state" - local legacy_state_old="$worktree_path/.claude/ralph-loop.local.state" - - # Check any of the state file locations - if [[ -f "$v2_state" ]] || [[ -f "$v2_state_legacy" ]] || [[ -f "$legacy_state" ]] || [[ -f "$legacy_state_old" ]]; then - found_any=true - - local branch - branch=$(git -C "$worktree_path" branch --show-current 2>/dev/null || echo "unknown") - - local marker="" - if [[ "$worktree_path" == "$current_dir" ]]; then - marker=" ${GREEN}(current)${NC}" - fi - - echo -e "${BOLD}$branch${NC}$marker" - echo " Path: $worktree_path" - - # Determine which state file to read (prefer new location) - local active_v2_state="" - local active_legacy_state="" - [[ -f "$v2_state" ]] && active_v2_state="$v2_state" - [[ -z "$active_v2_state" && -f "$v2_state_legacy" ]] && active_v2_state="$v2_state_legacy" - [[ -f "$legacy_state" ]] && active_legacy_state="$legacy_state" - [[ -z "$active_legacy_state" && -f "$legacy_state_old" ]] && active_legacy_state="$legacy_state_old" - - if [[ -n "$active_v2_state" ]]; then - local iteration max_iterations - iteration=$(jq -r '.iteration // 0' "$active_v2_state" 2>/dev/null || echo "?") - max_iterations=$(jq -r '.max_iterations // 0' "$active_v2_state" 2>/dev/null || echo "?") - echo " Mode: v2 (fresh context)" - echo " Iteration: $iteration / $max_iterations" - elif [[ -n "$active_legacy_state" ]]; then - local iteration max_iterations - iteration=$(grep '^iteration:' "$active_legacy_state" | sed 's/iteration: *//') - max_iterations=$(grep '^max_iterations:' "$active_legacy_state" | sed 's/max_iterations: *//') - echo " Mode: legacy (same session)" - echo " Iteration: $iteration / $(if [[ "$max_iterations" == "0" ]]; then echo "unlimited"; else echo "$max_iterations"; fi)" - fi - echo "" - fi - fi - done < <(git worktree list --porcelain) - - if [[ "$found_any" == "false" ]]; then - echo -e "${GREEN}No active Ralph loops in any worktree${NC}" - fi - - return 0 + echo "Ralph Loop Status - All Worktrees" + echo "==================================" + echo "" + + if ! git rev-parse --git-dir &>/dev/null; then + print_error "Not in a git repository" + return 1 + fi + + local found_any=false + local current_dir + current_dir=$(pwd) + + while IFS= read -r line; do + if [[ "$line" =~ ^worktree\ (.+)$ ]]; then + local worktree_path="${BASH_REMATCH[1]}" + + # Check for v2 state (new location first, then legacy) + local v2_state="$worktree_path/.agents/loop-state/loop-state.json" + local v2_state_legacy="$worktree_path/.claude/loop-state.json" + local legacy_state="$worktree_path/.agents/loop-state/ralph-loop.local.state" + local legacy_state_old="$worktree_path/.claude/ralph-loop.local.state" + + # Check any of the state file locations + if [[ -f "$v2_state" ]] || [[ -f "$v2_state_legacy" ]] || [[ -f "$legacy_state" ]] || [[ -f "$legacy_state_old" ]]; then + found_any=true + + local branch + branch=$(git -C "$worktree_path" branch --show-current 2>/dev/null || echo "unknown") + + local marker="" + if [[ "$worktree_path" == "$current_dir" ]]; then + marker=" ${GREEN}(current)${NC}" + fi + + echo -e "${BOLD}$branch${NC}$marker" + echo " Path: $worktree_path" + + # Determine which state file to read (prefer new location) + local active_v2_state="" + local active_legacy_state="" + [[ -f "$v2_state" ]] && active_v2_state="$v2_state" + [[ -z "$active_v2_state" && -f "$v2_state_legacy" ]] && active_v2_state="$v2_state_legacy" + [[ -f "$legacy_state" ]] && active_legacy_state="$legacy_state" + [[ -z "$active_legacy_state" && -f "$legacy_state_old" ]] && active_legacy_state="$legacy_state_old" + + if [[ -n "$active_v2_state" ]]; then + local iteration max_iterations + iteration=$(jq -r '.iteration // 0' "$active_v2_state" 2>/dev/null || echo "?") + max_iterations=$(jq -r '.max_iterations // 0' "$active_v2_state" 2>/dev/null || echo "?") + echo " Mode: v2 (fresh context)" + echo " Iteration: $iteration / $max_iterations" + elif [[ -n "$active_legacy_state" ]]; then + local iteration max_iterations + iteration=$(grep '^iteration:' "$active_legacy_state" | sed 's/iteration: *//') + max_iterations=$(grep '^max_iterations:' "$active_legacy_state" | sed 's/max_iterations: *//') + echo " Mode: legacy (same session)" + echo " Iteration: $iteration / $(if [[ "$max_iterations" == "0" ]]; then echo "unlimited"; else echo "$max_iterations"; fi)" + fi + echo "" + fi + fi + done < <(git worktree list --porcelain) + + if [[ "$found_any" == "false" ]]; then + echo -e "${GREEN}No active Ralph loops in any worktree${NC}" + fi + + return 0 } # Check for other active loops check_other_loops() { - if ! git rev-parse --git-dir &>/dev/null; then - return 0 - fi - - local current_dir - current_dir=$(pwd) - local other_loops=() - - while IFS= read -r line; do - if [[ "$line" =~ ^worktree\ (.+)$ ]]; then - local worktree_path="${BASH_REMATCH[1]}" - - if [[ "$worktree_path" == "$current_dir" ]]; then - continue - fi - - # Check all possible state file locations (new and legacy) - local v2_state="$worktree_path/.agents/loop-state/loop-state.json" - local v2_state_legacy="$worktree_path/.claude/loop-state.json" - local legacy_state="$worktree_path/.agents/loop-state/ralph-loop.local.state" - local legacy_state_old="$worktree_path/.claude/ralph-loop.local.state" - - if [[ -f "$v2_state" ]] || [[ -f "$v2_state_legacy" ]] || [[ -f "$legacy_state" ]] || [[ -f "$legacy_state_old" ]]; then - local branch - branch=$(git -C "$worktree_path" branch --show-current 2>/dev/null || echo "unknown") - local iteration="?" - - if [[ -f "$v2_state" ]]; then - iteration=$(jq -r '.iteration // 0' "$v2_state" 2>/dev/null || echo "?") - elif [[ -f "$v2_state_legacy" ]]; then - iteration=$(jq -r '.iteration // 0' "$v2_state_legacy" 2>/dev/null || echo "?") - elif [[ -f "$legacy_state" ]]; then - iteration=$(grep '^iteration:' "$legacy_state" | sed 's/iteration: *//') - elif [[ -f "$legacy_state_old" ]]; then - iteration=$(grep '^iteration:' "$legacy_state_old" | sed 's/iteration: *//') - fi - - other_loops+=("$branch (iteration $iteration)") - fi - fi - done < <(git worktree list --porcelain) - - if [[ ${#other_loops[@]} -gt 0 ]]; then - echo "" - print_warning "Other active Ralph loops detected:" - for loop in "${other_loops[@]}"; do - echo " - $loop" - done - echo "" - fi - - return 0 + if ! git rev-parse --git-dir &>/dev/null; then + return 0 + fi + + local current_dir + current_dir=$(pwd) + local other_loops=() + + while IFS= read -r line; do + if [[ "$line" =~ ^worktree\ (.+)$ ]]; then + local worktree_path="${BASH_REMATCH[1]}" + + if [[ "$worktree_path" == "$current_dir" ]]; then + continue + fi + + # Check all possible state file locations (new and legacy) + local v2_state="$worktree_path/.agents/loop-state/loop-state.json" + local v2_state_legacy="$worktree_path/.claude/loop-state.json" + local legacy_state="$worktree_path/.agents/loop-state/ralph-loop.local.state" + local legacy_state_old="$worktree_path/.claude/ralph-loop.local.state" + + if [[ -f "$v2_state" ]] || [[ -f "$v2_state_legacy" ]] || [[ -f "$legacy_state" ]] || [[ -f "$legacy_state_old" ]]; then + local branch + branch=$(git -C "$worktree_path" branch --show-current 2>/dev/null || echo "unknown") + local iteration="?" + + if [[ -f "$v2_state" ]]; then + iteration=$(jq -r '.iteration // 0' "$v2_state" 2>/dev/null || echo "?") + elif [[ -f "$v2_state_legacy" ]]; then + iteration=$(jq -r '.iteration // 0' "$v2_state_legacy" 2>/dev/null || echo "?") + elif [[ -f "$legacy_state" ]]; then + iteration=$(grep '^iteration:' "$legacy_state" | sed 's/iteration: *//') + elif [[ -f "$legacy_state_old" ]]; then + iteration=$(grep '^iteration:' "$legacy_state_old" | sed 's/iteration: *//') + fi + + other_loops+=("$branch (iteration $iteration)") + fi + fi + done < <(git worktree list --porcelain) + + if [[ ${#other_loops[@]} -gt 0 ]]; then + echo "" + print_warning "Other active Ralph loops detected:" + for loop in "${other_loops[@]}"; do + echo " - $loop" + done + echo "" + fi + + return 0 } # Generate re-anchor prompt generate_reanchor() { - if type loop_generate_reanchor &>/dev/null; then - local keywords="${1:-}" - loop_generate_reanchor "$keywords" - else - print_error "loop-common.sh not loaded" - return 1 - fi + if type loop_generate_reanchor &>/dev/null; then + local keywords="${1:-}" + loop_generate_reanchor "$keywords" + else + print_error "loop-common.sh not loaded" + return 1 + fi } # Check completion (legacy) check_completion() { - local output="$1" - local completion_promise="${2:-}" + local output="$1" + local completion_promise="${2:-}" - if [[ -z "$completion_promise" ]] || [[ "$completion_promise" == "null" ]]; then - echo "NO_PROMISE" - return 0 - fi + if [[ -z "$completion_promise" ]] || [[ "$completion_promise" == "null" ]]; then + echo "NO_PROMISE" + return 0 + fi - if grep -q "$completion_promise" <<< "$output" 2>/dev/null; then - echo "COMPLETE" - return 0 - fi + if grep -q "$completion_promise" <<<"$output" 2>/dev/null; then + echo "COMPLETE" + return 0 + fi - echo "NOT_COMPLETE" - return 0 + echo "NOT_COMPLETE" + return 0 } # Increment iteration (legacy) increment_iteration() { - if [[ ! -f "$RALPH_STATE_FILE" ]]; then - print_error "No active Ralph loop to increment" - return 1 - fi + if [[ ! -f "$RALPH_STATE_FILE" ]]; then + print_error "No active Ralph loop to increment" + return 1 + fi - local current_iteration - current_iteration=$(grep '^iteration:' "$RALPH_STATE_FILE" | sed 's/iteration: *//') + local current_iteration + current_iteration=$(grep '^iteration:' "$RALPH_STATE_FILE" | sed 's/iteration: *//') - if [[ ! "$current_iteration" =~ ^[0-9]+$ ]]; then - print_error "State file corrupted" - return 1 - fi + if [[ ! "$current_iteration" =~ ^[0-9]+$ ]]; then + print_error "State file corrupted" + return 1 + fi - local next_iteration=$((current_iteration + 1)) + local next_iteration=$((current_iteration + 1)) - local temp_file - temp_file=$(mktemp) - sed "s/^iteration: .*/iteration: $next_iteration/" "$RALPH_STATE_FILE" > "$temp_file" - mv "$temp_file" "$RALPH_STATE_FILE" + local temp_file + temp_file=$(mktemp) + sed "s/^iteration: .*/iteration: $next_iteration/" "$RALPH_STATE_FILE" >"$temp_file" + mv "$temp_file" "$RALPH_STATE_FILE" - echo "$next_iteration" - return 0 + echo "$next_iteration" + return 0 } # Get prompt (legacy) get_prompt() { - if [[ ! -f "$RALPH_STATE_FILE" ]]; then - print_error "No active Ralph loop" - return 1 - fi + if [[ ! -f "$RALPH_STATE_FILE" ]]; then + print_error "No active Ralph loop" + return 1 + fi - awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE" - return 0 + awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE" + return 0 } # Get completion promise (legacy) get_completion_promise() { - if [[ ! -f "$RALPH_STATE_FILE" ]]; then - echo "null" - return 0 - fi - - local frontmatter - frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE") - echo "$frontmatter" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/' - return 0 + if [[ ! -f "$RALPH_STATE_FILE" ]]; then + echo "null" + return 0 + fi + + local frontmatter + frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE") + echo "$frontmatter" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/' + return 0 } # ============================================================================= @@ -775,57 +808,57 @@ get_completion_promise() { # ============================================================================= main() { - local command="${1:-help}" - shift || true - - case "$command" in - run) - run_v2_loop "$@" - ;; - external) - # Legacy alias for 'run' - run_v2_loop "$@" - ;; - setup) - setup_loop "$@" - ;; - cancel) - cancel_loop - ;; - status) - show_status "$@" - ;; - reanchor) - generate_reanchor "$@" - ;; - check|check-completion) - if [[ $# -lt 1 ]]; then - print_error "check requires output text as argument" - return 1 - fi - local output="$1" - local promise="${2:-$(get_completion_promise)}" - check_completion "$output" "$promise" - ;; - increment) - increment_iteration - ;; - get-prompt) - get_prompt - ;; - get-completion-promise) - get_completion_promise - ;; - help|--help|-h) - show_help - ;; - *) - print_error "Unknown command: $command" - echo "" - show_help - return 1 - ;; - esac + local command="${1:-help}" + shift || true + + case "$command" in + run) + run_v2_loop "$@" + ;; + external) + # Legacy alias for 'run' + run_v2_loop "$@" + ;; + setup) + setup_loop "$@" + ;; + cancel) + cancel_loop + ;; + status) + show_status "$@" + ;; + reanchor) + generate_reanchor "$@" + ;; + check | check-completion) + if [[ $# -lt 1 ]]; then + print_error "check requires output text as argument" + return 1 + fi + local output="$1" + local promise="${2:-$(get_completion_promise)}" + check_completion "$output" "$promise" + ;; + increment) + increment_iteration + ;; + get-prompt) + get_prompt + ;; + get-completion-promise) + get_completion_promise + ;; + help | --help | -h) + show_help + ;; + *) + print_error "Unknown command: $command" + echo "" + show_help + return 1 + ;; + esac } main "$@"