Skip to content

feat(worktree): reuse an existing worktree for the branch#2652

Draft
haacked wants to merge 2 commits into
posthog-code/worktree-remote-branch-checkoutfrom
posthog-code/worktree-reuse-existing
Draft

feat(worktree): reuse an existing worktree for the branch#2652
haacked wants to merge 2 commits into
posthog-code/worktree-remote-branch-checkoutfrom
posthog-code/worktree-reuse-existing

Conversation

@haacked

@haacked haacked commented Jun 13, 2026

Copy link
Copy Markdown

Problem

Sometimes the branch you want to start a task on already has a worktree — e.g. an overnight agent spun one up, or you created it as a side task. Today, starting a worktree task on such a branch doesn't error: git worktree add fails, the code catches it, and silently creates a second, detached worktree at that branch's commit. You end up with a duplicate worktree that isn't even on the branch.

Changes

When the branch already has a (PostHog-managed) worktree checked out, we now ask instead of duplicating:

  • workspace.checkWorktreeBranch reports an existingWorktreePath when a worktree under the worktree base path is already on the branch.
  • The task-creation preflight shows a "Worktree already exists — use it?" dialog. Confirming reuses that worktree for the task (registers the association, no git worktree add); cancelling aborts before any task is created.
  • Threads a reuseExistingWorktree opt-in from the UI through the saga to the workspace server.

The existing-worktree prompt takes priority over the remote-only-branch prompt (a branch with a worktree also exists locally). Scoped to worktrees under the worktree base path, since the task↔worktree association re-derives paths from that base.

Note: Stacked on #2651 (remote-only-branch checkout) — it shares the checkWorktreeBranch preflight and confirm-dialog plumbing. Base/merge that PR first; this PR targets its branch.

How did you test this?

  • typecheck clean across all changed packages (git/workspace-server/host-router/core/ui).
  • Biome check clean on all changed files.
  • New unit test for the confirmation store (existingWorktreeConfirmStore.test.ts, 5 cases) — passing.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code from a Slack thread

haacked added 2 commits June 12, 2026 18:18
When starting a worktree task on a branch that exists only on the remote
(e.g. a contributor's PR branch not yet checked out locally), confirm with
the user and then fetch + check it out into the new worktree, instead of
failing with a "branch does not exist" error.

- Add remoteBranchExists() and WorktreeManager.createWorktreeForRemoteBranch().
- Add a workspace.checkWorktreeBranch query returning trunk/local/remote-only/missing.
- Preflight the branch in the task-creation flow; on "remote-only", show a
  confirmation dialog and pass allowRemoteBranchCheckout through to the server.

Generated-By: PostHog Code
Task-Id: 1c8ffc23-3673-4b14-b5b7-50637fca8fb2
When starting a worktree task on a branch that already has a worktree
checked out, confirm with the user and reuse that worktree for the task
instead of silently creating a second, detached worktree at the same commit.

- checkWorktreeBranch now reports an existingWorktreePath when a PostHog-managed
  worktree (under the worktree base path) is already on the branch.
- The task-creation preflight prompts on that and passes reuseExistingWorktree
  through to the server, which registers the task against the existing worktree
  rather than running `git worktree add`.

Stacked on the remote-only-branch checkout change; base that PR first.

Generated-By: PostHog Code
Task-Id: 1c8ffc23-3673-4b14-b5b7-50637fca8fb2
@github-actions

Copy link
Copy Markdown

React Doctor could not complete this scan.

No React dependency found in /tmp/react-doctor-baseline-TMHaNK/package.json. Add "react" to dependencies (or peerDependencies) and re-run.

Reviewed by React Doctor for commit 67acd3d.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (1)

  1. packages/workspace-server/src/services/workspace/workspace.ts, line 763-780 (link)

    P1 Reused worktree deleted when sharing task is removed

    cleanupWorktree is called unconditionally for any worktree-mode task deletion. Once two tasks share the same worktreePath (the reuse scenario this PR introduces), deleting either task calls deleteGitWorktree on that path, immediately breaking the other task's workspace.

    The existing guard at line 771–780 prevents cleanupRepoWorktreeFolder for folders with other active workspaces, but cleanupWorktree runs before that guard and has no equivalent check. A fix would skip cleanupWorktree when getAllTaskAssociations() reveals another active worktree task pointing to the same resolved path.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/workspace-server/src/services/workspace/workspace.ts
    Line: 763-780
    
    Comment:
    **Reused worktree deleted when sharing task is removed**
    
    `cleanupWorktree` is called unconditionally for any worktree-mode task deletion. Once two tasks share the same `worktreePath` (the reuse scenario this PR introduces), deleting *either* task calls `deleteGitWorktree` on that path, immediately breaking the other task's workspace.
    
    The existing guard at line 771–780 prevents `cleanupRepoWorktreeFolder` for folders with other active workspaces, but `cleanupWorktree` runs before that guard and has no equivalent check. A fix would skip `cleanupWorktree` when `getAllTaskAssociations()` reveals another active worktree task pointing to the same resolved path.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/workspace-server/src/services/workspace/workspace.ts:763-780
**Reused worktree deleted when sharing task is removed**

`cleanupWorktree` is called unconditionally for any worktree-mode task deletion. Once two tasks share the same `worktreePath` (the reuse scenario this PR introduces), deleting *either* task calls `deleteGitWorktree` on that path, immediately breaking the other task's workspace.

The existing guard at line 771–780 prevents `cleanupRepoWorktreeFolder` for folders with other active workspaces, but `cleanupWorktree` runs before that guard and has no equivalent check. A fix would skip `cleanupWorktree` when `getAllTaskAssociations()` reveals another active worktree task pointing to the same resolved path.

### Issue 2 of 2
packages/workspace-server/src/services/workspace/workspace.ts:479-485
**Wrong `worktreeName` for legacy-layout worktrees**

`path.basename(path.dirname(match.worktreePath))` extracts the correct name only for the *new* layout `<base>/<name>/<repo>`. For *legacy* layout `<base>/<repo>/<name>` (still handled by `deriveWorktreePath`) this expression yields the repository name, not the worktree name.

That incorrect name is stored via `worktreeRepo.create({ name: worktree.worktreeName })` and is later fed back into `deriveWorktreePath(folderPath, assoc.worktree)`, which would reconstruct the path as `<base>/<repoName>/<repoName>` — a path that does not exist — causing every subsequent `verifyWorkspaceExists`, `getWorkspace`, and `getWorkspaceEnv` call to fail for that task.

A reliable extraction: if the final component of `match.worktreePath` equals `path.basename(mainRepoPath)` it's the new layout and the name is `path.basename(path.dirname(…))`; otherwise it's the legacy layout and the name is `path.basename(match.worktreePath)`.

Reviews (1): Last reviewed commit: "feat(worktree): reuse an existing worktr..." | Re-trigger Greptile

Comment on lines +479 to +485
return {
worktreePath: match.worktreePath,
worktreeName: path.basename(path.dirname(match.worktreePath)),
branchName: branch,
baseBranch: branch,
createdAt: new Date().toISOString(),
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Wrong worktreeName for legacy-layout worktrees

path.basename(path.dirname(match.worktreePath)) extracts the correct name only for the new layout <base>/<name>/<repo>. For legacy layout <base>/<repo>/<name> (still handled by deriveWorktreePath) this expression yields the repository name, not the worktree name.

That incorrect name is stored via worktreeRepo.create({ name: worktree.worktreeName }) and is later fed back into deriveWorktreePath(folderPath, assoc.worktree), which would reconstruct the path as <base>/<repoName>/<repoName> — a path that does not exist — causing every subsequent verifyWorkspaceExists, getWorkspace, and getWorkspaceEnv call to fail for that task.

A reliable extraction: if the final component of match.worktreePath equals path.basename(mainRepoPath) it's the new layout and the name is path.basename(path.dirname(…)); otherwise it's the legacy layout and the name is path.basename(match.worktreePath).

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/workspace-server/src/services/workspace/workspace.ts
Line: 479-485

Comment:
**Wrong `worktreeName` for legacy-layout worktrees**

`path.basename(path.dirname(match.worktreePath))` extracts the correct name only for the *new* layout `<base>/<name>/<repo>`. For *legacy* layout `<base>/<repo>/<name>` (still handled by `deriveWorktreePath`) this expression yields the repository name, not the worktree name.

That incorrect name is stored via `worktreeRepo.create({ name: worktree.worktreeName })` and is later fed back into `deriveWorktreePath(folderPath, assoc.worktree)`, which would reconstruct the path as `<base>/<repoName>/<repoName>` — a path that does not exist — causing every subsequent `verifyWorkspaceExists`, `getWorkspace`, and `getWorkspaceEnv` call to fail for that task.

A reliable extraction: if the final component of `match.worktreePath` equals `path.basename(mainRepoPath)` it's the new layout and the name is `path.basename(path.dirname(…))`; otherwise it's the legacy layout and the name is `path.basename(match.worktreePath)`.

How can I resolve this? If you propose a fix, please make it concise.

@haacked haacked force-pushed the posthog-code/worktree-remote-branch-checkout branch from 0f66f43 to 7bfb8e9 Compare June 15, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant