diff --git a/scripts/clear-stale-ref-pr-5224.py b/scripts/clear-stale-ref-pr-5224.py new file mode 100755 index 0000000..a515fd6 --- /dev/null +++ b/scripts/clear-stale-ref-pr-5224.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Reset PR #5224 (clay/research-2026-04-29) for merge retry after stale-ref conflict. + +Background: PR #5224 has dual-approve verdicts (leo:approve, domain:approve) but +sat in status='conflict' with last_error showing a stale-ref ff-push failure: +"local ff-push failed: cannot lock ref 'refs/heads/main': is at 28e6fa93 but +expected efd613a6". Main moved during the merge attempt, conflict_rebase_attempts +hit the 3-attempt cap, and the PR stalled. + +The dual-approve verdicts mean the eval gate already passed — the failure was +purely mechanical (ff-push race, not content-related). Cherry-pick onto a fresh +main HEAD should resolve cleanly. + +This script resets the conflict state so the merge cycle picks the PR back up: + - status: 'conflict' → 'approved' + - conflict_rebase_attempts: N → 0 + - last_error: cleared + - audit_log entry: stage='ops', event='pr_5224_stale_ref_reset' + +If cherry-pick fails again on retry, merge cycle will set status='conflict' with +a fresh last_error, and we close the PR as stale (downstream clay sessions on +2026-04-30, 2026-05-09 etc. have already merged similar content). + +Per Ganymede observation #3 (May 10): DB mutations go through reviewable code +paths. Audit trail benefits from one-shape-per-commit. + +Idempotent — safe to re-run. If status is already 'approved' (or merged/closed), +prints current state and exits without writing. + +Usage: + python3 scripts/clear-stale-ref-pr-5224.py --dry-run + python3 scripts/clear-stale-ref-pr-5224.py +""" +import argparse +import json +import os +import sqlite3 +import sys +from pathlib import Path + +DB_PATH = os.environ.get("PIPELINE_DB", "/opt/teleo-eval/pipeline/pipeline.db") +PR_NUMBER = 5224 +EXPECTED_BRANCH = "clay/research-2026-04-29" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + if not Path(DB_PATH).exists(): + print(f"ERROR: DB not found at {DB_PATH}", file=sys.stderr) + sys.exit(1) + + conn = sqlite3.connect(DB_PATH, timeout=30) + conn.row_factory = sqlite3.Row + + row = conn.execute( + """SELECT number, branch, status, leo_verdict, domain_verdict, + conflict_rebase_attempts, last_error + FROM prs WHERE number = ?""", + (PR_NUMBER,), + ).fetchone() + if not row: + print(f" No PR row for #{PR_NUMBER} — nothing to reset.") + return + + print(f" PR #{row['number']} ({row['branch']})") + print(f" status={row['status']} leo={row['leo_verdict']} domain={row['domain_verdict']}") + print(f" conflict_rebase_attempts={row['conflict_rebase_attempts']}") + if row["last_error"]: + print(f" last_error={row['last_error'][:140]}") + + # Sanity: branch must match. If someone reused PR #5224 number we don't want + # to rewrite an unrelated PR's state. + if row["branch"] != EXPECTED_BRANCH: + print( + f" ABORT: branch mismatch — expected {EXPECTED_BRANCH}, " + f"got {row['branch']}. Refusing to write." + ) + sys.exit(2) + + # Sanity: dual-approve must hold. Reset is only safe when eval gate already passed. + if not (row["leo_verdict"] == "approve" and row["domain_verdict"] == "approve"): + print( + f" ABORT: PR is not dual-approve " + f"(leo={row['leo_verdict']}, domain={row['domain_verdict']}). " + "Refusing to reset to 'approved' status." + ) + sys.exit(2) + + if row["status"] in ("approved", "merging", "merged", "closed"): + print(f" Already at status={row['status']} — no-op.") + return + + if args.dry_run: + print( + " (dry-run) UPDATE prs SET status='approved', " + "conflict_rebase_attempts=0, last_error=NULL WHERE number=5224" + ) + return + + conn.execute( + """UPDATE prs SET + status = 'approved', + conflict_rebase_attempts = 0, + last_error = NULL, + last_attempt = datetime('now') + WHERE number = ?""", + (PR_NUMBER,), + ) + conn.execute( + """INSERT INTO audit_log (timestamp, stage, event, detail) + VALUES (datetime('now'), 'ops', 'pr_5224_stale_ref_reset', ?)""", + (json.dumps({ + "pr": PR_NUMBER, + "branch": EXPECTED_BRANCH, + "from_status": row["status"], + "from_conflict_rebase_attempts": row["conflict_rebase_attempts"], + "previous_error": (row["last_error"] or "")[:200], + "rationale": "stale-ref ff-push race; dual-approve eval already passed; retry cherry-pick on fresh main", + }),), + ) + conn.commit() + print(" Reset applied. Merge cycle will pick up on next interval (~30s).") + + +if __name__ == "__main__": + main()