#!/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()