Skip to content

Enforce single-PR-per-program invariant: preserve-branch-name, existing_pr/head_branch, no-suffix prose#44

Merged
mrjf merged 3 commits intomainfrom
copilot/enforce-single-pr-per-program
Apr 23, 2026
Merged

Enforce single-PR-per-program invariant: preserve-branch-name, existing_pr/head_branch, no-suffix prose#44
mrjf merged 3 commits intomainfrom
copilot/enforce-single-pr-per-program

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 22, 2026

gh-aw silently appends a hex salt to safe-output branches on collision, so every program's second iteration spawned a fresh draft PR instead of pushing to the existing one — a long-running program ended up with a stack of 10+ orphan PRs. Fixing this requires three coordinated pieces (config + scheduler + prose); any one alone is bypassable.

A. Config — workflows/autoloop.md safe-outputs

  • create-pull-request.preserve-branch-name: true so gh-aw stops auto-suffixing the canonical autoloop/{program} branch.
  • max: 1 on both create-pull-request and push-to-pull-request-branch (one safe-output of either kind per iteration, never both).

B. Scheduler — new existing_pr / head_branch outputs

New helper find_existing_pr_for_branch(repo, program_name, github_token) in workflows/scripts/autoloop_scheduler.py (mirrored to .github/workflows/scripts/) queries the open draft PR for autoloop/{program-name} and writes both fields into /tmp/gh-aw/autoloop.json:

  • Strategy 1: exact head=owner:autoloop/{name} query.
  • Strategy 2 fallback: paginate open PRs and match either a legacy framework-suffixed branch (autoloop/{name}-[0-9a-f]{6,40}) or a [Autoloop: {name}] title prefix — so installations upgrading from before preserve-branch-name: true find their in-flight PR instead of opening a duplicate.

The HTTP layer is broken out into a module-level _http_get_json helper so tests can inject a stub via the http_get_json parameter without touching urllib.

head_branch is always exactly autoloop/{program-name} (never null when a program is selected); existing_pr is the PR number or null.

C. Prose

  • "⚠️ CRITICAL — Branch Name Must Be Exact" callout under Branch Naming Convention with the three forbidden suffix patterns.
  • Step 5a (Push and wait for CI) item 3 rewritten into ordered sub-steps: (a) check existing_pr, (b) state-file PR field as fallback (closed/merged → treat as null), (c) only then create-pull-request with explicit branch: from head_branch. Step 5c (Accept) instructs the agent to use push-to-pull-request-branch rather than create-pull-request whenever a PR already exists.
  • New "Common Mistakes to Avoid" section restating the no-suffix and no-duplicate-PR rules.

Tests

8 new tests across tests/test_scheduling.py and tests/test_scheduler_e2e.py:

  • TestSafeOutputsConfig / TestProseGuidance — assert the config flags and prose anchors are present (regression guards against silent removal).
  • TestFindExistingPRForBranch — drives the new Python helper with a stubbed http_get_json covering no-PR, canonical hit, hex-suffix fallback, title-prefix fallback, and two negative cases (autoloop/coverage-extras and autoloop/signal must not match coverage / signal_processing).
  • Two new e2e tests run the scheduler end-to-end and assert head_branch == "autoloop/{name}" when a program is selected and head_branch is None when nothing is selected.

Out of scope

Copilot AI changed the title [WIP] Fix invariant for single PR per program Enforce single-PR-per-program invariant: preserve-branch-name, existing_pr/head_branch, no-suffix prose Apr 22, 2026
Copilot AI requested a review from mrjf April 22, 2026 22:38
Copilot finished work on behalf of mrjf April 22, 2026 22:38
@mrjf mrjf marked this pull request as ready for review April 23, 2026 02:48
@mrjf
Copy link
Copy Markdown
Contributor

mrjf commented Apr 23, 2026

@copilot resolve merge conflicts, and fix failing tests

Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 23, 2026

@copilot resolve merge conflicts, and fix failing tests

Done in 1dfeaa6. Merged origin/main, ported findExistingPRForBranch from the removed JS heredoc to Python (find_existing_pr_for_branch in workflows/scripts/autoloop_scheduler.py, mirrored to .github/workflows/scripts/), re-applied the prose changes on top of main's new Step 5a/5b/5c CI-gated accept flow, and rewrote the tests to call the Python helper directly with a stubbed http_get_json (no more Node subprocess). All 123 tests pass.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/bogus.invalid/bogus/issues
    • Triggering command: /usr/bin/python /usr/bin/python /home/REDACTED/work/autoloop/autoloop/workflows/scripts/autoloop_scheduler.py (http block)
  • https://api.github.com/repos/bogus.invalid/bogus/pulls
    • Triggering command: /usr/bin/python /usr/bin/python /home/REDACTED/work/autoloop/autoloop/workflows/scripts/autoloop_scheduler.py (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of mrjf April 23, 2026 02:57
@mrjf mrjf merged commit 2e07c75 into main Apr 23, 2026
8 checks passed
@mrjf mrjf deleted the copilot/enforce-single-pr-per-program branch April 23, 2026 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce the single-PR-per-program invariant: preserve-branch-name, existing_pr/head_branch pre-step outputs, explicit 'no suffixes' prose

2 participants