Skip to content

Worktree add doesn't check staleness — primary drift and stale local branches ship PRs with day-one merge conflicts #52

@chubes4

Description

@chubes4

Summary

workspace worktree add materializes whatever the local git state happens to be without checking whether the primary checkout or the target branch is drifted from origin. Result: worktrees get created on top of stale bases, commits go on, PR opens — and GitHub immediately flags merge conflicts that didn't need to exist.

What happens today

Workspace::worktree_add() (inc/Workspace/Workspace.php:870-917) branches on whether the requested branch exists locally:

Branch exists locallygit worktree add <path> <branch>.
No fetch. No upstream comparison. If refs/heads/fix/foo was cut from main three weeks ago and origin/main has moved 200 commits since, the worktree materializes at that stale tip. Commits go on top. PR opens. Day-one conflicts.

Branch doesn't exist (creating new):

git fetch --quiet origin
git worktree add -b <branch> <path> <base>

Where <base> is --from=<ref> if passed, else resolve_default_base()origin/HEAD (typically origin/main / origin/trunk), else HEAD.

The fetch is good and origin/HEAD as default is good. But:

  • If caller passes --from=main (local, common reflex) instead of --from=origin/main, the worktree is based on a potentially-stale local ref.
  • If origin/HEAD isn't set, we silently fall through to local HEAD of the primary — which the worktree-native design explicitly says is "the deployed branch, leave it alone." Primary working tree can lag origin/main arbitrarily.

Concrete failure paths

  1. Stale existing branch. Agent session A creates fix/foo off origin/main on day 0. Session A sits idle 2 weeks. Session B runs workspace worktree add data-machine fix/foo on day 14. Worktree materializes at the day-0 tip. Session B commits on top. PR opens against origin/main which is now 80 commits ahead. Day-one conflicts.

  2. Primary drift + local base. Primary data-machine checkout hasn't been git pulled in weeks (primary is read-only by convention). Agent runs worktree add data-machine new-feat --from=main. main is the stale local ref. New branch is cut from a stale base. Same story.

  3. Silent HEAD fallback. A repo without origin/HEAD configured (or where it's missing for any reason) hits return 'HEAD' at Workspace.php:1602. Worktree is cut from whatever the primary is currently checked out at, which may be a feature branch from last week that shouldn't be the base at all.

Proposal

Two separable changes. Ship as separate PRs so each is reviewable independently.

PR 1 — always fetch + surface staleness (non-breaking)

Change 1. Move the git fetch --quiet origin call out of the "new branch" arm and run it unconditionally at the top of worktree_add(). Cheap, shared object store, no downside.

Change 2. After the worktree is created, compute and include staleness info in the response:

  • For the existing-local-branch path: git rev-list --count <branch>..@{upstream} → add stale_commits_behind: N and upstream: origin/<branch> fields. Don't rebase (surprising mutation). Just tell the caller.
  • For the new-branch path: if --from=<base> was a local ref (no origin/ prefix), compute behind-count of <base> vs origin/<base> and surface the same fields.

Change 3. CLI rendering shows a warning line when stale_commits_behind > 0:

Bootstrap: ok
⚠ Worktree branch is 47 commits behind origin/main
  Consider rebasing before opening a PR:
    git -C <path> pull --rebase origin main

Purely additive. No behavior change for non-stale cases. Gives the agent enough signal to choose its next move.

PR 2 — --allow-stale gate + opt-in rebase (behavior change, separate review)

Change 1. Introduce a configurable threshold (default: 50 commits behind). If behind-count exceeds it and --allow-stale is not passed, return WP_Error with guidance:

primary `data-machine` is 127 commits behind origin/main.
Options:
  - workspace git-pull data-machine --allow-primary-mutation (refresh primary first)
  - worktree add … --from=origin/main (cut from remote ref)
  - worktree add … --allow-stale (proceed with known-stale base)

Change 2. --rebase-base opt-in flag. When creating a new branch on a stale base (or materializing a stale existing branch), auto-rebase onto origin/<base> before returning. Off by default (conservative), explicit opt-in.

Out of scope

  • Automatic git pull on the primary. Primary read-only-by-default is an explicit design choice from the worktree-native refactor (PR feat: worktree-native workspace for parallel branch work #22). Don't touch it here.
  • Conflict detection / resolution tooling. That's a PR-side workflow question, not a worktree-creation question.
  • Per-repo base-branch config (some repos use trunk, develop, etc.). resolve_default_base() already handles origin/HEAD correctly; we just need to consume it consistently.

Context

Why this matters

The worktree-native workflow (#22) is supposed to be the fast path: "worktree add → cook → push → PR." Day-one merge conflicts turn that into "worktree add → cook → push → PR → rebase → force-push → re-review," which is exactly the friction worktrees were meant to eliminate. Catching staleness at worktree add time is orders of magnitude cheaper than catching it at PR-review time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions