Skip to content

Fast-forward the long-running branch to main when ahead=0, instead of always merging #36

@mrjf

Description

@mrjf

Summary

In Step 3 of the agent's per-iteration flow, the current guidance says "merge the default branch into the long-running branch to pick up any upstream changes." This produces noisy "Merge main into branch" commits on the canonical branch whenever the branch has no unique commits (e.g., after a PR merged). The noisy merge commits then re-expose every historical file as a patch touch, which triggers gh-aw's E003 (>100 files) the next time the agent opens a new PR.

Replace the unconditional merge with an explicit four-case decision tree based on git rev-list --count.

Concrete failure mode

A long-running branch that was ahead=0, behind=97 relative to main. All 97 commits were already merged into main via previous PRs, so the canonical branch had no unique commits. On the next iteration the agent ran the current Step 3 (git merge origin/main), which produced a single merge commit whose diff-vs-main was 292 files. The subsequent create-pull-request call aggregated that across the commit series to 1434 file touches and failed with:

E003: Cannot create pull request with more than 100 files (received 1434).

This reliably happens to any program whose accepted iterations have merged into main and whose canonical branch was not fast-forwarded afterward.

Current upstream prose

workflows/autoloop.md lines 812–818:

  1. Check out the program's long-running branch autoloop/{program-name}. If the branch does not yet exist, create it from the default branch. If it does exist:
    • Fetch the default branch: git fetch origin main.
    • Check whether the branch's changes have already been merged into main. If git diff origin/main..autoloop/{program-name} produces no output (i.e., every change on the branch is already on main), the branch is stale — reset it to origin/main: git reset --hard origin/main.
    • Otherwise, merge the default branch into the long-running branch to pick up any upstream changes.

Two problems:

  1. git diff origin/main..autoloop/{program-name} checks content delta, not commit delta. After a squash-merge these give the same answer; after a non-fast-forward merge with conflict resolution they can disagree. git rev-list --count on both directions is the correct primitive.
  2. git reset --hard origin/main only updates the local branch. Without a follow-up git push --force-with-lease, the next iteration sees the remote branch still at the old HEAD and merges all over again.

Proposed change — replace Step 3 prose with an explicit script

git fetch origin main
if git ls-remote --exit-code origin autoloop/{program-name}; then
  # Branch exists — fetch it too so the ahead/behind counts below are
  # computed against up-to-date local copies of the remote tips.
  git fetch origin autoloop/{program-name}

  ahead=$(git rev-list --count origin/main..origin/autoloop/{program-name})
  behind=$(git rev-list --count origin/autoloop/{program-name}..origin/main)

  if [ "$ahead" = "0" ] && [ "$behind" != "0" ]; then
    # All of the branch's commits are already in main (typical case after a
    # successful merge of the previous iteration's PR). A merge here would
    # produce a noisy "Merge main into branch" commit that re-exposes every
    # historical file as a patch touch — the failure mode that triggers
    # gh-aw's E003 (>100 files) when a new PR is opened. Fast-forward the
    # canonical branch to main instead. This is lossless because ahead=0
    # proves every commit on the branch is already reachable from main.
    git checkout -B autoloop/{program-name} origin/main
    git push --force-with-lease origin autoloop/{program-name}
  elif [ "$ahead" != "0" ] && [ "$behind" != "0" ]; then
    # True divergence: branch has unique commits AND main has moved on.
    git checkout -B autoloop/{program-name} origin/autoloop/{program-name}
    git merge origin/main --no-edit -m "Merge main into autoloop/{program-name}"
  else
    # Already at main (ahead=0, behind=0) or only ahead of main (ahead>0,
    # behind=0). Nothing to merge — just check out the branch.
    git checkout -B autoloop/{program-name} origin/autoloop/{program-name}
  fi
else
  # Branch does not exist — create it from the default branch
  git checkout -b autoloop/{program-name} origin/main
fi

Four cases made explicit:

ahead behind Action Rationale
0 0 checkout (nothing to do) branch is exactly at main
0 >0 fast-forward + force-push branch's commits already in main; merging would produce noisy merge commit
>0 0 checkout (nothing to do) unique work preserved; no upstream drift to merge
>0 >0 checkout + merge true divergence

force-with-lease rather than force so if anyone else is simultaneously pushing to the branch, the update is rejected rather than overwriting their commits.

Also: sync-branches.md

This repo ships a companion sync-branches.md workflow that's supposed to keep autoloop/* branches in sync with main between iterations. It should adopt the same four-case decision tree — otherwise the companion workflow can re-introduce the noisy merge commits that per-iteration Step 3 avoids.

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions