2424
2525permissions : read-all
2626
27- timeout-minutes : 45
27+ timeout-minutes : 360
2828
2929network :
3030 allowed :
@@ -45,6 +45,7 @@ safe-outputs:
4545 title-prefix : " [Autoloop] "
4646 labels : [automation, autoloop]
4747 protected-files : fallback-to-issue
48+ preserve-branch-name : true
4849 max : 1
4950 push-to-pull-request-branch :
5051 target : " *"
@@ -434,10 +435,26 @@ steps:
434435 # Look up existing PR for the selected program's canonical branch
435436 existing_pr = None
436437 head_branch = None
438+
439+ def verify_pr_is_open(pr_number):
440+ """Check if a PR is still open via the GitHub API. Returns True if open."""
441+ try:
442+ verify_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
443+ verify_req = urllib.request.Request(verify_url, headers={
444+ "Authorization": f"token {github_token}",
445+ "Accept": "application/vnd.github.v3+json",
446+ })
447+ with urllib.request.urlopen(verify_req, timeout=30) as verify_resp:
448+ pr_data = json.loads(verify_resp.read().decode())
449+ return pr_data.get("state") == "open"
450+ except Exception:
451+ return True # If we can't verify, assume it's open (best effort)
452+
437453 if selected:
438454 head_branch = f"autoloop/{selected}"
439455 owner = repo.split("/")[0] if "/" in repo else ""
440456 if owner:
457+ # Strategy 1: exact branch match (works when branch has no framework suffix)
441458 try:
442459 pr_api_url = (
443460 f"https://api.github.com/repos/{repo}/pulls"
@@ -451,22 +468,54 @@ steps:
451468 open_prs = json.loads(pr_resp.read().decode())
452469 if open_prs:
453470 existing_pr = open_prs[0]["number"]
454- print(f" Found existing PR #{existing_pr} for branch {head_branch}")
455- else:
456- print(f" No existing PR found for branch {head_branch}")
471+ print(f" Found existing PR #{existing_pr} for exact branch {head_branch}")
457472 except Exception as e:
458- print(f" Warning: could not check for existing PRs: {e}")
473+ print(f" Warning: could not check for existing PRs by exact branch: {e}")
474+
475+ # Strategy 2: search by title and branch prefix (catches framework-generated
476+ # hash suffixes like autoloop/name-a1b2c3d4e5f6g7h8 created by create-pull-request)
477+ if existing_pr is None:
478+ try:
479+ title_marker = f"[Autoloop: {selected}]"
480+ branch_prefix = head_branch # e.g. autoloop/perf-comparison
481+ list_url = (
482+ f"https://api.github.com/repos/{repo}/pulls"
483+ f"?state=open&per_page=100&sort=created&direction=desc"
484+ )
485+ list_req = urllib.request.Request(list_url, headers={
486+ "Authorization": f"token {github_token}",
487+ "Accept": "application/vnd.github.v3+json",
488+ })
489+ with urllib.request.urlopen(list_req, timeout=30) as list_resp:
490+ all_open_prs = json.loads(list_resp.read().decode())
491+ # Match branch names: exact canonical name or canonical + framework hash suffix
492+ branch_pattern = re.compile(r'^' + re.escape(branch_prefix) + r'(-[0-9a-f]{16})?$')
493+ for pr in all_open_prs:
494+ pr_title = pr.get("title", "")
495+ pr_head_ref = pr.get("head", {}).get("ref", "")
496+ if title_marker in pr_title or branch_pattern.match(pr_head_ref):
497+ existing_pr = pr["number"]
498+ print(f" Found existing PR #{existing_pr} by title/branch-prefix (branch: {pr_head_ref})")
499+ break
500+ if existing_pr is None:
501+ print(f" No existing PR found for program {selected}")
502+ except Exception as e:
503+ print(f" Warning: could not search for existing PRs by title/prefix: {e}")
459504 else:
460505 print(f" Warning: could not parse owner from GITHUB_REPOSITORY='{repo}'")
461506
462- # Also check the state file for a recorded PR number as fallback
507+ # Strategy 3: check the state file for a recorded PR number as fallback
463508 if existing_pr is None:
464509 state = read_program_state(selected)
465510 pr_field = state.get("pr") or ""
466511 pr_match = re.match(r'^#?(\d+)$', pr_field.strip())
467512 if pr_match:
468- existing_pr = int(pr_match.group(1))
469- print(f" Found PR #{existing_pr} from state file for {selected}")
513+ pr_num = int(pr_match.group(1))
514+ if verify_pr_is_open(pr_num):
515+ existing_pr = pr_num
516+ print(f" Found open PR #{existing_pr} from state file for {selected}")
517+ else:
518+ print(f" PR #{pr_num} from state file is no longer open — ignoring")
470519
471520 result = {
472521 "selected": selected,
0 commit comments