Skip to content

Commit 7fefad0

Browse files
authored
Merge pull request #124 from githubnext/copilot/fix-multiple-prs-autoloop
Fix multiple PRs opened for single autoloop program
2 parents 0696674 + d3d7f15 commit 7fefad0

4 files changed

Lines changed: 104 additions & 39 deletions

File tree

.github/workflows/autoloop.lock.yml

Lines changed: 18 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/autoloop.md

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,50 @@ steps:
431431
if selected in issue_programs:
432432
selected_issue = issue_programs[selected]["issue_number"]
433433
434+
# Look up existing PR for the selected program's canonical branch
435+
existing_pr = None
436+
head_branch = None
437+
if selected:
438+
head_branch = f"autoloop/{selected}"
439+
owner = repo.split("/")[0] if "/" in repo else ""
440+
if owner:
441+
try:
442+
pr_api_url = (
443+
f"https://api.github.com/repos/{repo}/pulls"
444+
f"?state=open&head={owner}:{head_branch}&per_page=5"
445+
)
446+
pr_req = urllib.request.Request(pr_api_url, headers={
447+
"Authorization": f"token {github_token}",
448+
"Accept": "application/vnd.github.v3+json",
449+
})
450+
with urllib.request.urlopen(pr_req, timeout=30) as pr_resp:
451+
open_prs = json.loads(pr_resp.read().decode())
452+
if open_prs:
453+
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}")
457+
except Exception as e:
458+
print(f" Warning: could not check for existing PRs: {e}")
459+
else:
460+
print(f" Warning: could not parse owner from GITHUB_REPOSITORY='{repo}'")
461+
462+
# Also check the state file for a recorded PR number as fallback
463+
if existing_pr is None:
464+
state = read_program_state(selected)
465+
pr_field = state.get("pr") or ""
466+
pr_match = re.match(r'^#?(\d+)$', pr_field.strip())
467+
if pr_match:
468+
existing_pr = int(pr_match.group(1))
469+
print(f" Found PR #{existing_pr} from state file for {selected}")
470+
434471
result = {
435472
"selected": selected,
436473
"selected_file": selected_file,
437474
"selected_issue": selected_issue,
438475
"selected_target_metric": selected_target_metric,
476+
"existing_pr": existing_pr,
477+
"head_branch": head_branch,
439478
"issue_programs": {name: info["issue_number"] for name, info in issue_programs.items()},
440479
"deferred": deferred,
441480
"skipped": skipped,
@@ -449,6 +488,10 @@ steps:
449488
450489
print("=== Autoloop Program Check ===")
451490
print(f"Selected program: {selected or '(none)'} ({selected_file or 'n/a'})")
491+
if existing_pr:
492+
print(f"Existing PR: #{existing_pr} (branch: {head_branch})")
493+
else:
494+
print(f"Existing PR: (none — will create on first accepted iteration)")
452495
print(f"Deferred (next run): {deferred or '(none)'}")
453496
print(f"Programs skipped: {[s['name'] for s in skipped] or '(none)'}")
454497
print(f"Programs unconfigured: {unconfigured or '(none)'}")
@@ -538,6 +581,8 @@ The pre-step has already determined which program to run. Read `/tmp/gh-aw/autol
538581
- **`selected_file`**: The full path to the program's markdown file (either `.autoloop/programs/<name>/program.md`, `.autoloop/programs/<name>.md`, or `/tmp/gh-aw/issue-programs/<name>.md` for issue-based programs).
539582
- **`selected_issue`**: The GitHub issue number if the selected program came from an issue, or `null` if it came from a file.
540583
- **`selected_target_metric`**: The `target-metric` value from the program's frontmatter (a number), or `null` if the program is open-ended. Used to check the [halting condition](#halting-condition) after each accepted iteration.
584+
- **`existing_pr`**: The PR number (e.g., `42`) of an already-open PR for this program's branch, or `null` if no open PR exists. **If this is not null, you MUST use `push-to-pull-request-branch` to push to this PR — do NOT call `create-pull-request`.**
585+
- **`head_branch`**: The canonical branch name for this program (e.g., `autoloop/coverage`). Always use this exact branch name — never append suffixes.
541586
- **`issue_programs`**: A mapping of program name → issue number for all discovered issue-based programs.
542587
- **`deferred`**: Other programs that were due but will be handled in future runs.
543588
- **`unconfigured`**: Programs that still have the sentinel or placeholder content.
@@ -550,6 +595,7 @@ If `selected` is not null:
550595
3. Read the current state of all target files.
551596
4. Read the state file `{selected}.md` from the repo-memory folder for all state: the ⚙️ Machine State table (scheduling fields) plus the research sections (priorities, lessons, foreclosed avenues, iteration history).
552597
5. If `selected_issue` is not null, this is an issue-based program — also read the issue comments for any human steering input.
598+
6. **Check `existing_pr`**: if it is not null, a PR already exists — use `push-to-pull-request-branch` to push commits to it. Only call `create-pull-request` when `existing_pr` is null.
553599

554600
## Multiple Programs
555601

@@ -694,7 +740,7 @@ Each run executes **one iteration for the single selected program**:
694740

695741
If the state file does not yet exist, create it in the repo-memory folder using the template defined in the [Repo Memory](#repo-memory) section.
696742

697-
3. Note the `PR` field from the Machine State table. If it contains a PR number (e.g., `#42`), that is the **existing draft PR** for this program — you must update it, not create a new one.
743+
3. Note the `existing_pr` field from `/tmp/gh-aw/autoloop.json`. If it is not null, that is the **existing draft PR** for this program — you must push to it using `push-to-pull-request-branch`, not create a new one. Also check the `PR` field from the Machine State table as a fallback.
698744

699745
### Step 2: Analyze and Propose
700746

@@ -743,15 +789,15 @@ Each run executes **one iteration for the single selected program**:
743789
- Commit message body (after a blank line): `Run: {run_url}` referencing the GitHub Actions run URL.
744790
2. Push the commit to the long-running branch `autoloop/{program-name}`.
745791
3. **Find the existing PR or create one** — follow these steps in order:
746-
a. Check the `PR` field in the state file's **⚙️ Machine State** table. If it contains a PR number (e.g., `#42`), that is the existing draft PR.
747-
b. If the state file has no PR number, search GitHub for open PRs with head branch `autoloop/{program-name}`. Use the GitHub API: `GET /repos/{owner}/{repo}/pulls?state=open&head={owner}:autoloop/{program-name}`.
748-
c. **If an existing PR is found** (from either step a or b): use `push-to-pull-request-branch` to push additional commits to the existing PR. Update the PR body with the latest metric and a summary of the most recent accepted iteration. Add a comment to the PR summarizing the iteration: what changed, old metric, new metric, improvement delta, and a link to the actions run. **Do NOT call `create-pull-request`.**
749-
d. **If NO PR exists** for `autoloop/{program-name}`: create one using `create-pull-request`:
792+
a. **First, check `existing_pr` from `/tmp/gh-aw/autoloop.json`.** The pre-step has already looked up the open PR for this program. If `existing_pr` is not null, that is the existing draft PR — skip to step (c).
793+
b. If `existing_pr` is null, also check the `PR` field in the state file's **⚙️ Machine State** table as a fallback. If it contains a PR number (e.g., `#42`), verify it is still open via the GitHub API.
794+
c. **If an existing PR is found** (from step a or b): use `push-to-pull-request-branch` to push additional commits to the existing PR. Update the PR body with the latest metric and a summary of the most recent accepted iteration. Add a comment to the PR summarizing the iteration: what changed, old metric, new metric, improvement delta, and a link to the actions run. **Do NOT call `create-pull-request`.**
795+
d. **If NO PR exists** for `autoloop/{program-name}` (both `existing_pr` is null AND the state file has no PR): create one using `create-pull-request`:
750796
- Branch: `autoloop/{program-name}` (the branch you already created in Step 3 — do NOT let the framework auto-generate a branch name)
751797
- Title: `[Autoloop: {program-name}]`
752798
- Body includes: a summary of the program goal, link to the steering issue, the current best metric, and AI disclosure: `🤖 *This PR is maintained by Autoloop. Each accepted iteration adds a commit to this branch.*`
753799

754-
> ⚠️ **Never create a new PR if one already exists for `autoloop/{program-name}`.** Each program must have exactly one draft PR at any time. If you are unsure whether a PR exists, check the GitHub API before calling `create-pull-request`.
800+
> ⚠️ **Never create a new PR if one already exists for `autoloop/{program-name}`.** Each program must have exactly one draft PR at any time. The pre-step provides `existing_pr` in autoloop.json — always check it first. Only call `create-pull-request` when `existing_pr` is null AND the state file has no PR number.
755801
4. Ensure the steering issue exists (see [Steering Issue](#steering-issue) below). Add a comment to the steering issue linking to the commit and actions run.
756802
5. Add an entry to the experiment log issue.
757803
6. Update the state file `{program-name}.md` in the repo-memory folder:
@@ -790,6 +836,13 @@ Maintain a single open issue **per program** titled `[Autoloop: {program-name}]
790836
```markdown
791837
🤖 *Autoloop — an iterative optimization agent for this repository.*
792838

839+
| | |
840+
|---|---|
841+
| **Branch** | [`autoloop/{program-name}`](https://github.com/{owner}/{repo}/tree/autoloop/{program-name}) |
842+
| **Pull Request** | #{pr_number} |
843+
| **Steering Issue** | #{steering_issue_number} |
844+
| **State File** | [`{program-name}.md`](https://github.com/{owner}/{repo}/blob/memory/autoloop/{program-name}.md) |
845+
793846
## Program
794847

795848
**Goal**: {one-line summary from program.md}
@@ -817,6 +870,7 @@ Maintain a single open issue **per program** titled `[Autoloop: {program-name}]
817870
- Iterations in **reverse chronological order** (newest first).
818871
- Each iteration heading links to its GitHub Actions run.
819872
- Use `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}` for the current run URL.
873+
- The **links table at the top** must always show the current branch, PR, steering issue, and state file. Update the PR number when a new PR is created. When creating a continuation issue for a new month, copy the links table from the previous issue.
820874
- Close the previous month's issue and create a new one at month boundaries.
821875
- Maximum 50 iterations per issue; create a continuation issue if exceeded.
822876

@@ -1148,9 +1202,10 @@ After each iteration, prepend an entry to the **📊 Iteration History** section
11481202
> **Do NOT create a new branch with a suffix for each iteration.**
11491203
> Correct: `autoloop/coverage`
11501204
> Wrong: `autoloop/coverage-abc123`, `autoloop/coverage-iter42`, `autoloop/coverage-deadbeef1234`
1205+
> Use the `head_branch` field from `autoloop.json` — it is always the canonical name.
11511206
11521207
> **Do NOT create a new PR if one already exists for `autoloop/{program-name}`.**
1153-
> Always check the state file's `PR` field and the GitHub API before calling `create-pull-request`. If a PR exists, use `push-to-pull-request-branch` instead.
1208+
> The pre-step provides `existing_pr` in `autoloop.json`. If it is not null, **always** use `push-to-pull-request-branch` — never call `create-pull-request`. Only create a PR when `existing_pr` is null AND the state file has no PR number.
11541209
11551210
> **Do NOT let the gh-aw framework auto-generate a branch name when creating a PR.**
11561211
> Always specify the branch explicitly as `autoloop/{program-name}` when calling `create-pull-request`.

0 commit comments

Comments
 (0)