teleo-codex/ops/auto-fix-trigger.sh

290 lines
10 KiB
Bash
Executable file

#!/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="<!-- auto-fix-attempted -->"
# --- 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"