Skip to content

Local integration branch (dev) accumulates SHA-divergent history vs origin/dev over time — make merge uses git pull where it needs git reset --hard #156

Description

@sebastientaggart

Problem to Fix

After running the standard /start/submit-for-review workflow over many PRs, the local integration branch (BRANCH_DEV, e.g. dev) accumulates a SHA-divergent
history from origin/dev. The two branches remain content-equivalent (same files at the same versions) but git's commit graph shows them as wildly divergent.

Concretely, in a project running CodeCannon since November 2025, the symptom today is:

$ git status
On branch dev
Your branch and 'origin/dev' have diverged,
and have 466 and 465 different commits each, respectively.

$ git diff origin/dev dev --shortstat
1 file changed, 27 insertions(+), 389 deletions(-) # one intentional local change

466 unique local commits vs 465 unique remote commits, but a near-empty content diff — the unmistakable fingerprint of parallel merge commits for the same logical PR,
repeated hundreds of times.

Why it Matters

  1. git status and git pull become misleading and painful. Every interactive git pull on the integration branch attempts a merge instead of a fast-forward,
    requiring conflict resolution that has no real conflicts.
  2. Risk of catastrophic force-push. If a user (or another tool) ever runs git push origin dev --force-with-lease from local dev, they would rewrite the
    production-adjacent branch with a parallel history. Branch protection helps but not all repos / setups have it.
  3. Confusing onboarding signal. A new contributor pulling the repo sees their local dev immediately "ahead" of origin by hundreds of commits with no obvious cause,
    and there's no good way to fix it short of git reset --hard origin/dev.
  4. Compounds across the toolchain. Same pattern likely affects BRANCH_PROD via /deploy.

General Approach

The root cause is in Makefile.agents.mk:68-87, the merge: target:

merge:
      @branch=$$(git rev-parse --abbrev-ref HEAD); \
      ...
      gh pr merge --merge && \
      git checkout $(INTEGRATION_BRANCH) && \
      git pull origin $(INTEGRATION_BRANCH) && \
      echo "PR merged into $(INTEGRATION_BRANCH)."

The trailing git pull is the problem. After gh pr merge --merge:

  • origin/INTEGRATION_BRANCH has a fresh merge commit (created server-side by GitHub).
  • Local INTEGRATION_BRANCH either:
    • Is already behind (first iteration — fast-forward works), OR
    • Has a different prior state from a previous iteration of this same bug (every subsequent iteration — pull creates a merge commit).

Once the divergence starts (could be a single hand-edit on local dev, or an interrupted merge, or an earlier CodeCannon version), every subsequent make merge makes it
worse: each iteration adds two distinct merge commits — one server-side and one client-side — for the same logical PR.

Suggested fix — replace the git pull with git fetch + git reset --hard:

gh pr merge --merge && \
git checkout $(INTEGRATION_BRANCH) && \
git fetch origin $(INTEGRATION_BRANCH) && \
git reset --hard origin/$(INTEGRATION_BRANCH) && \
echo "PR merged into $(INTEGRATION_BRANCH)."

This treats origin/$(INTEGRATION_BRANCH) as the canonical source of truth and makes local $(INTEGRATION_BRANCH) a perfect mirror after every merge. No divergence can accumulate.

--hard is safe here because the CodeCannon workflow explicitly forbids direct commits to the integration branch (all changes flow through feature branches + PRs). Any
local-only commits on INTEGRATION_BRANCH are by definition work that wasn't shipped — and git reflog preserves them for 90 days in case of accident.

The same fix should be applied in two other places that use git pull on the integration branch:

  1. skills/start.md Step 4 ("Create feature branch") — git checkout {{BRANCH_DEV}} && git pull origin {{BRANCH_DEV}} should be git fetch && git reset --hard
    origin/{{BRANCH_DEV}} (or, if preserving "stop if user has uncommitted local dev work" is desired, use git pull --ff-only and fail loudly).
  2. skills/start.md Step 4 (Case B / resume) — same pattern.
  3. Any analogous step in skills/deploy.md that updates local BRANCH_PROD.

Using --ff-only is a slightly more conservative alternative — it would fail on a divergent local branch rather than silently rewriting it, surfacing the divergence to the user the first time it happens.

Complexity

Verification / QA effort: moderate

Manual repro is straightforward (run the workflow a few times against a test repo and observe git rev-list --count origin/dev..dev growing). Automated test would need to spin up a real GitHub-backed repo or mock gh pr merge behavior. Fix itself is one line; ensuring no regressions across trunk-mode (no BRANCH_DEV) vs two-branch vs
three-branch configurations requires testing each profile.

Acceptance Criteria

  • After running /start → /submit-for-review against a clean local clone, git rev-list --count dev..origin/dev and git rev-list --count origin/dev..dev both return 0.
  • Repeating the workflow 5 times in a row still leaves both counts at 0.
  • git log --oneline dev origin/dev shows the same merge commits — no parallel SHAs for the same PR.
  • In trunk mode (no BRANCH_DEV), the same invariant holds for BRANCH_PROD.
  • Existing projects with already-divergent local branches can recover by running the fixed make merge once (the git reset --hard brings them in line) — no manual cleanup
    script needed.
  • skills/start.md and skills/deploy.md use the same fetch + reset (or pull --ff-only) pattern wherever they previously used git pull on an integration branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-qaIssue is ready to be QA tested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions