#!/usr/bin/env bash # auto-fix-trigger.sh — Find PRs with requested changes, auto-fix mechanical issues. # # Two-tier response to review feedback: # 1. AUTO-FIX: Broken wiki links, missing frontmatter fields, schema compliance # 2. FLAG: Domain classification, claim reframing, confidence changes → notify proposer # # Mechanical issues are fixed by a headless Claude agent on the PR branch. # New commits trigger re-review on the next evaluate-trigger.sh cron cycle. # # Usage: # ./ops/auto-fix-trigger.sh # fix all PRs with requested changes # ./ops/auto-fix-trigger.sh 66 # fix a specific PR # ./ops/auto-fix-trigger.sh --dry-run # show what would be fixed, don't run # # Requirements: # - claude CLI (claude -p for headless mode) # - gh CLI authenticated with repo access # - Run from the teleo-codex repo root # # Safety: # - Lockfile prevents concurrent runs (separate from evaluate-trigger) # - Only fixes mechanical issues — never changes claim substance # - Max one fix cycle per PR per run (prevents infinite loops) # - Tracks fix attempts to avoid re-fixing already-attempted PRs set -euo pipefail # Allow nested Claude Code sessions unset CLAUDECODE 2>/dev/null || true REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" LOCKFILE="/tmp/auto-fix-trigger.lock" LOG_DIR="$REPO_ROOT/ops/sessions" TIMEOUT_SECONDS=300 # 5 min — fixes should be fast DRY_RUN=false SPECIFIC_PR="" FIX_MARKER="" # --- Parse arguments --- for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=true ;; [0-9]*) SPECIFIC_PR="$arg" ;; --help|-h) head -20 "$0" | tail -18 exit 0 ;; *) echo "Unknown argument: $arg" exit 1 ;; esac done # --- Pre-flight checks --- if ! gh auth status >/dev/null 2>&1; then echo "ERROR: gh CLI not authenticated." exit 1 fi if ! command -v claude >/dev/null 2>&1; then echo "ERROR: claude CLI not found." exit 1 fi # --- Lockfile --- if [ -f "$LOCKFILE" ]; then LOCK_PID=$(cat "$LOCKFILE" 2>/dev/null || echo "") if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then echo "Another auto-fix-trigger is running (PID $LOCK_PID). Exiting." exit 1 else rm -f "$LOCKFILE" fi fi echo $$ > "$LOCKFILE" trap 'rm -f "$LOCKFILE"' EXIT mkdir -p "$LOG_DIR" # --- Find PRs needing fixes --- if [ -n "$SPECIFIC_PR" ]; then PRS_TO_FIX="$SPECIFIC_PR" else OPEN_PRS=$(gh pr list --state open --json number --jq '.[].number' 2>/dev/null || echo "") if [ -z "$OPEN_PRS" ]; then echo "No open PRs found." exit 0 fi PRS_TO_FIX="" for pr in $OPEN_PRS; do # Check if PR has request_changes reviews HAS_CHANGES_REQUESTED=$(gh api "repos/{owner}/{repo}/pulls/$pr/reviews" \ --jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0") if [ "$HAS_CHANGES_REQUESTED" -eq 0 ]; then continue fi # Check if auto-fix was already attempted (marker comment exists) ALREADY_ATTEMPTED=$(gh pr view "$pr" --json comments \ --jq "[.comments[].body | select(contains(\"$FIX_MARKER\"))] | length" 2>/dev/null || echo "0") # Check if there are new commits since the last auto-fix attempt if [ "$ALREADY_ATTEMPTED" -gt 0 ]; then LAST_FIX_DATE=$(gh pr view "$pr" --json comments \ --jq "[.comments[] | select(.body | contains(\"$FIX_MARKER\")) | .createdAt] | last" 2>/dev/null || echo "") LAST_COMMIT_DATE=$(gh pr view "$pr" --json commits --jq '.commits[-1].committedDate' 2>/dev/null || echo "") if [ -n "$LAST_FIX_DATE" ] && [ -n "$LAST_COMMIT_DATE" ] && [[ "$LAST_COMMIT_DATE" < "$LAST_FIX_DATE" ]]; then echo "PR #$pr: Auto-fix already attempted, no new commits. Skipping." continue fi fi PRS_TO_FIX="$PRS_TO_FIX $pr" done PRS_TO_FIX=$(echo "$PRS_TO_FIX" | xargs) if [ -z "$PRS_TO_FIX" ]; then echo "No PRs need auto-fixing." exit 0 fi fi echo "PRs to auto-fix: $PRS_TO_FIX" if [ "$DRY_RUN" = true ]; then for pr in $PRS_TO_FIX; do echo "[DRY RUN] Would attempt auto-fix on PR #$pr" # Show the review feedback summary gh pr view "$pr" --json comments \ --jq '.comments[] | select(.body | test("Verdict.*request_changes|request changes"; "i")) | .body' 2>/dev/null \ | grep -iE "broken|missing|schema|field|link" | head -10 || echo " (no mechanical issues detected in comments)" done exit 0 fi # --- Auto-fix each PR --- FIXED=0 FLAGGED=0 for pr in $PRS_TO_FIX; do echo "" echo "=== Auto-fix PR #$pr ===" # Get the review feedback REVIEW_TEXT=$(gh pr view "$pr" --json comments \ --jq '.comments[].body' 2>/dev/null || echo "") if [ -z "$REVIEW_TEXT" ]; then echo " No review comments found. Skipping." continue fi # Classify issues as mechanical vs substantive # Mechanical: broken links, missing fields, schema compliance MECHANICAL_PATTERNS="broken wiki link|broken link|missing.*challenged_by|missing.*field|schema compliance|link.*needs to match|link text needs|missing wiki.link|add.*wiki.link|BROKEN WIKI LINK" # Substantive: domain classification, reframing, confidence, consider SUBSTANTIVE_PATTERNS="domain classification|consider.*reframing|soften.*to|confidence.*recalibrat|consider whether|territory violation|evaluator-as-proposer|conflict.of.interest" HAS_MECHANICAL=$(echo "$REVIEW_TEXT" | grep -ciE "$MECHANICAL_PATTERNS" || echo "0") HAS_SUBSTANTIVE=$(echo "$REVIEW_TEXT" | grep -ciE "$SUBSTANTIVE_PATTERNS" || echo "0") echo " Mechanical issues: $HAS_MECHANICAL" echo " Substantive issues: $HAS_SUBSTANTIVE" # --- Handle mechanical fixes --- if [ "$HAS_MECHANICAL" -gt 0 ]; then echo " Attempting mechanical auto-fix..." # Extract just the mechanical feedback lines for the fix agent MECHANICAL_FEEDBACK=$(echo "$REVIEW_TEXT" | grep -iE "$MECHANICAL_PATTERNS" | head -20) TIMESTAMP=$(date +%Y%m%d-%H%M%S) FIX_LOG="$LOG_DIR/autofix-pr${pr}-${TIMESTAMP}.log" PR_BRANCH=$(gh pr view "$pr" --json headRefName --jq '.headRefName' 2>/dev/null || echo "") FIX_PROMPT="You are a mechanical fix agent. Your ONLY job is to fix objective, mechanical issues in PR #${pr}. RULES: - Fix ONLY broken wiki links, missing frontmatter fields, and schema compliance issues. - NEVER change claim titles, arguments, confidence levels, or domain classification. - NEVER add new claims or remove existing ones. - NEVER rewrite prose or change the substance of any argument. - If you're unsure whether something is mechanical, SKIP IT. STEPS: 1. Run: gh pr checkout ${pr} 2. Read the review feedback below to understand what needs fixing. 3. For each mechanical issue: a. BROKEN WIKI LINKS: Find the correct filename with Glob, update the [[link]] text to match exactly. b. MISSING challenged_by: If a claim is rated 'likely' or higher and reviewers noted missing challenged_by, add a challenged_by field to the frontmatter. Use the counter-argument already mentioned in the claim body. c. MISSING WIKI LINKS: If reviewers named specific claims that should be linked, verify the file exists with Glob, then add to the Relevant Notes section. 4. Stage and commit changes: git add -A git commit -m 'auto-fix: mechanical fixes from review feedback - What was fixed (list each fix) Auto-Fix-Agent: teleo-eval-orchestrator' 5. Push: git push origin ${PR_BRANCH} REVIEW FEEDBACK (fix only the mechanical issues): ${MECHANICAL_FEEDBACK} FULL REVIEW CONTEXT: $(echo "$REVIEW_TEXT" | head -200) Work autonomously. Do not ask for confirmation. If there's nothing mechanical to fix, just exit." if perl -e "alarm $TIMEOUT_SECONDS; exec @ARGV" claude -p \ --model "sonnet" \ --allowedTools "Read,Write,Edit,Bash,Glob,Grep" \ --permission-mode bypassPermissions \ "$FIX_PROMPT" \ > "$FIX_LOG" 2>&1; then echo " Auto-fix agent completed." # Check if any commits were actually pushed NEW_COMMIT_DATE=$(gh pr view "$pr" --json commits --jq '.commits[-1].committedDate' 2>/dev/null || echo "") echo " Latest commit: $NEW_COMMIT_DATE" FIXED=$((FIXED + 1)) else EXIT_CODE=$? if [ "$EXIT_CODE" -eq 142 ] || [ "$EXIT_CODE" -eq 124 ]; then echo " Auto-fix: TIMEOUT after ${TIMEOUT_SECONDS}s." else echo " Auto-fix: FAILED (exit code $EXIT_CODE)." fi fi echo " Log: $FIX_LOG" fi # --- Flag substantive issues to proposer --- if [ "$HAS_SUBSTANTIVE" -gt 0 ]; then echo " Flagging substantive issues for proposer..." SUBSTANTIVE_FEEDBACK=$(echo "$REVIEW_TEXT" | grep -iE "$SUBSTANTIVE_PATTERNS" | head -15) # Determine proposer from branch name PROPOSER=$(gh pr view "$pr" --json headRefName --jq '.headRefName' 2>/dev/null | cut -d'/' -f1) FLAG_COMMENT="## Substantive Feedback — Needs Proposer Input The following review feedback requires the proposer's judgment and cannot be auto-fixed: \`\`\` ${SUBSTANTIVE_FEEDBACK} \`\`\` **Proposer:** ${PROPOSER} **Action needed:** Review the feedback above, make changes if you agree, then push to trigger re-review. $FIX_MARKER *Auto-fix agent — mechanical issues were ${HAS_MECHANICAL:+addressed}${HAS_MECHANICAL:-not found}, substantive issues flagged for human/agent review.*" gh pr comment "$pr" --body "$FLAG_COMMENT" 2>/dev/null echo " Flagged to proposer: $PROPOSER" FLAGGED=$((FLAGGED + 1)) elif [ "$HAS_MECHANICAL" -gt 0 ]; then # Only mechanical issues — post marker comment so we don't re-attempt MARKER_COMMENT="$FIX_MARKER *Auto-fix agent ran — mechanical fixes attempted. Substantive issues: none. Awaiting re-review.*" gh pr comment "$pr" --body "$MARKER_COMMENT" 2>/dev/null fi # Clean up branch git checkout main 2>/dev/null || git checkout -f main PR_BRANCH=$(gh pr view "$pr" --json headRefName --jq '.headRefName' 2>/dev/null || echo "") [ -n "$PR_BRANCH" ] && git branch -D "$PR_BRANCH" 2>/dev/null || true echo " Done." done echo "" echo "=== Auto-Fix Summary ===" echo "Fixed: $FIXED" echo "Flagged: $FLAGGED" echo "Logs: $LOG_DIR"