You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 locally → git 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.
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
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.
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.
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.
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:
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.
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
inc/Workspace/Workspace.php:870 — worktree_add() entry point
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.
Summary
workspace worktree addmaterializes whatever the local git state happens to be without checking whether the primary checkout or the target branch is drifted fromorigin. 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 locally →
git worktree add <path> <branch>.No fetch. No upstream comparison. If
refs/heads/fix/foowas cut frommainthree weeks ago andorigin/mainhas 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):
Where
<base>is--from=<ref>if passed, elseresolve_default_base()→origin/HEAD(typicallyorigin/main/origin/trunk), elseHEAD.The fetch is good and
origin/HEADas default is good. But:--from=main(local, common reflex) instead of--from=origin/main, the worktree is based on a potentially-stale local ref.origin/HEADisn't set, we silently fall through to localHEADof the primary — which the worktree-native design explicitly says is "the deployed branch, leave it alone." Primary working tree can lagorigin/mainarbitrarily.Concrete failure paths
Stale existing branch. Agent session A creates
fix/fooofforigin/mainon day 0. Session A sits idle 2 weeks. Session B runsworkspace worktree add data-machine fix/fooon day 14. Worktree materializes at the day-0 tip. Session B commits on top. PR opens againstorigin/mainwhich is now 80 commits ahead. Day-one conflicts.Primary drift + local base. Primary
data-machinecheckout hasn't beengit pulled in weeks (primary is read-only by convention). Agent runsworktree add data-machine new-feat --from=main.mainis the stale local ref. New branch is cut from a stale base. Same story.Silent
HEADfallback. A repo withoutorigin/HEADconfigured (or where it's missing for any reason) hitsreturn 'HEAD'atWorkspace.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 origincall out of the "new branch" arm and run it unconditionally at the top ofworktree_add(). Cheap, shared object store, no downside.Change 2. After the worktree is created, compute and include staleness info in the response:
git rev-list --count <branch>..@{upstream}→ addstale_commits_behind: Nandupstream: origin/<branch>fields. Don't rebase (surprising mutation). Just tell the caller.--from=<base>was a local ref (noorigin/prefix), compute behind-count of<base>vsorigin/<base>and surface the same fields.Change 3. CLI rendering shows a warning line when
stale_commits_behind > 0:Purely additive. No behavior change for non-stale cases. Gives the agent enough signal to choose its next move.
PR 2 —
--allow-stalegate + opt-in rebase (behavior change, separate review)Change 1. Introduce a configurable threshold (default: 50 commits behind). If behind-count exceeds it and
--allow-staleis not passed, returnWP_Errorwith guidance:Change 2.
--rebase-baseopt-in flag. When creating a new branch on a stale base (or materializing a stale existing branch), auto-rebase ontoorigin/<base>before returning. Off by default (conservative), explicit opt-in.Out of scope
git pullon 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.trunk,develop, etc.).resolve_default_base()already handlesorigin/HEADcorrectly; we just need to consume it consistently.Context
inc/Workspace/Workspace.php:870—worktree_add()entry pointinc/Workspace/Workspace.php:1594—resolve_default_base()inc/Workspace/Workspace.php:503—git_pull()(for the "refresh primary" guidance)--skip-bootstrap), PR feat(workspace): inject site-agent context into worktrees on creation #46 (--skip-context-injection). Same pattern applies to--allow-stale.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 addtime is orders of magnitude cheaper than catching it at PR-review time.