Skip to content

pilot worktree sync is not idempotent — a second sync of the same lane fails with an empty Merge conflict:  #171

Description

@buddhamangler

Summary

pilot worktree sync squash-merges the lane branch into the base branch. A squash records no ancestry, so git merge-base <base> spec/<slug> stays pinned at the lane's fork point no matter how many syncs have landed. Every later sync therefore replays the whole branch-vs-fork diff against a base branch that already carries the earlier syncs' content.

Any file an earlier sync already merged, and that the lane has modified since, comes back as both-sides-modified and the sync fails with:

Merge conflict: 

— an empty detail after the colon, so there is nothing to act on.

Follow-up work that only adds new files syncs repeatedly without complaint (git sees identical additions on both sides), which makes the failure look intermittent rather than structural.

Version: 9.14.0. Not addressed in 9.15.0.

Why this is a normal path, not an edge case

The spec workflow's sync gate offers "keep worktree, sync later" as a first-class choice. Taking it, continuing work in the lane, and syncing again is the intended use of that option — and it is exactly what breaks, because the plan/spec file itself was part of the first squash and is usually the first thing edited afterwards.

Reproduction

mkdir demo && cd demo
git init -q -b main . && echo v1 > f.txt && git add f.txt && git commit -qm base

pilot worktree create demo --json
WT=$(ls -d .worktrees/*)

(cd "$WT" && echo v2 > f.txt && git commit -qam "round 1")
pilot worktree sync --json demo          # ok

(cd "$WT" && echo v3 > f.txt && git commit -qam "round 2")
pilot worktree sync --json demo          # fails

Actual output:

{"success": true, "path": ".../demo/.worktrees/spec-demo-5a07d3c", "branch": "spec/demo", "base_branch": "main", "plan_slug": "demo"}
{"success": true, "error": null, "files_changed": 1, "commit_hash": "74c6a89..."}
{"success": false, "error": "Merge conflict: ", "files_changed": 0, "commit_hash": null}

Confirming the stale base:

merge-base main spec/demo : 5a07d3c   (the fork point)
main                      : 74c6a89   (already contains round 1)

Swap echo v3 > f.txt for echo x > g.txt and the second sync succeeds — the failure is specific to re-touching an already-synced file.

What is not wrong

Worth stating, since it bounds the severity: the failure is a stall, not data loss. Sync aborts its own merge, leaves the primary checkout clean (no MERGE_HEAD/SQUASH_MSG, base ref unmoved), and exits 1, so the documented sync && cleanup --force chain correctly stops. cleanup also refuses independently while the branch holds unmerged commits.

Workaround

From inside the lane worktree, before each subsequent sync:

git merge -s ours main -m "record sync anchor"

This moves the merge base to the base branch's tip without rewriting history or changing a tracked file, so the next sync carries only the delta. It has to be repeated before every later sync, since sync never re-anchors itself.

Relation to #166 / #169

Same underlying property — a squash records no ancestry — surfacing on a different command. #166/#169 were about cleanup mis-deriving "merged" from git state after a sync; this is sync itself mis-deriving the base for its own next run. The fix accepted there (have sync record what it produced, rather than re-deriving it from git afterwards) carries over directly: a recorded last-synced commit per lane is exactly what the next sync needs to squash only the delta.

Suggested fix

  1. Re-anchor after a successful sync — record the merge (e.g. an -s ours merge of the base branch into the lane, or reset the lane branch to the new base tip) so the next sync's merge base is the tip rather than the fork point. Alternatively, detect the already-synced prefix and squash only the delta.
  2. Populate the failure message with the conflicting paths. Merge conflict: with an empty detail gives the user nothing to work from, and the underlying git output is already available at that point.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions