feat(mcp): let agents change thread workspaces#8680

closed19
agents/mcp-workspaces/inventory
agents/mcp-workspaces/checkout
openedAug 29, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 1d ago
26 unresolved threads Β· Jump to file
Selected thread: apps/server/src/mcp/WorktreeMcpService.ts:1284 β€” <!-- MURMUR_IGNORE --> 🟠 **High** `mcp/WorktreeMcpService.ts:1284` `performCheckout` executes the entire guarded transition inside `Effect.uninterruptibleMask` without calling `restore`, so Git, status, inventory, and dispatch operations ignore cancellation. A stalled or abandoned request therefore keeps the per-thread and per-workspace guard entries until completion, causing subsequent calls to fail with `checkout_in_progress`. Use `restore` around the transition after acquiring the guard and registering its finalizer, as `handoff` does. βœ… Resolved in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284 <!-- macroscope-meta: {"kind":"code_review"} -->
0 / 17 viewed17 files changedLine totals unavailable

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 1d545798 Β· right 1465

Original diff
@@ -759,7 +983,851 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActualBranch = yield* readWorkspaceBranchOrNull(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActualBranch,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActualBranch !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorkspaceBindingInventory(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (
+      Option.isSome(currentInventory) &&
+      inventory.repositoryCommonDir !== currentInventory.value.repositoryCommonDir
+    ) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = Option.isSome(currentInventory)
+      ? currentInventory.value.currentWorktreeRoot
+      : null;
+    if (projectWorktreeRoot === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project checkout.",
+      );
+    }
+    if (Option.isSome(currentInventory) && currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        Option.isNone(currentInventory)
+          ? Effect.succeed({
+              isRepo: false,
+              refName: null,
+              hasWorkingTreeChanges: false,
+            })
+          : readWorkspaceStatus(recordedWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        if (requestedBranch !== undefined) {
+          selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+          if (createBranch && localRefByName.has(requestedBranch)) {
+            return yield* failure(
+              "invalid_request",
+              `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+            );
+          }
+          if (!createBranch && selectedRef === undefined) {
+            return yield* failure(
+              "invalid_request",
+              `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the project root.`,
+            );
+          }
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? (currentWorkspacePath ?? recordedWorkspacePath)
+              : (selectedWorktreePath ??
+                (currentWorkspacePath === null ||
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true)
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      Effect.gen(function* () {
+        const recordedPath = yield* threadWorkspacePath(thread, projectWorktreeRoot);
+        const recordedInventory = yield* loadWorkspaceBindingInventory(recordedPath);
+        const workspacePath = Option.match(recordedInventory, {
+          onNone: () => recordedPath,
+          onSome: (candidateInventory) =>
+            candidateInventory.repositoryCommonDir === inventory.repositoryCommonDir &&
+            candidateInventory.currentWorktreeRoot !== null
+              ? candidateInventory.currentWorktreeRoot
+              : recordedPath,
+        });
+        return [thread, workspacePath] as const;
+      }),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = `workspace:${inventory.repositoryCommonDir}:${targetWorkspacePath}`;
+    return yield* Effect.uninterruptibleMask(() =>
+      Effect.suspend(() => {
+        if (workspaceTransitionsInFlight.has(workspaceGuardKey)) {
+          return Effect.fail(
+            failure(
+              "checkout_in_progress",
+              `Another workspace transition is already in progress for '${targetWorkspacePath}'.`,
+            ),
+          );
+        }
+        workspaceTransitionsInFlight.add(workspaceGuardKey);
+        return Effect.gen(function* () {
+          const [latestProjection, latestInventory, latestBindings, latestTargetBefore] =
+            yield* Effect.all(
+              [
+                loadThread(scope),
+                loadWorktrees(projectWorkspaceRoot),
+                loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                readWorkspaceStatus(targetWorkspacePath),
+              ],
+              { concurrency: 4 },
+            );
+          if (
+            latestProjection.thread.branch !== projection.thread.branch ||
+            latestProjection.thread.worktreePath !== projection.thread.worktreePath ||
+            latestProjection.thread.archivedAt !== projection.thread.archivedAt
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Thread '${scope.threadId}' changed workspace state while checkout was being prepared. Retry from its current binding.`,
+            );
+          }
+          if (
+            latestInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+            !latestInventory.worktrees.some((worktree) => worktree.path === targetWorkspacePath)
+          ) {
+            return yield* failure(
+              "scope_mismatch",
+              `Checkout target '${targetWorkspacePath}' is no longer registered in the calling thread's Git repository.`,
+            );
+          }
+          if (
+            latestTargetBefore.refName !== targetBefore.refName ||
+            latestTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Checkout '${targetWorkspacePath}' changed while the transition was being prepared. Retry from its current Git state.`,
+            );
+          }
+          if (shouldMutateCheckout && latestTargetBefore.hasWorkingTreeChanges) {
+            return yield* failure(
+              "dirty_workspace",
+              `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+            );
+          }
+          const latestOtherBindings = latestBindings
+            .filter(
+              ([thread, workspacePath]) =>
+                thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+            )
+            .map(([thread]) => thread);
+          const latestActiveBinding = latestOtherBindings.find(
+            (thread) => thread.activeRunId !== null,
+          );
+          if (
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestActiveBinding
+          ) {
+            return yield* failure(
+              "workspace_in_use",
+              `Checkout '${targetWorkspacePath}' is in use by active thread '${latestActiveBinding.id}' (${latestActiveBinding.title}).`,
+            );
+          }
+          if (
+            targetWorkspacePath !== projectWorktreeRoot &&
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `Worktree '${targetWorkspacePath}' is already bound to thread '${latestOtherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+            );
+          }
+          if (
+            targetWorkspacePath === projectWorktreeRoot &&
+            shouldMutateCheckout &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `The project root is also bound to thread '${latestOtherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+            );
+          }
+
+          let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+            targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+          let resolvedBranch: string | null = targetBefore.refName;
+          const targetBeforeCommit = shouldMutateCheckout
+            ? yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to record the checkout's current commit"))
+            : null;
+          const requestedTransitionCommit =
+            shouldMutateCheckout && requestedBranch !== undefined
+              ? createBranch
+                ? targetBeforeCommit
+                : yield* gitWorkflow
+                    .resolveCommit({ cwd: targetWorkspacePath, revision: requestedBranch })
+                    .pipe(asOperationFailed(`Unable to resolve ref '${requestedBranch}'`))
+              : null;
+          let ownedCheckoutState: {
+            readonly refName: string | null;
+            readonly hasWorkingTreeChanges: boolean;
+            readonly commitSha: string;
+          } | null = null;
+
+          const captureCheckoutState = Effect.fn("WorktreeMcpService.captureCheckoutState")(
+            function* () {
+              const status = yield* readWorkspaceStatus(targetWorkspacePath);
+              const commit = yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to identify the checkout's current commit"));
+              return {
+                refName: status.refName,
+                hasWorkingTreeChanges: status.hasWorkingTreeChanges,
+                commitSha: commit.commitSha,
+              };
+            },
+          );
+
+          const rollbackOwnedCheckout = Effect.fn("WorktreeMcpService.rollbackOwnedCheckout")(
+            function* () {
+              if (!shouldMutateCheckout) {
+                return "not_needed" as const;
+              }
+              if (ownedCheckoutState === null || targetBeforeCommit === null) {
+                return "not_possible" as const;
+              }
+              const [latestStateExit, latestBindings, callerProjectionExit] = yield* Effect.all(
+                [
+                  Effect.exit(captureCheckoutState()),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  Effect.exit(threadManagement.getThreadProjection(scope.threadId)),
+                ],
+                { concurrency: 3 },
+              );
+              const anotherOwner = latestBindings.some(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              );
+              const callerStillInitial =
+                Exit.isSuccess(callerProjectionExit) &&
+                callerProjectionExit.value.thread.branch === projection.thread.branch &&
+                callerProjectionExit.value.thread.worktreePath === projection.thread.worktreePath &&
+                callerProjectionExit.value.thread.archivedAt === projection.thread.archivedAt &&
+                callerProjectionExit.value.thread.deletedAt === projection.thread.deletedAt;
+              if (
+                Exit.isFailure(latestStateExit) ||
+                latestStateExit.value.refName !== ownedCheckoutState.refName ||
+                latestStateExit.value.hasWorkingTreeChanges ||
+                latestStateExit.value.hasWorkingTreeChanges !==
+                  ownedCheckoutState.hasWorkingTreeChanges ||
+                latestStateExit.value.commitSha !== ownedCheckoutState.commitSha ||
+                anotherOwner ||
+                !callerStillInitial
+              ) {
+                return "not_possible" as const;
+              }
+              const checkoutChanged =
+                latestStateExit.value.refName !== targetBefore.refName ||
+                latestStateExit.value.commitSha !== targetBeforeCommit.commitSha;
+              if (checkoutChanged && targetBefore.refName === null) {
+                return "not_possible" as const;
+              }
+              const rollbackExit = yield* Effect.exit(
+                checkoutChanged
+                  ? gitWorkflow.switchRef({
+                      cwd: targetWorkspacePath,
+                      refName: targetBefore.refName!,
+                    })
+                  : Effect.void,
C
cursorBotAug 30, 2026

Rollback ignores moved original branch

Medium Severity

rollbackOwnedCheckout attests the post-switch checkout, then switchRefs back to targetBefore.refName without checking that this ref still points at targetBeforeCommit. If that branch moved during the attempt, rollback lands on a different commit than the workspace started from, which breaks the rule that unattested Git state is left untouched.

Reviewed by Cursor Bugbot for commit 1d545798de018346d5a02146a79727b602844f6d. Configure here.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 8aafc416 Β· right 1055

Original diff
@@ -707,7 +894,770 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActual.refName,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActual.refName !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorktrees(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (inventory.repositoryCommonDir !== currentInventory.repositoryCommonDir) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = currentInventory.currentWorktreeRoot;
+    if (projectWorktreeRoot === null || currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project or thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        readWorkspaceStatus(currentWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? currentWorkspacePath
+              : (selectedWorktreePath ??
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
C
cursorBotAug 29, 2026

Remote default checkout targets project root

High Severity

Auto workspace resolution sends an attached thread that requests the remote default ref (for example origin/main) to projectWorktreeRoot and then mutates that checkout. Remote refs always have a null worktreePath, and origin's HEAD is marked isDefault, so the local-default heuristic misfires. A non-default remote such as origin/feature correctly stays on the current worktree. The result is an unexpected branch switch or detach on the shared project root instead of checking out the requested remote ref in the thread's current workspace.

Reviewed by Cursor Bugbot for commit 8aafc41636ec84adab80a84e289c7cefb0757f1b. Configure here.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision a671e359 Β· right 982

Original diff
@@ -463,21 +631,654 @@ const make = Effect.gen(function* () {
       yield* requireCapability(scope);
       const projection = yield* loadThread(scope);
       const project = yield* loadProject(scope, projection.thread.projectId);
-
-      const defaultStartFromOrigin = yield* readDefaultStartFromOrigin;
+      const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+      const workspacePath = normalizePath(projection.thread.worktreePath ?? projectWorkspaceRoot);
+      const [defaultStartFromOrigin, actual, refs] = yield* Effect.all(
+        [
+          readDefaultStartFromOrigin,
+          readWorkspaceStatus(workspacePath),
+          loadRefs(projectWorkspaceRoot, "local"),
+        ],
+        { concurrency: 3 },
+      );
+      const knownWorkspacePaths = new Set([
+        projectWorkspaceRoot,
+        ...refs.flatMap((ref) =>
+          ref.isRemote === true || ref.worktreePath === null
+            ? []
+            : [normalizePath(ref.worktreePath)],
+        ),
+      ]);
+      const agreement = !knownWorkspacePaths.has(workspacePath)
+        ? "workspace_missing"
+        : !actual.isRepo
+          ? "not_repository"
+          : actual.refName !== projection.thread.branch
+            ? "branch_mismatch"
+            : "in_sync";
 
       const result: WorktreeMcpStatusResult = {
         attached: projection.thread.worktreePath !== null,
         worktreePath: projection.thread.worktreePath,
         branch: projection.thread.branch,
-        projectWorkspaceRoot: project.workspaceRoot,
+        projectWorkspaceRoot,
         defaultStartFromOrigin,
+        recordedWorkspace: {
+          branch: projection.thread.branch,
+          worktreePath: projection.thread.worktreePath,
+        },
+        actualWorkspace: {
+          workspacePath,
+          isRepo: actual.isRepo,
+          branch: actual.refName,
+          hasWorkingTreeChanges: actual.hasWorkingTreeChanges,
+        },
+        agreement,
       };
       return result;
     },
   );
 
-  return WorktreeMcpService.of({ handoff, status });
+  const listWorktrees: WorktreeMcpService["Service"]["listWorktrees"] = Effect.fn(
+    "WorktreeMcpService.listWorktrees",
+  )(function* (scope) {
+    yield* requireCapability(scope);
+    const projection = yield* loadThread(scope);
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+    const [refs, threads] = yield* Effect.all(
+      [loadRefs(projectWorkspaceRoot, "local"), loadProjectThreads(projection.thread.projectId)],
+      { concurrency: 2 },
+    );
+
+    const branchByWorkspacePath = new Map<string, string | null>([[projectWorkspaceRoot, null]]);
+    for (const ref of refs) {
+      if (ref.isRemote === true || ref.worktreePath === null) continue;
+      branchByWorkspacePath.set(normalizePath(ref.worktreePath), ref.name);
+    }
+
+    const worktrees = yield* Effect.forEach(
+      [...branchByWorkspacePath.entries()],
+      ([workspacePath, branch]) =>
+        readWorkspaceStatus(workspacePath).pipe(
+          Effect.map((actual) => ({
+            path: workspacePath,
+            branch,
+            actualBranch: actual.refName,
+            isRepo: actual.isRepo,
+            isProjectRoot: workspacePath === projectWorkspaceRoot,
+            hasWorkingTreeChanges: actual.hasWorkingTreeChanges,
+            bindings: threads
+              .filter(
+                (thread) => threadWorkspacePath(thread, projectWorkspaceRoot) === workspacePath,
+              )
+              .map((thread) => ({
+                threadId: thread.id,
+                title: thread.title,
+                status: thread.status,
+                recordedBranch: thread.branch,
+                recordedWorktreePath: thread.worktreePath,
+                active: thread.activeRunId !== null,
+                callingThread: thread.id === scope.threadId,
+              })),
+          })),
+        ),
+      { concurrency: 8 },
+    );
+
+    return {
+      projectWorkspaceRoot,
+      worktrees: worktrees.toSorted(
+        (left, right) =>
+          Number(right.isProjectRoot) - Number(left.isProjectRoot) ||
+          left.path.localeCompare(right.path),
+      ),
+    } satisfies WorktreeMcpListResult;
+  });
+
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+    const currentWorkspacePath = normalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(currentWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      const actual = yield* readWorkspaceStatus(handoff.worktreePath);
+      return {
+        previous: {
+          workspacePath: currentWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActual.refName,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: actual.refName,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActual.refName !== actual.refName,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        readWorkspaceStatus(currentWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set([
+      projectWorkspaceRoot,
+      ...localRefs.flatMap((ref) =>
+        ref.worktreePath === null ? [] : [normalizePath(ref.worktreePath)],
+      ),
+    ]);
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        targetWorkspacePath = normalizePath(input.target.path);
+        if (
+          targetWorkspacePath === projectWorkspaceRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorkspaceRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorkspaceRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : normalizePath(selectedRef.worktreePath);
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorkspaceRoot
+            : workspace === "current"
+              ? currentWorkspacePath
+              : (selectedWorktreePath ??
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true
+                  ? projectWorkspaceRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : normalizePath(selectedRef.worktreePath);
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch ||
+        (selectedRef?.isRemote === true
+          ? targetBefore.refName !== requestedBranch &&
+            targetBefore.refName !== requestedBranch.replace(/^[^/]+\//, "")
+          : targetBefore.refName !== requestedBranch));
+    const otherBindings = threads.filter(
+      (thread) =>
+        thread.id !== scope.threadId &&
+        threadWorkspacePath(thread, projectWorkspaceRoot) === targetWorkspacePath,
+    );
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorkspaceRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorkspaceRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    return yield* Effect.uninterruptibleMask(() =>
+      Effect.gen(function* () {
+        let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+          targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+        let createdBranch: string | null = null;
+
+        if (shouldMutateCheckout && requestedBranch !== undefined) {
M
macroscopeappBotAug 29, 2026

🟠 High mcp/WorktreeMcpService.ts:982

checkout can let transitions from different threads concurrently mutate the same project root or worktree, leaving the earlier thread's durable branch binding inconsistent with the branch Git leaves checked out and allowing two threads to share one mutable workspace. The per-thread workspaceTransitionsInFlight guard does not prevent this, and each dispatch validates only that thread's prior binding, so both optimistic updates can commit. Add workspace-level serialization or recheck workspace ownership after the Git mutation and before dispatch.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 982:
2
3`checkout` can let transitions from different threads concurrently mutate the same project root or worktree, leaving the earlier thread's durable branch binding inconsistent with the branch Git leaves checked out and allowing two threads to share one mutable workspace. The per-thread `workspaceTransitionsInFlight` guard does not prevent this, and each dispatch validates only that thread's prior binding, so both optimistic updates can commit. Add workspace-level serialization or recheck workspace ownership after the Git mutation and before dispatch.
J
Julius MarmingeAug 29, 2026

Fixed in 98151e0c2e. Checkout now acquires an in-process guard for the canonical target workspace in addition to the per-thread guard, then rechecks Git inventory, status, and thread ownership while holding it. Added a two-caller race test targeting the same worktree.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 9335400d Β· right 1433

Original diff
@@ -725,7 +926,819 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActual.refName,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActual.refName !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorktrees(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (inventory.repositoryCommonDir !== currentInventory.repositoryCommonDir) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = currentInventory.currentWorktreeRoot;
+    if (projectWorktreeRoot === null || currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project or thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        readWorkspaceStatus(currentWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? currentWorkspacePath
+              : (selectedWorktreePath ??
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      Effect.gen(function* () {
+        const recordedPath = yield* threadWorkspacePath(thread, projectWorktreeRoot);
+        const recordedInventory = yield* loadWorkspaceBindingInventory(recordedPath);
+        const workspacePath = Option.match(recordedInventory, {
+          onNone: () => recordedPath,
+          onSome: (candidateInventory) =>
+            candidateInventory.repositoryCommonDir === inventory.repositoryCommonDir &&
+            candidateInventory.currentWorktreeRoot !== null
+              ? candidateInventory.currentWorktreeRoot
+              : recordedPath,
+        });
+        return [thread, workspacePath] as const;
+      }),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = `workspace:${inventory.repositoryCommonDir}:${targetWorkspacePath}`;
+    return yield* Effect.uninterruptibleMask(() =>
+      Effect.suspend(() => {
+        if (workspaceTransitionsInFlight.has(workspaceGuardKey)) {
+          return Effect.fail(
+            failure(
+              "checkout_in_progress",
+              `Another workspace transition is already in progress for '${targetWorkspacePath}'.`,
+            ),
+          );
+        }
+        workspaceTransitionsInFlight.add(workspaceGuardKey);
+        return Effect.gen(function* () {
+          const [latestProjection, latestInventory, latestBindings, latestTargetBefore] =
+            yield* Effect.all(
+              [
+                loadThread(scope),
+                loadWorktrees(projectWorkspaceRoot),
+                loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                readWorkspaceStatus(targetWorkspacePath),
+              ],
+              { concurrency: 4 },
+            );
+          if (
+            latestProjection.thread.branch !== projection.thread.branch ||
+            latestProjection.thread.worktreePath !== projection.thread.worktreePath ||
+            latestProjection.thread.archivedAt !== projection.thread.archivedAt
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Thread '${scope.threadId}' changed workspace state while checkout was being prepared. Retry from its current binding.`,
+            );
+          }
+          if (
+            latestInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+            !latestInventory.worktrees.some((worktree) => worktree.path === targetWorkspacePath)
+          ) {
+            return yield* failure(
+              "scope_mismatch",
+              `Checkout target '${targetWorkspacePath}' is no longer registered in the calling thread's Git repository.`,
+            );
+          }
+          if (
+            latestTargetBefore.refName !== targetBefore.refName ||
+            latestTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Checkout '${targetWorkspacePath}' changed while the transition was being prepared. Retry from its current Git state.`,
+            );
+          }
+          if (shouldMutateCheckout && latestTargetBefore.hasWorkingTreeChanges) {
+            return yield* failure(
+              "dirty_workspace",
+              `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+            );
+          }
+          const latestOtherBindings = latestBindings
+            .filter(
+              ([thread, workspacePath]) =>
+                thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+            )
+            .map(([thread]) => thread);
+          const latestActiveBinding = latestOtherBindings.find(
+            (thread) => thread.activeRunId !== null,
+          );
+          if (
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestActiveBinding
+          ) {
+            return yield* failure(
+              "workspace_in_use",
+              `Checkout '${targetWorkspacePath}' is in use by active thread '${latestActiveBinding.id}' (${latestActiveBinding.title}).`,
+            );
+          }
+          if (
+            targetWorkspacePath !== projectWorktreeRoot &&
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `Worktree '${targetWorkspacePath}' is already bound to thread '${latestOtherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+            );
+          }
+          if (
+            targetWorkspacePath === projectWorktreeRoot &&
+            shouldMutateCheckout &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `The project root is also bound to thread '${latestOtherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+            );
+          }
+
+          let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+            targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+          let resolvedBranch: string | null = targetBefore.refName;
+          const targetBeforeCommit = shouldMutateCheckout
+            ? yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to record the checkout's current commit"))
+            : null;
+          const requestedTransitionCommit =
+            shouldMutateCheckout && requestedBranch !== undefined
+              ? createBranch
+                ? targetBeforeCommit
+                : yield* gitWorkflow
+                    .resolveCommit({ cwd: targetWorkspacePath, revision: requestedBranch })
+                    .pipe(asOperationFailed(`Unable to resolve ref '${requestedBranch}'`))
+              : null;
+          let ownedCheckoutState: {
+            readonly refName: string | null;
+            readonly hasWorkingTreeChanges: boolean;
+            readonly commitSha: string;
+          } | null = null;
+
+          const captureCheckoutState = Effect.fn("WorktreeMcpService.captureCheckoutState")(
+            function* () {
+              const status = yield* readWorkspaceStatus(targetWorkspacePath);
+              const commit = yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to identify the checkout's current commit"));
+              return {
+                refName: status.refName,
+                hasWorkingTreeChanges: status.hasWorkingTreeChanges,
+                commitSha: commit.commitSha,
+              };
+            },
+          );
+
+          const rollbackOwnedCheckout = Effect.fn("WorktreeMcpService.rollbackOwnedCheckout")(
+            function* () {
+              if (!shouldMutateCheckout) {
+                return "not_needed" as const;
+              }
+              if (ownedCheckoutState === null || targetBeforeCommit === null) {
+                return "not_possible" as const;
+              }
+              const [latestStateExit, latestBindings, callerProjectionExit] = yield* Effect.all(
+                [
+                  Effect.exit(captureCheckoutState()),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  Effect.exit(threadManagement.getThreadProjection(scope.threadId)),
+                ],
+                { concurrency: 3 },
+              );
+              const anotherOwner = latestBindings.some(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              );
+              const callerStillInitial =
+                Exit.isSuccess(callerProjectionExit) &&
+                callerProjectionExit.value.thread.branch === projection.thread.branch &&
+                callerProjectionExit.value.thread.worktreePath === projection.thread.worktreePath &&
+                callerProjectionExit.value.thread.archivedAt === projection.thread.archivedAt &&
+                callerProjectionExit.value.thread.deletedAt === projection.thread.deletedAt;
+              if (
+                Exit.isFailure(latestStateExit) ||
+                latestStateExit.value.refName !== ownedCheckoutState.refName ||
+                latestStateExit.value.hasWorkingTreeChanges ||
+                latestStateExit.value.hasWorkingTreeChanges !==
+                  ownedCheckoutState.hasWorkingTreeChanges ||
+                latestStateExit.value.commitSha !== ownedCheckoutState.commitSha ||
+                anotherOwner ||
+                !callerStillInitial
+              ) {
+                return "not_possible" as const;
+              }
+              const checkoutChanged =
+                latestStateExit.value.refName !== targetBefore.refName ||
+                latestStateExit.value.commitSha !== targetBeforeCommit.commitSha;
+              if (checkoutChanged && targetBefore.refName === null) {
+                return "not_possible" as const;
+              }
+              const rollbackExit = yield* Effect.exit(
+                checkoutChanged
+                  ? gitWorkflow.switchRef({
+                      cwd: targetWorkspacePath,
+                      refName: targetBefore.refName!,
+                    })
+                  : Effect.void,
+              );
+              return Exit.isSuccess(rollbackExit) ? ("rolled_back" as const) : ("failed" as const);
+            },
+          );
+
+          if (shouldMutateCheckout && requestedBranch !== undefined) {
+            const [mutationProjection, mutationBindings, mutationTargetBefore] = yield* Effect.all(
+              [
+                loadThread(scope),
+                loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                readWorkspaceStatus(targetWorkspacePath),
+              ],
+              { concurrency: 3 },
+            );
+            if (
+              mutationProjection.thread.branch !== projection.thread.branch ||
+              mutationProjection.thread.worktreePath !== projection.thread.worktreePath ||
+              mutationProjection.thread.archivedAt !== projection.thread.archivedAt ||
+              mutationProjection.thread.deletedAt !== projection.thread.deletedAt ||
+              mutationTargetBefore.refName !== targetBefore.refName ||
+              mutationTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+            ) {
+              return yield* failure(
+                "checkout_in_progress",
+                `Thread or checkout state changed immediately before Git mutation. Retry from the current workspace state.`,
+              );
+            }
+            const mutationOtherBindings = mutationBindings
+              .filter(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              )
+              .map(([thread]) => thread);
+            const mutationActiveBinding = mutationOtherBindings.find(
+              (thread) => thread.activeRunId !== null,
+            );
+            if (mutationActiveBinding !== undefined) {
+              return yield* failure(
+                "workspace_in_use",
+                `Checkout '${targetWorkspacePath}' became active for thread '${mutationActiveBinding.id}' (${mutationActiveBinding.title}) before Git mutation.`,
+              );
+            }
+            if (mutationOtherBindings.length > 0) {
+              return yield* failure(
+                "workspace_shared",
+                `Checkout '${targetWorkspacePath}' became bound to thread '${mutationOtherBindings[0]!.id}' before Git mutation.`,
+              );
+            }
+            if (createBranch) {
+              yield* gitWorkflow
+                .createRef({
+                  cwd: targetWorkspacePath,
+                  refName: requestedBranch,
+                  switchRef: false,
+                })
+                .pipe(asOperationFailed(`Unable to create branch '${requestedBranch}'`));
+            }
+            const switchExit = yield* Effect.exit(
M
macroscopeappBotAug 29, 2026

🟑 Medium mcp/WorktreeMcpService.ts:1433

When target.create successfully creates the branch but switchRef fails, the checkout returns operation_failed while leaving the newly created local branch behind. rollbackOwnedCheckout only restores checkout state and never calls deleteLocalBranch, so retries with the same branch are rejected as already existing. Clean up the created branch on this failure path when it is safe to do so.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 1433:
2
3When `target.create` successfully creates the branch but `switchRef` fails, the checkout returns `operation_failed` while leaving the newly created local branch behind. `rollbackOwnedCheckout` only restores checkout state and never calls `deleteLocalBranch`, so retries with the same branch are rejected as already existing. Clean up the created branch on this failure path when it is safe to do so.
J
Julius MarmingeAug 29, 2026

Intentional conservative behavior in 05f8e6450d, matching the parent review direction. A failed switch does not establish safe ownership of the created ref: it may have become attached or advanced elsewhere, so deleting it to make the original create request retryable risks data loss. The result preserves the branch and reports the failure; a retry can select/reuse the retained branch after inspecting current state. The focused test verifies no delete is attempted.

apps/server/src/mcp/WorktreeMcpService.test.ts Β· Earlier revision 875fd5e4 Β· right 2658–2658

Original diff
@@ -1756,6 +2019,1512 @@ describe("t3_worktree_list", () => {
   });
 });
 
+describe("t3_thread_checkout", () => {
+  const rootRefs = [
+    {
+      name: "dev",
+      current: true,
+      isDefault: true,
+      worktreePath: workspaceRoot,
+    },
+    {
+      name: "feature/checkout",
+      current: false,
+      isDefault: false,
+      worktreePath: null,
+    },
+  ] as const;
+
+  it.effect("switches the actual branch before updating the durable binding", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "branch", branch: "feature/checkout" },
+      });
+      expect(result.checkoutAction).toBe("switched");
+      expect(result.current).toMatchObject({
+        workspacePath: workspaceRoot,
+        recordedBranch: "feature/checkout",
+        actualBranch: "feature/checkout",
+      });
+      expect(harness.switchRef).toHaveBeenCalledWith({
+        cwd: workspaceRoot,
+        refName: "feature/checkout",
+      });
+      expect(harness.dispatch).toHaveBeenCalledWith(
+        expect.objectContaining({
+          type: "thread.metadata.update",
+          branch: "feature/checkout",
+          worktreePath: null,
+          expectedBranch: "dev",
+          expectedWorktreePath: null,
+        }),
+      );
+      expect(harness.switchRef.mock.invocationCallOrder[0]).toBeLessThan(
+        harness.dispatch.mock.invocationCallOrder[0]!,
+      );
+    });
+  });
+
+  it.effect(
+    "asks Git to resolve a remote ref even when a same-named local branch is current",
+    () => {
+      const harness = makeHarness({
+        thread: { branch: "feature", worktreePath: null },
+        refs: [
+          {
+            name: "feature",
+            current: true,
+            isDefault: false,
+            worktreePath: workspaceRoot,
+          },
+          {
+            name: "origin/feature",
+            current: false,
+            isDefault: false,
+            isRemote: true,
+            worktreePath: null,
+          },
+        ],
+        workspaceStatuses: { [workspaceRoot]: { branch: "feature" } },
+        switchRefResultBranch: "feature",
+      });
+      return Effect.gen(function* () {
+        const result = yield* runCheckout(harness, {
+          target: { type: "branch", branch: "origin/feature" },
+        });
+        expect(result.checkoutAction).toBe("switched");
+        expect(result.current.actualBranch).toBe("feature");
+        expect(harness.switchRef).toHaveBeenCalledWith({
+          cwd: workspaceRoot,
+          refName: "origin/feature",
+        });
+      });
+    },
+  );
+
+  it.effect("records a verified detached checkout of an explicit remote ref", () => {
+    const remoteCommit = "remote-feature-commit";
+    const harness = makeHarness({
+      thread: { branch: "feature", worktreePath: null },
+      refs: [
+        {
+          name: "feature",
+          current: true,
+          isDefault: false,
+          worktreePath: workspaceRoot,
+        },
+        {
+          name: "origin/feature",
+          current: false,
+          isDefault: false,
+          isRemote: true,
+          worktreePath: null,
+        },
+      ],
+      workspaceStatuses: { [workspaceRoot]: { branch: "feature" } },
+      switchRefResultBranch: null,
+      resolvedCommits: ["local-feature-commit", remoteCommit, remoteCommit],
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "branch", branch: "origin/feature" },
+      });
+
+      expect(result.checkoutAction).toBe("switched");
+      expect(result.current).toMatchObject({ recordedBranch: null, actualBranch: null });
+      expect(harness.dispatch).toHaveBeenCalledWith(
+        expect.objectContaining({
+          type: "thread.metadata.update",
+          branch: null,
+          expectedArchived: false,
+        }),
+      );
+    });
+  });
+
+  it.effect("refuses to bind when Git changes again after resolving the requested ref", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+      switchRefResultBranch: "feature/checkout",
+      refChangeAfterSwitch: "feature/intervening",
+    });
+    return Effect.gen(function* () {
+      const exit = yield* Effect.exit(
+        runCheckout(harness, {
+          target: { type: "branch", branch: "feature/checkout" },
+        }),
+      );
+      expectTypedFailure(exit, {
+        _tag: "WorktreeMcpFailure",
+        code: "partial_failure",
+        partial: {
+          actualBranch: "feature/intervening",
+          rollback: "not_possible",
+        },
+      });
+      expect(harness.dispatch).not.toHaveBeenCalled();
+      expect(harness.switchRef).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  it.effect("rechecks the caller binding after commit resolution and before Git mutation", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+      threadAttachedOnCall: 3,
+    });
+    return Effect.gen(function* () {
+      const exit = yield* Effect.exit(
+        runCheckout(harness, {
+          target: { type: "branch", branch: "feature/checkout" },
+        }),
+      );
+
+      expectTypedFailure(exit, {
+        _tag: "WorktreeMcpFailure",
+        code: "checkout_in_progress",
+      });
+      expect(harness.resolveCommit).toHaveBeenCalledTimes(2);
+      expect(harness.switchRef).not.toHaveBeenCalled();
+      expect(harness.dispatch).not.toHaveBeenCalled();
+    });
+  });
+
+  for (const [state, option] of [
+    ["archived", { threadArchivedOnCall: 4 }],
+    ["deleted", { threadDeletedOnCall: 4 }],
+  ] as const) {
+    it.effect(`preserves Git state when the thread is ${state} before binding`, () => {
+      const harness = makeHarness({
+        thread: { branch: "dev", worktreePath: null },
+        refs: rootRefs,
+        workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+        ...option,
+      });
+      return Effect.gen(function* () {
+        const exit = yield* Effect.exit(
+          runCheckout(harness, {
+            target: { type: "branch", branch: "feature/checkout" },
+          }),
+        );
+        expectTypedFailure(exit, {
+          _tag: "WorktreeMcpFailure",
+          code: "partial_failure",
+          partial: { actualBranch: "feature/checkout", rollback: "not_possible" },
+        });
+        expect(harness.switchRef).toHaveBeenCalledTimes(1);
+        expect(harness.dispatch).not.toHaveBeenCalled();
+        expect(harness.sendToThread).not.toHaveBeenCalled();
+      });
+    });
+  }
+
+  it.effect("creates and checks out a new branch in the current workspace", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "branch", branch: "feature/created", create: true },
+      });
+      expect(result.checkoutAction).toBe("created");
+      expect(result.current).toMatchObject({
+        recordedBranch: "feature/created",
+        actualBranch: "feature/created",
+      });
+      expect(harness.createRef).toHaveBeenCalledWith({
+        cwd: workspaceRoot,
+        refName: "feature/created",
+        switchRef: false,
+      });
+      expect(harness.switchRef).toHaveBeenCalledWith({
+        cwd: workspaceRoot,
+        refName: "feature/created",
+      });
+    });
+  });
+
+  it.effect("retains a created branch when a later binding operation fails", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+      dispatchFails: true,
+    });
+    return Effect.gen(function* () {
+      const exit = yield* Effect.exit(
+        runCheckout(harness, {
+          target: { type: "branch", branch: "feature/created-rollback", create: true },
+        }),
+      );
+
+      expectTypedFailure(exit, { _tag: "WorktreeMcpFailure", code: "operation_failed" });
+      expect(harness.switchRef).toHaveBeenNthCalledWith(2, {
+        cwd: workspaceRoot,
+        refName: "dev",
+      });
+      expect(harness.deleteLocalBranch).not.toHaveBeenCalled();
+    });
+  });
+
+  it.effect("reuses an existing worktree and queues continuation after binding", () => {
+    const worktreePath = "/worktrees/project/feature-checkout";
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: [
+        rootRefs[0],
+        {
+          name: "feature/checkout",
+          current: false,
+          isDefault: false,
+          worktreePath,
+        },
+      ],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [worktreePath]: { branch: "feature/checkout" },
+      },
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "branch", branch: "feature/checkout" },
+        continuationPrompt: "Continue in the reused worktree.",
+      });
+      expect(result.checkoutAction).toBe("reused");
+      expect(result.workspaceChanged).toBe(true);
+      expect(result.callerTurnEnds).toBe(true);
+      expect(result.continuation).toEqual({ status: "scheduled", delivery: "queued" });
+      expect(harness.switchRef).not.toHaveBeenCalled();
+      expect(harness.dispatch.mock.invocationCallOrder[0]).toBeLessThan(
+        harness.sendToThread.mock.invocationCallOrder[0]!,
+      );
+    });
+  });
+
+  it.effect("returns an attached thread to the project root", () => {
+    const worktreePath = "/worktrees/project/feature-checkout";
+    const harness = makeHarness({
+      thread: { branch: "feature/checkout", worktreePath },
+      refs: [
+        rootRefs[0],
+        {
+          name: "feature/checkout",
+          current: true,
+          isDefault: false,
+          worktreePath,
+        },
+      ],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [worktreePath]: { branch: "feature/checkout" },
+      },
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, { target: { type: "project_root" } });
+      expect(result.current).toMatchObject({
+        workspacePath: workspaceRoot,
+        recordedBranch: "dev",
+        recordedWorktreePath: null,
+        actualBranch: "dev",
+      });
+      expect(result.workspaceChanged).toBe(true);
+      expect(harness.switchRef).not.toHaveBeenCalled();
+    });
+  });
+
+  it.effect("repairs a missing saved worktree by returning to the project root", () => {
+    const missingPath = "/worktrees/project/deleted";
+    const harness = makeHarness({
+      thread: { branch: "feature/deleted", worktreePath: missingPath },
+      refs: rootRefs,
+      worktrees: [{ path: workspaceRoot, refName: "dev" }],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [missingPath]: { branch: null, isRepo: false },
+      },
+      worktreeInventoryFailsFor: new Set([missingPath]),
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, { target: { type: "project_root" } });
+
+      expect(result.previous).toMatchObject({
+        workspacePath: missingPath,
+        recordedBranch: "feature/deleted",
+        actualBranch: null,
+      });
+      expect(result.current).toMatchObject({
+        workspacePath: workspaceRoot,
+        recordedBranch: "dev",
+        recordedWorktreePath: null,
+        actualBranch: "dev",
+      });
+      expect(harness.switchRef).not.toHaveBeenCalled();
+      expect(harness.dispatch).toHaveBeenCalledWith(
+        expect.objectContaining({
+          expectedBranch: "feature/deleted",
+          expectedWorktreePath: missingPath,
+          branch: "dev",
+          worktreePath: null,
+        }),
+      );
+    });
+  });
+
+  it.effect("repairs a missing saved worktree by reusing a listed checkout", () => {
+    const missingPath = "/worktrees/project/deleted";
+    const targetPath = "/worktrees/project/existing";
+    const harness = makeHarness({
+      thread: { branch: "feature/deleted", worktreePath: missingPath },
+      refs: [
+        rootRefs[0],
+        {
+          name: "feature/existing",
+          current: true,
+          isDefault: false,
+          worktreePath: targetPath,
+        },
+      ],
+      worktrees: [
+        { path: workspaceRoot, refName: "dev" },
+        { path: targetPath, refName: "feature/existing" },
+      ],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [missingPath]: { branch: null, isRepo: false },
+        [targetPath]: { branch: "feature/existing" },
+      },
+      worktreeInventoryFailsFor: new Set([missingPath]),
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "worktree", path: targetPath },
+      });
+
+      expect(result.checkoutAction).toBe("reused");
+      expect(result.previous.workspacePath).toBe(missingPath);
+      expect(result.current).toMatchObject({
+        workspacePath: targetPath,
+        recordedBranch: "feature/existing",
+        recordedWorktreePath: targetPath,
+      });
+      expect(harness.switchRef).not.toHaveBeenCalled();
+    });
+  });
+
+  it.effect("repairs a missing saved worktree by creating from the healthy project root", () => {
+    const missingPath = "/worktrees/project/deleted";
+    const harness = makeHarness({
+      thread: { branch: "feature/deleted", worktreePath: missingPath },
+      refs: rootRefs,
+      worktrees: [{ path: workspaceRoot, refName: "dev" }],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [missingPath]: { branch: null, isRepo: false },
+      },
+      worktreeInventoryFailsFor: new Set([missingPath]),
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "new_worktree", branch: "feature/recovered" },
+      });
+
+      expect(result.previous.actualBranch).toBeNull();
+      expect(result.current.recordedWorktreePath).toBe("/worktrees/project/feature/recovered");
+      expect(harness.createWorktree).toHaveBeenCalledWith(
+        expect.objectContaining({
+          cwd: workspaceRoot,
+          refName: "dev",
+          newRefName: "feature/recovered",
+        }),
+      );
+      expect(harness.dispatch).toHaveBeenCalledWith(
+        expect.objectContaining({
+          expectedBranch: "feature/deleted",
+          expectedWorktreePath: missingPath,
+        }),
+      );
+    });
+  });
+
+  it.effect("fails closed when the recorded checkout inventory succeeds but status fails", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      worktrees: [{ path: workspaceRoot, refName: "dev" }],
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+      localStatusFailsOnCall: 1,
+    });
+    return Effect.gen(function* () {
+      const exit = yield* Effect.exit(
+        runCheckout(harness, {
+          target: { type: "branch", branch: "feature/checkout" },
+        }),
+      );
+
+      expectTypedFailure(exit, { _tag: "WorktreeMcpFailure", code: "operation_failed" });
+      expect(harness.switchRef).not.toHaveBeenCalled();
+      expect(harness.createRef).not.toHaveBeenCalled();
+      expect(harness.dispatch).not.toHaveBeenCalled();
+    });
+  });
+
+  it.effect("applies branch existence checks to project-root targets", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+    });
+    return Effect.gen(function* () {
+      const existingExit = yield* Effect.exit(
+        runCheckout(harness, {
+          target: { type: "project_root", branch: "feature/checkout", create: true },
+        }),
+      );
+      expectTypedFailure(existingExit, {
+        _tag: "WorktreeMcpFailure",
+        code: "invalid_request",
+        message:
+          "Local branch 'feature/checkout' already exists. Omit target.create to check it out.",
+      });
+
+      const missingExit = yield* Effect.exit(
+        runCheckout(harness, {
+          target: { type: "project_root", branch: "feature/missing" },
+        }),
+      );
+      expectTypedFailure(missingExit, {
+        _tag: "WorktreeMcpFailure",
+        code: "invalid_request",
+        message:
+          "Branch or remote ref 'feature/missing' does not exist. Pass target.create=true to create a local branch from the project root.",
+      });
+      expect(harness.switchRef).not.toHaveBeenCalled();
+      expect(harness.createRef).not.toHaveBeenCalled();
+    });
+  });
+
+  it.effect("creates a new worktree for an already attached thread", () => {
+    const sourcePath = "/worktrees/project/source";
+    const harness = makeHarness({
+      thread: { branch: "feature/source", worktreePath: sourcePath },
+      refs: [
+        {
+          name: "dev",
+          current: true,
+          isDefault: true,
+          worktreePath: workspaceRoot,
+        },
+        {
+          name: "feature/source",
+          current: true,
+          isDefault: false,
+          worktreePath: sourcePath,
+        },
+      ],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [sourcePath]: { branch: "feature/source" },
+      },
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "new_worktree", branch: "feature/new-checkout" },
+        continuationPrompt: "Continue in the new worktree.",
+      });
+      expect(result.checkoutAction).toBe("created");
+      expect(result.current.recordedWorktreePath).toBe("/worktrees/project/feature/new-checkout");
+      expect(result.current.actualBranch).toBe("feature/new-checkout");
+      expect(result.continuation).toEqual({ status: "scheduled", delivery: "queued" });
+      expect(harness.createWorktree).toHaveBeenCalledWith(
+        expect.objectContaining({ refName: "feature/source" }),
+      );
+      expect(harness.dispatch).toHaveBeenCalledWith(
+        expect.objectContaining({
+          expectedBranch: "feature/source",
+          expectedWorktreePath: sourcePath,
+        }),
+      );
+    });
+  });
+
+  it.effect("does not turn a completed new-worktree handoff into a status-read failure", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev" } },
+      localStatusFailsOnCall: 3,
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "new_worktree", branch: "feature/completed-handoff" },
+      });
+      expect(result.current.actualBranch).toBe("feature/completed-handoff");
+      expect(harness.localStatus).toHaveBeenCalledTimes(2);
+    });
+  });
+
+  it.effect("reuses a detached worktree without inventing a branch", () => {
+    const detachedPath = "/worktrees/project/detached";
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: [rootRefs[0]],
+      worktrees: [
+        { path: workspaceRoot, refName: "dev" },
+        { path: detachedPath, refName: null },
+      ],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [detachedPath]: { branch: null },
+      },
+    });
+    return Effect.gen(function* () {
+      const result = yield* runCheckout(harness, {
+        target: { type: "worktree", path: detachedPath },
+      });
+      expect(result.current).toMatchObject({
+        workspacePath: detachedPath,
+        recordedBranch: null,
+        recordedWorktreePath: detachedPath,
+        actualBranch: null,
+      });
+      expect(harness.dispatch).toHaveBeenCalledWith(
+        expect.objectContaining({ branch: null, worktreePath: detachedPath }),
+      );
+    });
+  });
+
+  it.effect("rejects dirty files before switching branches", () => {
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: rootRefs,
+      workspaceStatuses: { [workspaceRoot]: { branch: "dev", dirty: true } },
+    });
+    return Effect.gen(function* () {
+      const exit = yield* Effect.exit(
+        runCheckout(harness, {
+          target: { type: "branch", branch: "feature/checkout" },
+        }),
+      );
+      expectTypedFailure(exit, { _tag: "WorktreeMcpFailure", code: "dirty_workspace" });
+      expect(harness.switchRef).not.toHaveBeenCalled();
+      expect(harness.dispatch).not.toHaveBeenCalled();
+    });
+  });
+
+  it.effect("rejects a worktree bound to another idle thread", () => {
+    const worktreePath = "/worktrees/project/shared";
+    const harness = makeHarness({
+      thread: { branch: "dev", worktreePath: null },
+      refs: [
+        rootRefs[0],
+        {
+          name: "feature/shared",
+          current: false,
+          isDefault: false,
+          worktreePath,
+        },
+      ],
+      workspaceStatuses: {
+        [workspaceRoot]: { branch: "dev" },
+        [worktreePath]: { branch: "feature/shared" },
+      },
+      projectThreads: [
+        { id: threadId, title: "Caller", branch: "dev", worktreePath: null },
+        {
+          id: ThreadId.make("thread-worktree-shared"),
+          title: "Shared owner",
+          branch: "feature/shared",
+          worktreePath,
+        },
+      ],
+    });
+    return Effect.gen(function* () {
+      const exit = yield* Effect.exit(
+        runCheckout(harness, { target: { type: "worktree", path: worktreePath } }),
+      );
+      expectTypedFailure(exit, { _tag: "WorktreeMcpFailure", code: "workspace_shared" });
+    });
+  });
+
+  it.effect("bounds nested binding identity reads by the requested page", () => {

apps/server/src/mcp/WorktreeMcpService.test.ts Β· right 2658–2658

M
macroscopeappBotAug 30, 2026

mcp/WorktreeMcpService.test.ts:2658

This block (through the does not attribute a nested independent repository... case) is t3_worktree_list coverage living inside the t3_thread_checkout suite, and three of the cases β€” includes archived thread bindings retained on a physical checkout, attributes a nested recorded cwd to its physical worktree root, and does not attribute a nested independent repository to the project worktree β€” are byte-identical duplicates of tests still present in the t3_worktree_list describe above. Looks like a merge/rebase artifact. Consider deleting the duplicates here and moving the remaining runList cases back into describe("t3_worktree_list") so the checkout suite only exercises runCheckout.

No longer relevant as of 1ed8ca43207f300ed83e1f991be080eb0b9f3e54

J
Julius MarmingeAug 30, 2026

Fixed mechanically in 1ed8ca43207f300ed83e1f991be080eb0b9f3e54. I verified the archived-binding, nested-cwd attribution, and nested-independent-repository runList case bodies duplicated the existing t3_worktree_list coverage, then removed only the checkout-describe copies. The three unique pagination/bounded-identity runList cases were moved intact into the t3_worktree_list describe. No production code or assertions changed. WorktreeMcpService passes 114/114, canonical t3 typecheck exits 0, and targeted formatting/diff check pass.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 0a622c2b Β· right 992

Original diff
@@ -730,7 +927,818 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActual.refName,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActual.refName !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorktrees(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
C
cursorBotAug 29, 2026

Missing worktrees block all moves

Medium Severity

t3_thread_checkout and attached t3_worktree_handoff both require the thread's recorded workspace to still be a readable project worktree before any move. A workspace_missing binding therefore cannot return to the project root, reuse another listed worktree, or create a new one, even though the project repository itself is available.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 0a622c2b38dc25c215036226855d9392ff539540. Configure here.

J
Julius MarmingeAug 29, 2026

Confirmed and fixed in 03d0e2bce4. A recorded checkout now resolves through the canonical binding inventory as optional: only a confirmed non-repository/missing source is treated as stale, while unresolved Git failures still fail closed. Recovery targets are always selected from the healthy project inventory, so return-to-root and listed-worktree reuse never touch the missing checkout; new-worktree handoff resolves its base from the physical project root. Focused regressions cover all three recovery paths.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 9335400d Β· right 489

Original diff
@@ -319,8 +476,14 @@ const make = Effect.gen(function* () {
         // suspend: build the rollback only if cleanup actually runs. Removing
         // the worktree must succeed before deleting its freshly created branch;
         // otherwise the branch may still be checked out there.
+        let createdWorktreeRemoved = false;
         const removeCreatedWorktree = Effect.suspend(() =>
           gitWorkflow.removeWorktree({ cwd: projectCwd, path: worktreePath, force: true }).pipe(
+            Effect.tap(() =>
+              Effect.sync(() => {
+                createdWorktreeRemoved = true;
+              }),
+            ),
             Effect.andThen(
               Effect.suspend(() =>
                 gitWorkflow.deleteLocalBranch({
M
macroscopeappBotAug 29, 2026

🟠 High mcp/WorktreeMcpService.ts:489

Rollback can delete commits belonging to another actor. After removeWorktree succeeds, deleteLocalBranch runs git branch -D on worktree.worktree.refName without confirming that the branch still points to the commit created by this handoff, so a concurrent checkout or branch update can cause unrelated commits to be deleted. Verify the branch tip is unchanged before deleting it, or use a compare-and-delete operation.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 489:
2
3Rollback can delete commits belonging to another actor. After `removeWorktree` succeeds, `deleteLocalBranch` runs `git branch -D` on `worktree.worktree.refName` without confirming that the branch still points to the commit created by this handoff, so a concurrent checkout or branch update can cause unrelated commits to be deleted. Verify the branch tip is unchanged before deleting it, or use a compare-and-delete operation.
J
Julius MarmingeAug 29, 2026

Fixed in 05f8e6450d. Failed handoff cleanup still removes the newly created worktree path, but no longer force-deletes the branch by name; the branch is retained because concurrent activity means this operation cannot prove the ref is still unattached and unchanged. Focused handoff race and dispatch-failure tests assert removal plus branch retention.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision a671e359 Β· right 1129

Original diff
@@ -463,21 +631,654 @@ const make = Effect.gen(function* () {
       yield* requireCapability(scope);
       const projection = yield* loadThread(scope);
       const project = yield* loadProject(scope, projection.thread.projectId);
-
-      const defaultStartFromOrigin = yield* readDefaultStartFromOrigin;
+      const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+      const workspacePath = normalizePath(projection.thread.worktreePath ?? projectWorkspaceRoot);
+      const [defaultStartFromOrigin, actual, refs] = yield* Effect.all(
+        [
+          readDefaultStartFromOrigin,
+          readWorkspaceStatus(workspacePath),
+          loadRefs(projectWorkspaceRoot, "local"),
+        ],
+        { concurrency: 3 },
+      );
+      const knownWorkspacePaths = new Set([
+        projectWorkspaceRoot,
+        ...refs.flatMap((ref) =>
+          ref.isRemote === true || ref.worktreePath === null
+            ? []
+            : [normalizePath(ref.worktreePath)],
+        ),
+      ]);
+      const agreement = !knownWorkspacePaths.has(workspacePath)
+        ? "workspace_missing"
+        : !actual.isRepo
+          ? "not_repository"
+          : actual.refName !== projection.thread.branch
+            ? "branch_mismatch"
+            : "in_sync";
 
       const result: WorktreeMcpStatusResult = {
         attached: projection.thread.worktreePath !== null,
         worktreePath: projection.thread.worktreePath,
         branch: projection.thread.branch,
-        projectWorkspaceRoot: project.workspaceRoot,
+        projectWorkspaceRoot,
         defaultStartFromOrigin,
+        recordedWorkspace: {
+          branch: projection.thread.branch,
+          worktreePath: projection.thread.worktreePath,
+        },
+        actualWorkspace: {
+          workspacePath,
+          isRepo: actual.isRepo,
+          branch: actual.refName,
+          hasWorkingTreeChanges: actual.hasWorkingTreeChanges,
+        },
+        agreement,
       };
       return result;
     },
   );
 
-  return WorktreeMcpService.of({ handoff, status });
+  const listWorktrees: WorktreeMcpService["Service"]["listWorktrees"] = Effect.fn(
+    "WorktreeMcpService.listWorktrees",
+  )(function* (scope) {
+    yield* requireCapability(scope);
+    const projection = yield* loadThread(scope);
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+    const [refs, threads] = yield* Effect.all(
+      [loadRefs(projectWorkspaceRoot, "local"), loadProjectThreads(projection.thread.projectId)],
+      { concurrency: 2 },
+    );
+
+    const branchByWorkspacePath = new Map<string, string | null>([[projectWorkspaceRoot, null]]);
+    for (const ref of refs) {
+      if (ref.isRemote === true || ref.worktreePath === null) continue;
+      branchByWorkspacePath.set(normalizePath(ref.worktreePath), ref.name);
+    }
+
+    const worktrees = yield* Effect.forEach(
+      [...branchByWorkspacePath.entries()],
+      ([workspacePath, branch]) =>
+        readWorkspaceStatus(workspacePath).pipe(
+          Effect.map((actual) => ({
+            path: workspacePath,
+            branch,
+            actualBranch: actual.refName,
+            isRepo: actual.isRepo,
+            isProjectRoot: workspacePath === projectWorkspaceRoot,
+            hasWorkingTreeChanges: actual.hasWorkingTreeChanges,
+            bindings: threads
+              .filter(
+                (thread) => threadWorkspacePath(thread, projectWorkspaceRoot) === workspacePath,
+              )
+              .map((thread) => ({
+                threadId: thread.id,
+                title: thread.title,
+                status: thread.status,
+                recordedBranch: thread.branch,
+                recordedWorktreePath: thread.worktreePath,
+                active: thread.activeRunId !== null,
+                callingThread: thread.id === scope.threadId,
+              })),
+          })),
+        ),
+      { concurrency: 8 },
+    );
+
+    return {
+      projectWorkspaceRoot,
+      worktrees: worktrees.toSorted(
+        (left, right) =>
+          Number(right.isProjectRoot) - Number(left.isProjectRoot) ||
+          left.path.localeCompare(right.path),
+      ),
+    } satisfies WorktreeMcpListResult;
+  });
+
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+    const currentWorkspacePath = normalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(currentWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      const actual = yield* readWorkspaceStatus(handoff.worktreePath);
+      return {
+        previous: {
+          workspacePath: currentWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActual.refName,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: actual.refName,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActual.refName !== actual.refName,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        readWorkspaceStatus(currentWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set([
+      projectWorkspaceRoot,
+      ...localRefs.flatMap((ref) =>
+        ref.worktreePath === null ? [] : [normalizePath(ref.worktreePath)],
+      ),
+    ]);
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        targetWorkspacePath = normalizePath(input.target.path);
+        if (
+          targetWorkspacePath === projectWorkspaceRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorkspaceRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorkspaceRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : normalizePath(selectedRef.worktreePath);
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorkspaceRoot
+            : workspace === "current"
+              ? currentWorkspacePath
+              : (selectedWorktreePath ??
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true
+                  ? projectWorkspaceRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : normalizePath(selectedRef.worktreePath);
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch ||
+        (selectedRef?.isRemote === true
+          ? targetBefore.refName !== requestedBranch &&
+            targetBefore.refName !== requestedBranch.replace(/^[^/]+\//, "")
+          : targetBefore.refName !== requestedBranch));
+    const otherBindings = threads.filter(
+      (thread) =>
+        thread.id !== scope.threadId &&
+        threadWorkspacePath(thread, projectWorkspaceRoot) === targetWorkspacePath,
+    );
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorkspaceRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorkspaceRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    return yield* Effect.uninterruptibleMask(() =>
+      Effect.gen(function* () {
+        let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+          targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+        let createdBranch: string | null = null;
+
+        if (shouldMutateCheckout && requestedBranch !== undefined) {
+          if (createBranch) {
+            yield* gitWorkflow
+              .createRef({
+                cwd: targetWorkspacePath,
+                refName: requestedBranch,
+                switchRef: false,
+              })
+              .pipe(asOperationFailed(`Unable to create branch '${requestedBranch}'`));
+            createdBranch = requestedBranch;
+          }
+          const switchExit = yield* Effect.exit(
+            gitWorkflow.switchRef({ cwd: targetWorkspacePath, refName: requestedBranch }),
+          );
+          if (Exit.isFailure(switchExit)) {
+            const afterFailedSwitchExit = yield* Effect.exit(
+              readWorkspaceStatus(targetWorkspacePath),
+            );
+            if (Exit.isFailure(afterFailedSwitchExit)) {
+              return yield* failure(
+                "partial_failure",
+                `Branch checkout failed and the resulting Git state could not be verified: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: null,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            const afterFailedSwitch = afterFailedSwitchExit.value;
+            const checkoutChanged = afterFailedSwitch.refName !== targetBefore.refName;
+            if (checkoutChanged && targetBefore.refName === null) {
+              return yield* failure(
+                "partial_failure",
+                `Branch checkout failed after changing Git state and the previous detached ref cannot be restored automatically: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: afterFailedSwitch.refName,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            const rollback = checkoutChanged
+              ? gitWorkflow.switchRef({
+                  cwd: targetWorkspacePath,
+                  refName: targetBefore.refName!,
+                })
+              : Effect.void;
+            const cleanup = rollback.pipe(
+              Effect.andThen(
+                createdBranch === null
+                  ? Effect.void
+                  : gitWorkflow.deleteLocalBranch({
+                      cwd: targetWorkspacePath,
+                      refName: createdBranch,
+                      force: true,
+                    }),
+              ),
+            );
+            const cleanupExit = yield* Effect.exit(cleanup);
+            if (Exit.isFailure(cleanupExit)) {
+              const actualBranch = yield* readWorkspaceBranchOrNull(targetWorkspacePath);
+              return yield* failure(
+                "partial_failure",
+                `Branch checkout failed and rollback also failed: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch,
+                  rollback: "failed",
+                },
+              );
+            }
+            return yield* failure(
+              "operation_failed",
+              `Unable to check out '${requestedBranch}': ${errorMessage(Cause.squash(switchExit.cause))}`,
+            );
+          }
+          checkoutAction = createBranch ? "created" : "switched";
+        }
+
+        const actualExit = yield* Effect.exit(readWorkspaceStatus(targetWorkspacePath));
+        if (Exit.isFailure(actualExit)) {
+          const detail = errorMessage(Cause.squash(actualExit.cause));
+          if (checkoutAction === "switched" || checkoutAction === "created") {
+            if (targetBefore.refName === null) {
+              return yield* failure(
+                "partial_failure",
+                `Git checkout completed but its resulting state could not be verified: ${detail}`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: null,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            const rollbackExit = yield* Effect.exit(
+              gitWorkflow
+                .switchRef({ cwd: targetWorkspacePath, refName: targetBefore.refName })
+                .pipe(
+                  Effect.andThen(
+                    createdBranch === null
+                      ? Effect.void
+                      : gitWorkflow.deleteLocalBranch({
+                          cwd: targetWorkspacePath,
+                          refName: createdBranch,
+                          force: true,
+                        }),
+                  ),
+                ),
+            );
+            if (Exit.isFailure(rollbackExit)) {
+              const actualBranch = yield* readWorkspaceBranchOrNull(targetWorkspacePath);
+              return yield* failure(
+                "partial_failure",
+                `Git checkout completed, verification failed, and rollback also failed: ${detail}`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch,
+                  rollback: "failed",
+                },
+              );
+            }
+          }
+          return yield* failure(
+            "operation_failed",
+            `Unable to verify the selected checkout '${targetWorkspacePath}': ${detail}`,
+          );
+        }
+        const actual = actualExit.value;
+        const nextBranch = actual.refName;
+        const workspaceChanged = targetWorkspacePath !== currentWorkspacePath;
+        const nextWorktreePath = workspaceChanged
+          ? targetWorkspacePath === projectWorkspaceRoot
+            ? null
+            : targetWorkspacePath
+          : projection.thread.worktreePath;
+        const bindingChanged =
+          nextBranch !== projection.thread.branch ||
+          nextWorktreePath !== projection.thread.worktreePath;
+
+        if (bindingChanged) {
+          const dispatchExit = yield* Effect.exit(
+            threadManagement.dispatch({
M
macroscopeappBotAug 29, 2026

🟠 High mcp/WorktreeMcpService.ts:1129

performCheckout can leave Git inconsistent with the durable thread binding when another actor changes branch or worktreePath after the initial projection read. It switches the checkout before the optimistic dispatch at threadManagement.dispatch detects the stale binding, then rolls Git back to targetBefore while the concurrent metadata update remains committed; add a binding recheck immediately before mutating Git, as performHandoff does.

βœ… Resolved in 98151e0c2ee5b55e702164a7ae31136b96d351d9

J
Julius MarmingeAug 29, 2026

Fixed in 98151e0c2e. Immediately before any Git mutation or binding move, checkout reloads the calling thread and rejects if branch, worktree path, or archive state changed. The metadata command still carries expectedBranch/expectedWorktreePath as the durable compare-and-set backstop. Added a regression test that changes the binding during preparation and verifies Git is untouched.

J
Julius MarmingeAug 29, 2026

Addressed in 8aafc41636ec84adab80a84e289c7cefb0757f1b with a durable backstop rather than relying on another adjacent read. Workspace-binding metadata updates now carry optional expectedArchived: false alongside the branch/worktree CAS, and the production V2 decider checks all three inside its serialized command decision. A real V2 regression archives first, then dispatches the previously prepared workspace update and proves the binding is rejected and unchanged. Rollback also refuses to act once caller lifecycle or ownership differs.

M
macroscopeappBotAug 29, 2026

The CAS and ownership-aware rollback improve recovery, but do not close the race: the recheck at 1176 is followed by resolveCommit calls before mutation. A concurrent actor can bind the target in that window with its observed branch, after which this checkout changes Git; rollback then correctly declines as unsafe, but that actor’s durable binding can disagree with Git. The binding/ownership validation still needs to be adjacent to mutation, or all binding writers need a shared lock.

J
Julius MarmingeAug 29, 2026

Fixed in 9335400d20. Ref/commit resolution now completes before a final adjacent gate re-reads the caller lifecycle, physical bindings across projects, and target ref/dirty state immediately before createRef/switchRef. The deterministic regression changes the caller binding during commit resolution and verifies checkout_in_progress with no Git mutation or metadata dispatch.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 4453207f Β· right 1168

Original diff
@@ -730,7 +954,851 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActualBranch = yield* readWorkspaceBranchOrNull(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActualBranch,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActualBranch !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorkspaceBindingInventory(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (
+      Option.isSome(currentInventory) &&
+      inventory.repositoryCommonDir !== currentInventory.value.repositoryCommonDir
+    ) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = Option.isSome(currentInventory)
+      ? currentInventory.value.currentWorktreeRoot
+      : null;
+    if (projectWorktreeRoot === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project checkout.",
+      );
+    }
+    if (Option.isSome(currentInventory) && currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        readWorkspaceStatus(recordedWorkspacePath).pipe(
+          Effect.orElseSucceed(() => ({
+            isRepo: false,
+            refName: null,
+            hasWorkingTreeChanges: false,
+          })),
+        ),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        if (requestedBranch !== undefined) {
+          selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+          if (createBranch && localRefByName.has(requestedBranch)) {
+            return yield* failure(
+              "invalid_request",
+              `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+            );
+          }
+          if (!createBranch && selectedRef === undefined) {
+            return yield* failure(
+              "invalid_request",
+              `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the project root.`,
+            );
+          }
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? (currentWorkspacePath ?? recordedWorkspacePath)
+              : (selectedWorktreePath ??
+                (currentWorkspacePath === null ||
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true)
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
C
cursorBotAug 30, 2026

Status failure reported as not a repo

Low Severity

When the recorded checkout inventory succeeds but the follow-up status read fails, that failure is turned into { isRepo: false }. A same-workspace checkout then rejects the target as not a Git repository instead of failing closed as an unresolved Git error.

Reviewed by Cursor Bugbot for commit 4453207fd2655b3debb17b3146e8bc45110f1958. Configure here.

J
Julius MarmingeAug 30, 2026

Fixed in 1d545798de018346d5a02146a79727b602844f6d. Once inventory confirms the recorded checkout exists, the status read stays in the typed error channel and returns before ref selection or mutation. The synthetic result is now limited to inventory-confirmed missing/non-repository paths; the regression asserts no switch, ref creation, or metadata dispatch.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 69afd25d Β· right 584

Original diff
@@ -321,77 +510,130 @@ const make = Effect.gen(function* () {
             }).pipe(Effect.as({ status: "failed", detail } as const));
           });
 
-        // suspend: build the rollback only if cleanup actually runs. Removing
-        // the worktree must succeed before deleting its freshly created branch;
-        // otherwise the branch may still be checked out there.
+        // Removing the new worktree is safe because this call still owns its
+        // path. Retain the branch: after concurrent Git activity, branch-name
+        // identity alone is not enough to authorize deleting the ref.
+        let createdWorktreeRemoved = false;
         const removeCreatedWorktree = Effect.suspend(() =>
           gitWorkflow.removeWorktree({ cwd: projectCwd, path: worktreePath, force: true }).pipe(
-            Effect.andThen(
-              Effect.suspend(() =>
-                gitWorkflow.deleteLocalBranch({
-                  cwd: projectCwd,
-                  refName: worktree.worktree.refName,
-                  force: true,
-                }),
-              ),
+            Effect.tap(() =>
+              Effect.sync(() => {
+                createdWorktreeRemoved = true;
+              }),
             ),
           ),
-        ).pipe(Effect.ignoreCause({ log: true }));
-
-        const recheckAndBind = Effect.gen(function* () {
-          // The projection was read before the potentially slow git work
-          // above; a concurrent binding (for example from the UI) could have
-          // attached the thread in the meantime. Re-check before committing so
-          // the race cannot leave a second, untracked worktree.
-          const recheck = yield* loadThread(scope);
-          if (recheck.thread.worktreePath !== null) {
-            return yield* alreadyInWorktree(recheck.thread.worktreePath);
+        );
+
+        const recheckExit = yield* Effect.exit(
+          Effect.gen(function* () {
+            // The projection was read before the potentially slow git work
+            // above; a concurrent binding (for example from the UI) could have
+            // attached the thread in the meantime. Re-check before committing so
+            // the race cannot leave a second, untracked worktree.
+            const recheck = yield* loadThread(scope);
+            if (
+              recheck.thread.worktreePath !== projection.thread.worktreePath ||
+              recheck.thread.branch !== projection.thread.branch
+            ) {
+              return yield* failure(
+                "operation_failed",
+                `Thread '${scope.threadId}' changed workspace while the new worktree was being created; the handoff was rolled back.`,
+              );
+            }
+            // Mirror the up-front archived check: the thread may have been
+            // archived during the slow git work, and an archived thread must
+            // not be bound to a fresh worktree it can never use.
+            if (recheck.thread.archivedAt !== null) {
+              return yield* failure(
+                "invalid_request",
+                `Thread '${scope.threadId}' was archived while the worktree was being created; the handoff was rolled back.`,
+              );
+            }
+          }),
+        );
+        if (Exit.isFailure(recheckExit)) {
+          if (Cause.hasInterruptsOnly(recheckExit.cause)) {
+            return yield* Effect.failCause(recheckExit.cause as Cause.Cause<never>);
           }
-          // Mirror the up-front archived check: the thread may have been
-          // archived during the slow git work, and an archived thread must
-          // not be bound to a fresh worktree it can never use.
-          if (recheck.thread.archivedAt !== null) {
+          const cleanupExit = yield* Effect.exit(removeCreatedWorktree);
+          if (Exit.isFailure(cleanupExit)) {
             return yield* failure(
-              "invalid_request",
-              `Thread '${scope.threadId}' was archived while the worktree was being created; the handoff was rolled back.`,
+              "partial_failure",
+              `The handoff failed before binding and the created worktree could not be removed: ${errorMessage(Cause.squash(recheckExit.cause))}`,
+              {
+                workspacePath: worktreePath,
+                recordedBranch: projection.thread.branch,
+                actualBranch: createdWorktreeRemoved ? null : worktree.worktree.refName,
+                rollback: "failed",
+              },
             );
           }
-          yield* threadManagement
-            .dispatch({
-              type: "thread.metadata.update",
-              commandId: ids.commandId,
-              threadId: scope.threadId,
-              branch: worktree.worktree.refName,
-              worktreePath,
-              expectedWorktreePath: null,
-            })
-            .pipe(
-              Effect.catchCause((cause) =>
-                // Interrupt-only causes propagate unchanged: whether the
-                // dispatch committed is unknown, so neither a typed failure
-                // nor a rollback would be correct. Failures and defects
-                // (including mixed causes) map to a typed operation_failed.
-                Cause.hasInterruptsOnly(cause)
-                  ? Effect.failCause(cause as Cause.Cause<never>)
-                  : Effect.fail(
-                      failure(
-                        "operation_failed",
-                        `Unable to re-point the thread at the worktree: ${errorMessage(Cause.squash(cause))}`,
-                      ),
-                    ),
-              ),
-            );
-        }).pipe(
-          // onError: the worktree was already created, so any failure between
-          // here and the committed binding (recheck read, recheck race,
-          // dispatch typed failure or defect) must remove it again so a failed
-          // handoff leaves nothing behind on disk. Interrupt-only causes skip
-          // the removal: the binding may have committed, and force-deleting a
-          // worktree the thread now points at would be worse than leaking one.
-          Effect.onError((cause) =>
-            Cause.hasInterruptsOnly(cause) ? Effect.void : removeCreatedWorktree,
-          ),
+          return yield* Effect.failCause(recheckExit.cause);
+        }
+
+        const dispatchExit = yield* Effect.exit(
+          threadManagement.dispatch({
+            type: "thread.metadata.update",
+            commandId: ids.commandId,
+            threadId: scope.threadId,
+            branch: worktree.worktree.refName,
+            worktreePath,
+            expectedBranch: projection.thread.branch,
+            expectedWorktreePath: projection.thread.worktreePath,
+            expectedArchived: false,
+          }),
C
cursorBotAug 30, 2026

New worktrees can be double-bound

High Severity

performHandoff creates the worktree and then rechecks only the caller thread before binding. It never takes the physical checkout guard and never inspects other owners of the new path. A concurrent t3_thread_checkout of that branch or path can bind another thread first; the handoff still commits, so two threads share one mutable checkout.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 69afd25d7a604c150202fec55efd927f0c8eda83. Configure here.

J
Julius MarmingeAug 30, 2026

Fixed in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284. After creation, handoff resolves the actual physical checkout, acquires the same common-dir/worktree-root guard as checkout, and rereads global active/archived bindings before committing. The deterministic handoff-versus-checkout race proves the winning binding survives and the losing handoff retains rather than deletes the checkout. Every cleanup path now re-proves no competing owner, unchanged caller binding, exact physical identity, clean/ref state, and the pre-create expected HEAD; ambiguous or changed state is retained. The focused suite passes 117/117.

docs/user/source-control.md Β· Earlier revision 98151e0c Β· right 85–85

Original diff
@@ -65,14 +65,24 @@ The **Source Control settings** page shows you exactly what's connected:
 
 Run a quick **Rescan** after setting up a new machine or changing credentials.
 
-### Let an agent inspect its checkout
+### Let an agent change its checkout
 
 Agents running through T3 Code can inspect the checkout recorded on their thread and compare it
 with Git's actual branch. They can also list the project root and Git-registered worktrees,
 including detached checkouts, dirty state, and the durable branch and worktree path recorded for
 other threads using each checkout. Git resolves symlinked checkout paths through the repository's
-real worktree inventory. These read paths apply only to the calling thread's current project and
-do not create, remove, prune, or revive worktrees.
+real worktree inventory.
+
+An agent can move its current thread to an existing branch, return to the project root, reuse an
+unclaimed worktree, or create a new worktree. T3 Code performs the Git operation before it updates
+the thread's saved branch and worktree path. It refuses to switch a dirty checkout and will not
+silently stash or discard files. It also refuses to take over a worktree owned by another thread or
+switch a shared root while another thread is bound there.
+
+Moving between workspace paths restarts the agent session in the selected checkout. The agent can
+queue a continuation before that restart, so longer work resumes without needing the browser to
+stay open. These controls apply only to the calling thread and its current project. They do not
+remove, prune, or revive worktrees.

docs/user/source-control.md Β· right 85–85

M
macroscopeappBotAug 29, 2026

🟒 Low user/source-control.md:85

A failed t3_worktree_handoff can remove the newly created target worktree, so this absolute statement misleads callers troubleshooting a failed handoff. The rollback calls gitWorkflow.removeWorktree when the pre-commit recheck or durable binding update fails; please qualify the documentation to distinguish that cleanup from normal worktree controls.

suggestion
1They do not remove, prune, or revive existing worktrees; a failed handoff removes a newly created target worktree when its pre-commit recheck or durable binding update fails.
Also found in 1 other location(s)

docs/orchestration-v2/orchestrator-mcp-server.md:345

t3_worktree_handoff does remove its newly created target worktree when the pre-commit recheck or durable binding update fails: removeCreatedWorktree calls gitWorkflow.removeWorktree on both rollback paths. Thus the absolute statement that neither tool removes a target worktree is incorrect and can mislead callers troubleshooting a failed handoff.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @docs/user/source-control.md around line 85:
2
3A failed `t3_worktree_handoff` can remove the newly created target worktree, so this absolute statement misleads callers troubleshooting a failed handoff. The rollback calls `gitWorkflow.removeWorktree` when the pre-commit recheck or durable binding update fails; please qualify the documentation to distinguish that cleanup from normal worktree controls.
4
5Also found in 1 other location(s):
6- docs/orchestration-v2/orchestrator-mcp-server.md:345 -- `t3_worktree_handoff` does remove its newly created target worktree when the pre-commit recheck or durable binding update fails: `removeCreatedWorktree` calls `gitWorkflow.removeWorktree` on both rollback paths. Thus the absolute statement that neither tool removes a target worktree is incorrect and can mislead callers troubleshooting a failed handoff.
J
Julius MarmingeAug 29, 2026

Fixed in d1e40d08db. Both user and internal docs now distinguish existing-worktree controls from rollback cleanup: a failed new-worktree transition may remove only the checkout it just created when the durable binding did not commit.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision d653ad1c Β· right 1567

Original diff
@@ -759,7 +1111,862 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActualBranch = yield* readWorkspaceBranchOrNull(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActualBranch,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActualBranch !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorkspaceBindingInventory(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (
+      Option.isSome(currentInventory) &&
+      inventory.repositoryCommonDir !== currentInventory.value.repositoryCommonDir
+    ) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = Option.isSome(currentInventory)
+      ? currentInventory.value.currentWorktreeRoot
+      : null;
+    if (projectWorktreeRoot === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project checkout.",
+      );
+    }
+    if (Option.isSome(currentInventory) && currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        Option.isNone(currentInventory)
+          ? Effect.succeed({
+              isRepo: false,
+              refName: null,
+              hasWorkingTreeChanges: false,
+            })
+          : readWorkspaceStatus(recordedWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        if (requestedBranch !== undefined) {
+          selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+          if (createBranch && localRefByName.has(requestedBranch)) {
+            return yield* failure(
+              "invalid_request",
+              `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+            );
+          }
+          if (!createBranch && selectedRef === undefined) {
+            return yield* failure(
+              "invalid_request",
+              `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the project root.`,
+            );
+          }
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? (currentWorkspacePath ?? recordedWorkspacePath)
+              : (selectedWorktreePath ??
+                (currentWorkspacePath === null ||
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true)
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      Effect.gen(function* () {
+        const recordedPath = yield* threadWorkspacePath(thread, projectWorktreeRoot);
+        const recordedInventory = yield* loadWorkspaceBindingInventory(recordedPath);
+        const workspacePath = Option.match(recordedInventory, {
+          onNone: () => recordedPath,
+          onSome: (candidateInventory) =>
+            candidateInventory.repositoryCommonDir === inventory.repositoryCommonDir &&
+            candidateInventory.currentWorktreeRoot !== null
+              ? candidateInventory.currentWorktreeRoot
+              : recordedPath,
+        });
+        return [thread, workspacePath] as const;
+      }),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = physicalWorkspaceGuardKey(
+      inventory.repositoryCommonDir,
+      targetWorkspacePath,
+    );
+    return yield* Effect.uninterruptibleMask((restore) =>
+      Effect.suspend(() => {
+        if (workspaceTransitionsInFlight.has(workspaceGuardKey)) {
+          return Effect.fail(
+            failure(
+              "checkout_in_progress",
+              `Another workspace transition is already in progress for '${targetWorkspacePath}'.`,
+            ),
+          );
+        }
+        workspaceTransitionsInFlight.add(workspaceGuardKey);
+        return Effect.gen(function* () {
+          const [latestProjection, latestInventory, latestBindings, latestTargetBefore] =
+            yield* restore(
+              Effect.all(
+                [
+                  loadThread(scope),
+                  loadWorktrees(projectWorkspaceRoot),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  readWorkspaceStatus(targetWorkspacePath),
+                ],
+                { concurrency: 4 },
+              ),
+            );
+          if (
+            latestProjection.thread.branch !== projection.thread.branch ||
+            latestProjection.thread.worktreePath !== projection.thread.worktreePath ||
+            latestProjection.thread.archivedAt !== projection.thread.archivedAt
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Thread '${scope.threadId}' changed workspace state while checkout was being prepared. Retry from its current binding.`,
+            );
+          }
+          if (
+            latestInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+            !latestInventory.worktrees.some((worktree) => worktree.path === targetWorkspacePath)
+          ) {
+            return yield* failure(
+              "scope_mismatch",
+              `Checkout target '${targetWorkspacePath}' is no longer registered in the calling thread's Git repository.`,
+            );
+          }
+          if (
+            latestTargetBefore.refName !== targetBefore.refName ||
+            latestTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Checkout '${targetWorkspacePath}' changed while the transition was being prepared. Retry from its current Git state.`,
+            );
+          }
+          if (shouldMutateCheckout && latestTargetBefore.hasWorkingTreeChanges) {
+            return yield* failure(
+              "dirty_workspace",
+              `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+            );
+          }
+          const latestOtherBindings = latestBindings
+            .filter(
+              ([thread, workspacePath]) =>
+                thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+            )
+            .map(([thread]) => thread);
+          const latestActiveBinding = latestOtherBindings.find(
+            (thread) => thread.activeRunId !== null,
+          );
+          if (
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestActiveBinding
+          ) {
+            return yield* failure(
+              "workspace_in_use",
+              `Checkout '${targetWorkspacePath}' is in use by active thread '${latestActiveBinding.id}' (${latestActiveBinding.title}).`,
+            );
+          }
+          if (
+            targetWorkspacePath !== projectWorktreeRoot &&
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `Worktree '${targetWorkspacePath}' is already bound to thread '${latestOtherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+            );
+          }
+          if (
+            targetWorkspacePath === projectWorktreeRoot &&
+            shouldMutateCheckout &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `The project root is also bound to thread '${latestOtherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+            );
+          }
+
+          let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+            targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+          let resolvedBranch: string | null = targetBefore.refName;
+          const targetBeforeCommit = shouldMutateCheckout
+            ? yield* restore(
+                gitWorkflow
+                  .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                  .pipe(asOperationFailed("Unable to record the checkout's current commit")),
+              )
+            : null;
+          const requestedTransitionCommit =
+            shouldMutateCheckout && requestedBranch !== undefined
+              ? createBranch
+                ? targetBeforeCommit
+                : yield* restore(
+                    gitWorkflow
+                      .resolveCommit({ cwd: targetWorkspacePath, revision: requestedBranch })
+                      .pipe(asOperationFailed(`Unable to resolve ref '${requestedBranch}'`)),
+                  )
+              : null;
+          let ownedCheckoutState: {
+            readonly refName: string | null;
+            readonly hasWorkingTreeChanges: boolean;
+            readonly commitSha: string;
+          } | null = null;
+
+          const captureCheckoutState = Effect.fn("WorktreeMcpService.captureCheckoutState")(
+            function* () {
+              const status = yield* readWorkspaceStatus(targetWorkspacePath);
+              const commit = yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to identify the checkout's current commit"));
+              return {
+                refName: status.refName,
+                hasWorkingTreeChanges: status.hasWorkingTreeChanges,
+                commitSha: commit.commitSha,
+              };
+            },
+          );
+
+          const rollbackOwnedCheckout = Effect.fn("WorktreeMcpService.rollbackOwnedCheckout")(
+            function* () {
+              if (!shouldMutateCheckout) {
+                return "not_needed" as const;
+              }
+              if (ownedCheckoutState === null || targetBeforeCommit === null) {
+                return "not_possible" as const;
+              }
+              const [latestStateExit, latestBindings, callerProjectionExit] = yield* Effect.all(
+                [
+                  Effect.exit(captureCheckoutState()),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  Effect.exit(threadManagement.getThreadProjection(scope.threadId)),
+                ],
+                { concurrency: 3 },
+              );
C
cursorBotAug 30, 2026

Rollback fails closed without partial details

Medium Severity

rollbackOwnedCheckout lets loadActiveWorkspaceBindings fail the effect instead of treating that unresolved-owner case as not_possible. After Git has already moved, callers then surface a generic operation_failed with no partial payload, so agents cannot tell that checkout changed, that the binding did not commit, or that rollback was skipped.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit d653ad1c5d343d0257fd4b82a2acc03840db68b5. Configure here.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 0a622c2b Β· right 1057

Original diff
@@ -730,7 +927,818 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActual.refName,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActual.refName !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorktrees(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (inventory.repositoryCommonDir !== currentInventory.repositoryCommonDir) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = currentInventory.currentWorktreeRoot;
+    if (projectWorktreeRoot === null || currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project or thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        readWorkspaceStatus(currentWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        break;
C
cursorBotAug 29, 2026

Project root skips branch checks

Low Severity

target.type='project_root' never applies the existence checks used for target.type='branch'. create=true on an existing local branch still counts as a mutation, so dirty and shared-root guards fire and createRef later fails. A missing branch without create also falls through to switchRef and surfaces as operation_failed.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 0a622c2b38dc25c215036226855d9392ff539540. Configure here.

J
Julius MarmingeAug 29, 2026

Confirmed and fixed in 03d0e2bce4. project_root requests with a branch now resolve the same local/remote ref maps and reject create=true for an existing local branch or create=false for a missing ref before dirty/shared/mutation checks. The focused regression asserts both typed invalid_request outcomes and that neither createRef nor switchRef runs.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 05f8e645 Β· right 258

Original diff
@@ -162,34 +202,141 @@ const make = Effect.gen(function* () {
         asOperationFailed(`Unable to read git status in '${workspacePath}'`),
       );
 
-  const handoffIds = (scope: McpInvocationScope) =>
+  const loadWorkspaceBindingInventory = Effect.fn(
+    "WorktreeMcpService.loadWorkspaceBindingInventory",
+  )(function* (workspacePath: string) {
+    const inventoryExit = yield* Effect.exit(loadWorktrees(workspacePath));
+    if (Exit.isSuccess(inventoryExit)) {
+      return Option.some(inventoryExit.value);
+    }
+    const statusExit = yield* Effect.exit(readWorkspaceStatus(workspacePath));
+    if (Exit.isSuccess(statusExit) && !statusExit.value.isRepo) {
+      return Option.none();
+    }
+    return yield* Effect.failCause(inventoryExit.cause);
+  });
+
+  const loadActiveWorkspaceBindings = Effect.fn("WorktreeMcpService.loadActiveWorkspaceBindings")(
+    function* (repositoryCommonDir: string) {
+      const snapshot = yield* threadManagement
+        .getShellSnapshot()
+        .pipe(asOperationFailed("Unable to inspect thread workspace bindings"));
+      const byProject = new Map<ProjectId, Array<OrchestrationV2ThreadShell>>();
+      for (const thread of [...snapshot.threads, ...snapshot.archivedThreads]) {
+        const projectThreads = byProject.get(thread.projectId) ?? [];
+        projectThreads.push(thread);
+        byProject.set(thread.projectId, projectThreads);
+      }
+
+      return yield* Effect.forEach(
+        [...byProject.entries()],
+        ([projectId, projectThreads]) =>
+          projects.getById(projectId).pipe(
+            asOperationFailed(
+              `Unable to read project ${projectId} while checking workspace owners`,
+            ),
+            Effect.flatMap(
+              Option.match({
+                onNone: () => Effect.succeed([]),
+                onSome: (project) =>
+                  Effect.gen(function* () {
+                    const projectInventory = yield* loadWorkspaceBindingInventory(
+                      project.workspaceRoot,
+                    );
+                    return yield* Effect.forEach(projectThreads, (thread) =>
+                      Effect.gen(function* () {
+                        const inventory =
+                          thread.worktreePath === null
+                            ? projectInventory
+                            : yield* loadWorkspaceBindingInventory(thread.worktreePath);
+                        if (
+                          Option.isNone(inventory) ||
+                          inventory.value.repositoryCommonDir !== repositoryCommonDir ||
+                          inventory.value.currentWorktreeRoot === null
+                        ) {
+                          return [];
+                        }
C
cursorBotAug 29, 2026

Unresolved same-repo owners skipped

Medium Severity

loadActiveWorkspaceBindings drops a thread when inventory belongs to the target repository but currentWorktreeRoot is null. That is an unresolved same-repo owner, yet checkout continues as if nobody holds the checkout instead of failing closed.

Reviewed by Cursor Bugbot for commit 05f8e6450db277e831f923c4c278f790716a9090. Configure here.

docs/orchestration-v2/orchestrator-mcp-server.md Β· Earlier revision 98151e0c Β· right 337–374

Original diff
@@ -317,6 +317,34 @@ entry includes its listed and actual branch, dirty state, and threads bound to t
 Bindings keep each thread's recorded branch and worktree path separate from the actual checkout.
 The tool does not create, remove, prune, or repair worktrees.
 
+### `t3_thread_checkout`
+
+Changes the calling existing thread to one of four typed targets:
+
+- a branch, optionally creating it in the current checkout or project root;
+- an existing worktree returned by `t3_worktree_list`;
+- the project root; or
+- a new worktree created through the existing Git worktree service.
+
+The service verifies project scope and the actual Git ref before it writes the durable
+`thread.metadata.update` command. The command includes the expected old branch and worktree path,
+so a concurrent metadata change is rejected. A failed metadata write rolls a branch switch back
+when possible and reports a typed partial failure if rollback also fails.
+
+The service never stashes or discards files. Branch switches reject dirty checkouts. Reusing a
+dedicated worktree bound to another thread is rejected, as is mutating a shared project root while
+another live thread is bound there. Moving to a shared project root without switching its branch is
+allowed unless another root-bound thread has an active run.

docs/orchestration-v2/orchestrator-mcp-server.md Β· right 337–374

M
macroscopeappBotAug 29, 2026

🟒 Low orchestration-v2/orchestrator-mcp-server.md:337

The documentation says moving to a shared project root is allowed when other threads are idle, but t3_thread_checkout rejects the root mutation whenever any other thread is bound there, including idle threads. Update the allowance to state that the move is rejected whenever another thread is bound to the root.

No longer relevant as of 1ed8ca43207f300ed83e1f991be080eb0b9f3e54

J
Julius MarmingeAug 29, 2026

This is already accurate in d1e40d08db. For a project-root move without a branch switch, shouldMutateCheckout is false: the workspace_shared root guard applies only when shouldMutateCheckout is true, while the preceding workspace_in_use guard rejects the move only when another root-bound thread has an active run. An idle root owner therefore remains allowed exactly as documented.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision d1e40d08 Β· right 200

Original diff
@@ -154,6 +194,61 @@ const make = Effect.gen(function* () {
       .listProjectThreads({ projectId, includeSubagents: true })
       .pipe(asOperationFailed(`Unable to list threads in project ${projectId}`));
 
+  const loadActiveWorkspaceBindings = Effect.fn("WorktreeMcpService.loadActiveWorkspaceBindings")(
+    function* (repositoryCommonDir: string) {
+      const snapshot = yield* threadManagement
+        .getShellSnapshot({ location: "active" })
M
macroscopeappBotAug 29, 2026

🟠 High mcp/WorktreeMcpService.ts:200

loadActiveWorkspaceBindings excludes archived threads, so an active thread can claim or switch an archived thread's worktreePath; after thread.unarchive, both threads retain usable bindings to the same mutable checkout. Include archived threads in this ownership recheck so unarchiving cannot create a shared worktree.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 200:
2
3`loadActiveWorkspaceBindings` excludes archived threads, so an active thread can claim or switch an archived thread's `worktreePath`; after `thread.unarchive`, both threads retain usable bindings to the same mutable checkout. Include archived threads in this ownership recheck so unarchiving cannot create a shared worktree.
J
Julius MarmingeAug 29, 2026

Fixed in 5ef4d4c3499e4b6114850f44717f98ebc9d60a93. Canonical ownership discovery now combines active and archived shell snapshots before repository/worktree identity resolution, so an archived binding reserves the same physical checkout and blocks reuse. Added a regression proving an archived owner produces workspace_shared without Git mutation or binding dispatch.

docs/orchestration-v2/orchestrator-mcp-server.md Β· Earlier revision 1d545798 Β· right 49–50

Original diff
@@ -44,10 +44,10 @@ Before `ProviderSessionManager` opens a new V2 provider session, it asks
 - the concrete provider instance; and
 - the provider session.
 
-The credential grants `preview` and `orchestration` capabilities. Credentials
-expire after a maximum lifetime, expire when idle, and are revoked when the
-provider session is released. The raw token is not persisted in orchestration
-state.
+The credential grants `orchestration` and `worktree` capabilities. It grants `preview` only when
+the provider session reports `browserToolsAvailable`; disabling agent browser access removes that
+capability. Credentials expire after a maximum lifetime, expire when idle, and are revoked when
+the provider session is released. The raw token is not persisted in orchestration state.

docs/orchestration-v2/orchestrator-mcp-server.md Β· right 49–50

M
macroscopeappBotAug 30, 2026

🟑 Medium orchestration-v2/orchestrator-mcp-server.md:49

The documentation promises that credentials expire after a maximum lifetime, but McpSessionRegistry.touch runs for provider turns, so an active session can keep its credential indefinitely; the stated security bound is not enforced. Remove the maximum-lifetime claim and document only the liveness-based expiry.

suggestion
1capability. Credentials expire when idle and are revoked when
2the provider session is released. The raw token is not persisted in orchestration state.
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @docs/orchestration-v2/orchestrator-mcp-server.md around lines 49-50:
2
3The documentation promises that credentials expire after a maximum lifetime, but `McpSessionRegistry.touch` runs for provider turns, so an active session can keep its credential indefinitely; the stated security bound is not enforced. Remove the maximum-lifetime claim and document only the liveness-based expiry.
J
Julius MarmingeAug 30, 2026

Fixed in 76ccdb2bbb30da271f74fa152ba3c3df14409ba9. The docs now match McpSessionRegistry: resolve and provider-turn touch refresh lastAliveAt, pruning uses only livenessWindowMs, and provider-session release revokes eagerly. The conditional preview wording remains intact. Targeted formatter and diff checks pass.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 8aafc416 Β· right 933

Original diff
@@ -707,7 +894,770 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
C
cursorBotAug 29, 2026

New worktree skips workspace serialization

Medium Severity

new_worktree calls performHandoff without taking the per-physical-workspace guard, and handoff never claims the created path. After createWorktree succeeds and before the binding commits, another thread can list the new checkout, pass ownership checks, and bind to it. Both threads then share one mutable worktree, which the new guards were meant to prevent.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 8aafc41636ec84adab80a84e289c7cefb0757f1b. Configure here.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision e7221b8a Β· right 1715

Original diff
@@ -756,7 +1110,862 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActualBranch = yield* readWorkspaceBranchOrNull(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActualBranch,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActualBranch !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorkspaceBindingInventory(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (
+      Option.isSome(currentInventory) &&
+      inventory.repositoryCommonDir !== currentInventory.value.repositoryCommonDir
+    ) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = Option.isSome(currentInventory)
+      ? currentInventory.value.currentWorktreeRoot
+      : null;
+    if (projectWorktreeRoot === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project checkout.",
+      );
+    }
+    if (Option.isSome(currentInventory) && currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        Option.isNone(currentInventory)
+          ? Effect.succeed({
+              isRepo: false,
+              refName: null,
+              hasWorkingTreeChanges: false,
+            })
+          : readWorkspaceStatus(recordedWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        if (requestedBranch !== undefined) {
+          selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+          if (createBranch && localRefByName.has(requestedBranch)) {
+            return yield* failure(
+              "invalid_request",
+              `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+            );
+          }
+          if (!createBranch && selectedRef === undefined) {
+            return yield* failure(
+              "invalid_request",
+              `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the project root.`,
+            );
+          }
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? (currentWorkspacePath ?? recordedWorkspacePath)
+              : (selectedWorktreePath ??
+                (currentWorkspacePath === null ||
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true)
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      Effect.gen(function* () {
+        const recordedPath = yield* threadWorkspacePath(thread, projectWorktreeRoot);
+        const recordedInventory = yield* loadWorkspaceBindingInventory(recordedPath);
+        const workspacePath = Option.match(recordedInventory, {
+          onNone: () => recordedPath,
+          onSome: (candidateInventory) =>
+            candidateInventory.repositoryCommonDir === inventory.repositoryCommonDir &&
+            candidateInventory.currentWorktreeRoot !== null
+              ? candidateInventory.currentWorktreeRoot
+              : recordedPath,
+        });
+        return [thread, workspacePath] as const;
+      }),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = physicalWorkspaceGuardKey(
+      inventory.repositoryCommonDir,
+      targetWorkspacePath,
+    );
+    return yield* Effect.uninterruptibleMask((restore) =>
+      Effect.suspend(() => {
+        if (workspaceTransitionsInFlight.has(workspaceGuardKey)) {
+          return Effect.fail(
+            failure(
+              "checkout_in_progress",
+              `Another workspace transition is already in progress for '${targetWorkspacePath}'.`,
+            ),
+          );
+        }
+        workspaceTransitionsInFlight.add(workspaceGuardKey);
+        return Effect.gen(function* () {
+          const [latestProjection, latestInventory, latestBindings, latestTargetBefore] =
+            yield* restore(
+              Effect.all(
+                [
+                  loadThread(scope),
+                  loadWorktrees(projectWorkspaceRoot),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  readWorkspaceStatus(targetWorkspacePath),
+                ],
+                { concurrency: 4 },
+              ),
+            );
+          if (
+            latestProjection.thread.branch !== projection.thread.branch ||
+            latestProjection.thread.worktreePath !== projection.thread.worktreePath ||
+            latestProjection.thread.archivedAt !== projection.thread.archivedAt
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Thread '${scope.threadId}' changed workspace state while checkout was being prepared. Retry from its current binding.`,
+            );
+          }
+          if (
+            latestInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+            !latestInventory.worktrees.some((worktree) => worktree.path === targetWorkspacePath)
+          ) {
+            return yield* failure(
+              "scope_mismatch",
+              `Checkout target '${targetWorkspacePath}' is no longer registered in the calling thread's Git repository.`,
+            );
+          }
+          if (
+            latestTargetBefore.refName !== targetBefore.refName ||
+            latestTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Checkout '${targetWorkspacePath}' changed while the transition was being prepared. Retry from its current Git state.`,
+            );
+          }
+          if (shouldMutateCheckout && latestTargetBefore.hasWorkingTreeChanges) {
+            return yield* failure(
+              "dirty_workspace",
+              `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+            );
+          }
+          const latestOtherBindings = latestBindings
+            .filter(
+              ([thread, workspacePath]) =>
+                thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+            )
+            .map(([thread]) => thread);
+          const latestActiveBinding = latestOtherBindings.find(
+            (thread) => thread.activeRunId !== null,
+          );
+          if (
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestActiveBinding
+          ) {
+            return yield* failure(
+              "workspace_in_use",
+              `Checkout '${targetWorkspacePath}' is in use by active thread '${latestActiveBinding.id}' (${latestActiveBinding.title}).`,
+            );
+          }
+          if (
+            targetWorkspacePath !== projectWorktreeRoot &&
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `Worktree '${targetWorkspacePath}' is already bound to thread '${latestOtherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+            );
+          }
+          if (
+            targetWorkspacePath === projectWorktreeRoot &&
+            shouldMutateCheckout &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `The project root is also bound to thread '${latestOtherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+            );
+          }
+
+          let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+            targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+          let resolvedBranch: string | null = targetBefore.refName;
+          const targetBeforeCommit = shouldMutateCheckout
+            ? yield* restore(
+                gitWorkflow
+                  .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                  .pipe(asOperationFailed("Unable to record the checkout's current commit")),
+              )
+            : null;
+          const requestedTransitionCommit =
+            shouldMutateCheckout && requestedBranch !== undefined
+              ? createBranch
+                ? targetBeforeCommit
+                : yield* restore(
+                    gitWorkflow
+                      .resolveCommit({ cwd: targetWorkspacePath, revision: requestedBranch })
+                      .pipe(asOperationFailed(`Unable to resolve ref '${requestedBranch}'`)),
+                  )
+              : null;
+          let ownedCheckoutState: {
+            readonly refName: string | null;
+            readonly hasWorkingTreeChanges: boolean;
+            readonly commitSha: string;
+          } | null = null;
+
+          const captureCheckoutState = Effect.fn("WorktreeMcpService.captureCheckoutState")(
+            function* () {
+              const status = yield* readWorkspaceStatus(targetWorkspacePath);
+              const commit = yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to identify the checkout's current commit"));
+              return {
+                refName: status.refName,
+                hasWorkingTreeChanges: status.hasWorkingTreeChanges,
+                commitSha: commit.commitSha,
+              };
+            },
+          );
+
+          const rollbackOwnedCheckout = Effect.fn("WorktreeMcpService.rollbackOwnedCheckout")(
+            function* () {
+              if (!shouldMutateCheckout) {
+                return "not_needed" as const;
+              }
+              if (ownedCheckoutState === null || targetBeforeCommit === null) {
+                return "not_possible" as const;
+              }
+              const [latestStateExit, latestBindings, callerProjectionExit] = yield* Effect.all(
+                [
+                  Effect.exit(captureCheckoutState()),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  Effect.exit(threadManagement.getThreadProjection(scope.threadId)),
+                ],
+                { concurrency: 3 },
+              );
+              const anotherOwner = latestBindings.some(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              );
+              const callerStillInitial =
+                Exit.isSuccess(callerProjectionExit) &&
+                callerProjectionExit.value.thread.branch === projection.thread.branch &&
+                callerProjectionExit.value.thread.worktreePath === projection.thread.worktreePath &&
+                callerProjectionExit.value.thread.archivedAt === projection.thread.archivedAt &&
+                callerProjectionExit.value.thread.deletedAt === projection.thread.deletedAt;
+              if (
+                Exit.isFailure(latestStateExit) ||
+                latestStateExit.value.refName !== ownedCheckoutState.refName ||
+                latestStateExit.value.hasWorkingTreeChanges ||
+                latestStateExit.value.hasWorkingTreeChanges !==
+                  ownedCheckoutState.hasWorkingTreeChanges ||
+                latestStateExit.value.commitSha !== ownedCheckoutState.commitSha ||
+                anotherOwner ||
+                !callerStillInitial
+              ) {
+                return "not_possible" as const;
+              }
+              const checkoutChanged =
+                latestStateExit.value.refName !== targetBefore.refName ||
+                latestStateExit.value.commitSha !== targetBeforeCommit.commitSha;
+              if (checkoutChanged && targetBefore.refName === null) {
+                return "not_possible" as const;
+              }
+              const rollbackExit = yield* Effect.exit(
+                checkoutChanged
+                  ? gitWorkflow.switchRef({
+                      cwd: targetWorkspacePath,
+                      refName: targetBefore.refName!,
+                    })
+                  : Effect.void,
+              );
+              return Exit.isSuccess(rollbackExit) ? ("rolled_back" as const) : ("failed" as const);
+            },
+          );
+
+          if (shouldMutateCheckout && requestedBranch !== undefined) {
+            const [mutationProjection, mutationBindings, mutationTargetBefore] = yield* restore(
+              Effect.all(
+                [
+                  loadThread(scope),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  readWorkspaceStatus(targetWorkspacePath),
+                ],
+                { concurrency: 3 },
+              ),
+            );
+            if (
+              mutationProjection.thread.branch !== projection.thread.branch ||
+              mutationProjection.thread.worktreePath !== projection.thread.worktreePath ||
+              mutationProjection.thread.archivedAt !== projection.thread.archivedAt ||
+              mutationProjection.thread.deletedAt !== projection.thread.deletedAt ||
+              mutationTargetBefore.refName !== targetBefore.refName ||
+              mutationTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+            ) {
+              return yield* failure(
+                "checkout_in_progress",
+                `Thread or checkout state changed immediately before Git mutation. Retry from the current workspace state.`,
+              );
+            }
+            const mutationOtherBindings = mutationBindings
+              .filter(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              )
+              .map(([thread]) => thread);
+            const mutationActiveBinding = mutationOtherBindings.find(
+              (thread) => thread.activeRunId !== null,
+            );
+            if (mutationActiveBinding !== undefined) {
+              return yield* failure(
+                "workspace_in_use",
+                `Checkout '${targetWorkspacePath}' became active for thread '${mutationActiveBinding.id}' (${mutationActiveBinding.title}) before Git mutation.`,
+              );
+            }
+            if (mutationOtherBindings.length > 0) {
+              return yield* failure(
+                "workspace_shared",
+                `Checkout '${targetWorkspacePath}' became bound to thread '${mutationOtherBindings[0]!.id}' before Git mutation.`,
+              );
+            }
+            if (createBranch) {
+              yield* gitWorkflow
+                .createRef({
+                  cwd: targetWorkspacePath,
+                  refName: requestedBranch,
+                  switchRef: false,
+                })
+                .pipe(asOperationFailed(`Unable to create branch '${requestedBranch}'`));
+            }
+            const switchExit = yield* Effect.exit(
+              gitWorkflow.switchRef({ cwd: targetWorkspacePath, refName: requestedBranch }),
+            );
+            if (Exit.isFailure(switchExit)) {
+              const afterFailedSwitchExit = yield* Effect.exit(captureCheckoutState());
+              if (Exit.isSuccess(afterFailedSwitchExit)) {
+                const observed = afterFailedSwitchExit.value;
+                const unchanged =
+                  observed.refName === targetBefore.refName &&
+                  observed.commitSha === targetBeforeCommit?.commitSha;
+                const requestedState =
+                  !observed.hasWorkingTreeChanges &&
+                  requestedTransitionCommit !== null &&
+                  observed.commitSha === requestedTransitionCommit.commitSha &&
+                  (selectedRef?.isRemote === true
+                    ? observed.refName === null
+                    : observed.refName === requestedBranch);
+                if (unchanged || requestedState) {
+                  ownedCheckoutState = observed;
+                }
+              }
+              const rollback = yield* rollbackOwnedCheckout();
+              if (rollback === "not_possible" || rollback === "failed") {
+                return yield* failure(
+                  "partial_failure",
+                  `Branch checkout failed and rollback was ${rollback === "failed" ? "unsuccessful" : "unsafe"}: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                  {
+                    workspacePath: targetWorkspacePath,
+                    recordedBranch: projection.thread.branch,
+                    actualBranch: Exit.isSuccess(afterFailedSwitchExit)
+                      ? afterFailedSwitchExit.value.refName
+                      : null,
+                    rollback: rollback === "failed" ? "failed" : "not_possible",
+                  },
+                );
+              }
+              return yield* failure(
+                "operation_failed",
+                `Unable to check out '${requestedBranch}': ${errorMessage(Cause.squash(switchExit.cause))}`,
+              );
+            }
+            resolvedBranch = switchExit.value.refName;
+            if (resolvedBranch === null && selectedRef?.isRemote !== true) {
+              return yield* failure(
+                "partial_failure",
+                `Git reported a detached checkout after selecting '${requestedBranch}'. The durable thread binding was not changed.`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: null,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            checkoutAction = createBranch ? "created" : "switched";
M
macroscopeappBotSep 5, 2026

🟑 Medium mcp/WorktreeMcpService.ts:1715

After a successful branch switch, checkout leaves VcsStatusBroadcaster's cached status unchanged, so subscribers and later getStatus calls can continue reporting the previous branch/PR status until another refresh occurs. The normal vcsSwitchRef path refreshes this cache; call vcsStatusBroadcaster.refreshStatus(targetWorkspacePath) after the successful checkout here as well.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 1715:
2
3After a successful branch switch, `checkout` leaves `VcsStatusBroadcaster`'s cached status unchanged, so subscribers and later `getStatus` calls can continue reporting the previous branch/PR status until another refresh occurs. The normal `vcsSwitchRef` path refreshes this cache; call `vcsStatusBroadcaster.refreshStatus(targetWorkspacePath)` after the successful checkout here as well.
J
Julius MarmingeSep 5, 2026

Fixed in 48384161a148da3e5965a62a47ed586b2bd37f03. After a switched or newly-created checkout is durably bound, the service starts the existing broadcaster refresh as an ignored detached effect. Refresh failure therefore cannot relabel a committed checkout as failed. Focused upstack coverage passes 115/115.

docs/user/source-control.md Β· Earlier revision 69afd25d Β· right 84–86

Original diff
@@ -65,16 +65,27 @@ The **Source Control settings** page shows you exactly what's connected:
 
 Run a quick **Rescan** after setting up a new machine or changing credentials.
 
-### Let an agent inspect its checkout
+### Let an agent change its checkout
 
 Agents running through T3 Code can inspect the checkout recorded on their thread and compare it
 with Git's actual branch. They can also list the project root and Git-registered worktrees,
 including detached checkouts, dirty state, and the durable branch and worktree path recorded for
 other threads using each checkout. Git resolves symlinked checkout paths through the repository's
 real common-directory and physical-worktree identity, including when a project opens in a nested
-folder. Worktree results are paginated, and missing or unreadable checkouts are
-reported without hiding the rest. These read paths apply only to the calling thread's current
-project and do not create, remove, prune, or revive worktrees.
+folder. Worktree results are paginated, and missing or unreadable checkouts are reported without
+hiding the rest.
+
+An agent can move its current thread to an existing branch, return to the project root, reuse an
+unclaimed worktree, or create a new worktree. T3 Code performs the Git operation before it updates
+the thread's saved branch and worktree path. It refuses to switch a dirty checkout and will not
+silently stash or discard files. It also refuses to take over a worktree owned by another thread or
+switch a shared root while another thread is bound there.
+
+Moving between workspace paths restarts the agent session in the selected checkout. The agent can
+queue a continuation before that restart, so longer work resumes without needing the browser to
+stay open. These controls apply only to the calling thread and its current project. They do not

docs/user/source-control.md Β· right 84–86

M
macroscopeappBotAug 30, 2026

🟒 Low user/source-control.md:84

The documentation incorrectly says every workspace-path move restarts the agent session, but a path-only checkout ends the current turn unless continuationPrompt is supplied; users must send another message to continue. Please document the conditional continuation behavior.

suggestion
1Moving between workspace paths ends the current agent turn in the selected checkout. When the agent queues a continuation before that transition, the session resumes without needing the browser to stay open.
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @docs/user/source-control.md around lines 84-86:
2
3The documentation incorrectly says every workspace-path move restarts the agent session, but a path-only checkout ends the current turn unless `continuationPrompt` is supplied; users must send another message to continue. Please document the conditional continuation behavior.
J
Julius MarmingeAug 30, 2026

Fixed in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284. The user guide now says a workspace-path move ends the current turn; continuationPrompt queues a replacement turn, while an omitted continuation leaves the thread idle until its next message. The internal MCP documentation uses the same conditional wording.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 1d545798 Β· right 1664

Original diff
@@ -759,7 +983,851 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActualBranch = yield* readWorkspaceBranchOrNull(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActualBranch,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActualBranch !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorkspaceBindingInventory(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (
+      Option.isSome(currentInventory) &&
+      inventory.repositoryCommonDir !== currentInventory.value.repositoryCommonDir
+    ) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = Option.isSome(currentInventory)
+      ? currentInventory.value.currentWorktreeRoot
+      : null;
+    if (projectWorktreeRoot === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project checkout.",
+      );
+    }
+    if (Option.isSome(currentInventory) && currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        Option.isNone(currentInventory)
+          ? Effect.succeed({
+              isRepo: false,
+              refName: null,
+              hasWorkingTreeChanges: false,
+            })
+          : readWorkspaceStatus(recordedWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        if (requestedBranch !== undefined) {
+          selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+          if (createBranch && localRefByName.has(requestedBranch)) {
+            return yield* failure(
+              "invalid_request",
+              `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+            );
+          }
+          if (!createBranch && selectedRef === undefined) {
+            return yield* failure(
+              "invalid_request",
+              `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the project root.`,
+            );
+          }
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? (currentWorkspacePath ?? recordedWorkspacePath)
+              : (selectedWorktreePath ??
+                (currentWorkspacePath === null ||
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true)
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      Effect.gen(function* () {
+        const recordedPath = yield* threadWorkspacePath(thread, projectWorktreeRoot);
+        const recordedInventory = yield* loadWorkspaceBindingInventory(recordedPath);
+        const workspacePath = Option.match(recordedInventory, {
+          onNone: () => recordedPath,
+          onSome: (candidateInventory) =>
+            candidateInventory.repositoryCommonDir === inventory.repositoryCommonDir &&
+            candidateInventory.currentWorktreeRoot !== null
+              ? candidateInventory.currentWorktreeRoot
+              : recordedPath,
+        });
+        return [thread, workspacePath] as const;
+      }),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = `workspace:${inventory.repositoryCommonDir}:${targetWorkspacePath}`;
+    return yield* Effect.uninterruptibleMask(() =>
+      Effect.suspend(() => {
+        if (workspaceTransitionsInFlight.has(workspaceGuardKey)) {
+          return Effect.fail(
+            failure(
+              "checkout_in_progress",
+              `Another workspace transition is already in progress for '${targetWorkspacePath}'.`,
+            ),
+          );
+        }
+        workspaceTransitionsInFlight.add(workspaceGuardKey);
+        return Effect.gen(function* () {
+          const [latestProjection, latestInventory, latestBindings, latestTargetBefore] =
+            yield* Effect.all(
+              [
+                loadThread(scope),
+                loadWorktrees(projectWorkspaceRoot),
+                loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                readWorkspaceStatus(targetWorkspacePath),
+              ],
+              { concurrency: 4 },
+            );
+          if (
+            latestProjection.thread.branch !== projection.thread.branch ||
+            latestProjection.thread.worktreePath !== projection.thread.worktreePath ||
+            latestProjection.thread.archivedAt !== projection.thread.archivedAt
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Thread '${scope.threadId}' changed workspace state while checkout was being prepared. Retry from its current binding.`,
+            );
+          }
+          if (
+            latestInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+            !latestInventory.worktrees.some((worktree) => worktree.path === targetWorkspacePath)
+          ) {
+            return yield* failure(
+              "scope_mismatch",
+              `Checkout target '${targetWorkspacePath}' is no longer registered in the calling thread's Git repository.`,
+            );
+          }
+          if (
+            latestTargetBefore.refName !== targetBefore.refName ||
+            latestTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Checkout '${targetWorkspacePath}' changed while the transition was being prepared. Retry from its current Git state.`,
+            );
+          }
+          if (shouldMutateCheckout && latestTargetBefore.hasWorkingTreeChanges) {
+            return yield* failure(
+              "dirty_workspace",
+              `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+            );
+          }
+          const latestOtherBindings = latestBindings
+            .filter(
+              ([thread, workspacePath]) =>
+                thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+            )
+            .map(([thread]) => thread);
+          const latestActiveBinding = latestOtherBindings.find(
+            (thread) => thread.activeRunId !== null,
+          );
+          if (
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestActiveBinding
+          ) {
+            return yield* failure(
+              "workspace_in_use",
+              `Checkout '${targetWorkspacePath}' is in use by active thread '${latestActiveBinding.id}' (${latestActiveBinding.title}).`,
+            );
+          }
+          if (
+            targetWorkspacePath !== projectWorktreeRoot &&
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `Worktree '${targetWorkspacePath}' is already bound to thread '${latestOtherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+            );
+          }
+          if (
+            targetWorkspacePath === projectWorktreeRoot &&
+            shouldMutateCheckout &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `The project root is also bound to thread '${latestOtherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+            );
+          }
+
+          let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+            targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+          let resolvedBranch: string | null = targetBefore.refName;
+          const targetBeforeCommit = shouldMutateCheckout
+            ? yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to record the checkout's current commit"))
+            : null;
+          const requestedTransitionCommit =
+            shouldMutateCheckout && requestedBranch !== undefined
+              ? createBranch
+                ? targetBeforeCommit
+                : yield* gitWorkflow
+                    .resolveCommit({ cwd: targetWorkspacePath, revision: requestedBranch })
+                    .pipe(asOperationFailed(`Unable to resolve ref '${requestedBranch}'`))
+              : null;
+          let ownedCheckoutState: {
+            readonly refName: string | null;
+            readonly hasWorkingTreeChanges: boolean;
+            readonly commitSha: string;
+          } | null = null;
+
+          const captureCheckoutState = Effect.fn("WorktreeMcpService.captureCheckoutState")(
+            function* () {
+              const status = yield* readWorkspaceStatus(targetWorkspacePath);
+              const commit = yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to identify the checkout's current commit"));
+              return {
+                refName: status.refName,
+                hasWorkingTreeChanges: status.hasWorkingTreeChanges,
+                commitSha: commit.commitSha,
+              };
+            },
+          );
+
+          const rollbackOwnedCheckout = Effect.fn("WorktreeMcpService.rollbackOwnedCheckout")(
+            function* () {
+              if (!shouldMutateCheckout) {
+                return "not_needed" as const;
+              }
+              if (ownedCheckoutState === null || targetBeforeCommit === null) {
+                return "not_possible" as const;
+              }
+              const [latestStateExit, latestBindings, callerProjectionExit] = yield* Effect.all(
+                [
+                  Effect.exit(captureCheckoutState()),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  Effect.exit(threadManagement.getThreadProjection(scope.threadId)),
+                ],
+                { concurrency: 3 },
+              );
+              const anotherOwner = latestBindings.some(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              );
+              const callerStillInitial =
+                Exit.isSuccess(callerProjectionExit) &&
+                callerProjectionExit.value.thread.branch === projection.thread.branch &&
+                callerProjectionExit.value.thread.worktreePath === projection.thread.worktreePath &&
+                callerProjectionExit.value.thread.archivedAt === projection.thread.archivedAt &&
+                callerProjectionExit.value.thread.deletedAt === projection.thread.deletedAt;
+              if (
+                Exit.isFailure(latestStateExit) ||
+                latestStateExit.value.refName !== ownedCheckoutState.refName ||
+                latestStateExit.value.hasWorkingTreeChanges ||
+                latestStateExit.value.hasWorkingTreeChanges !==
+                  ownedCheckoutState.hasWorkingTreeChanges ||
+                latestStateExit.value.commitSha !== ownedCheckoutState.commitSha ||
+                anotherOwner ||
+                !callerStillInitial
+              ) {
+                return "not_possible" as const;
+              }
+              const checkoutChanged =
+                latestStateExit.value.refName !== targetBefore.refName ||
+                latestStateExit.value.commitSha !== targetBeforeCommit.commitSha;
+              if (checkoutChanged && targetBefore.refName === null) {
+                return "not_possible" as const;
+              }
+              const rollbackExit = yield* Effect.exit(
+                checkoutChanged
+                  ? gitWorkflow.switchRef({
+                      cwd: targetWorkspacePath,
+                      refName: targetBefore.refName!,
+                    })
+                  : Effect.void,
+              );
+              return Exit.isSuccess(rollbackExit) ? ("rolled_back" as const) : ("failed" as const);
+            },
+          );
+
+          if (shouldMutateCheckout && requestedBranch !== undefined) {
+            const [mutationProjection, mutationBindings, mutationTargetBefore] = yield* Effect.all(
+              [
+                loadThread(scope),
+                loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                readWorkspaceStatus(targetWorkspacePath),
+              ],
+              { concurrency: 3 },
+            );
+            if (
+              mutationProjection.thread.branch !== projection.thread.branch ||
+              mutationProjection.thread.worktreePath !== projection.thread.worktreePath ||
+              mutationProjection.thread.archivedAt !== projection.thread.archivedAt ||
+              mutationProjection.thread.deletedAt !== projection.thread.deletedAt ||
+              mutationTargetBefore.refName !== targetBefore.refName ||
+              mutationTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+            ) {
+              return yield* failure(
+                "checkout_in_progress",
+                `Thread or checkout state changed immediately before Git mutation. Retry from the current workspace state.`,
+              );
+            }
+            const mutationOtherBindings = mutationBindings
+              .filter(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              )
+              .map(([thread]) => thread);
+            const mutationActiveBinding = mutationOtherBindings.find(
+              (thread) => thread.activeRunId !== null,
+            );
+            if (mutationActiveBinding !== undefined) {
+              return yield* failure(
+                "workspace_in_use",
+                `Checkout '${targetWorkspacePath}' became active for thread '${mutationActiveBinding.id}' (${mutationActiveBinding.title}) before Git mutation.`,
+              );
+            }
+            if (mutationOtherBindings.length > 0) {
+              return yield* failure(
+                "workspace_shared",
+                `Checkout '${targetWorkspacePath}' became bound to thread '${mutationOtherBindings[0]!.id}' before Git mutation.`,
+              );
+            }
+            if (createBranch) {
+              yield* gitWorkflow
+                .createRef({
+                  cwd: targetWorkspacePath,
+                  refName: requestedBranch,
+                  switchRef: false,
+                })
+                .pipe(asOperationFailed(`Unable to create branch '${requestedBranch}'`));
+            }
+            const switchExit = yield* Effect.exit(
+              gitWorkflow.switchRef({ cwd: targetWorkspacePath, refName: requestedBranch }),
+            );
+            if (Exit.isFailure(switchExit)) {
+              const afterFailedSwitchExit = yield* Effect.exit(captureCheckoutState());
+              if (Exit.isSuccess(afterFailedSwitchExit)) {
+                const observed = afterFailedSwitchExit.value;
+                const unchanged =
+                  observed.refName === targetBefore.refName &&
+                  observed.commitSha === targetBeforeCommit?.commitSha;
+                const requestedState =
+                  !observed.hasWorkingTreeChanges &&
+                  requestedTransitionCommit !== null &&
+                  observed.commitSha === requestedTransitionCommit.commitSha &&
+                  (selectedRef?.isRemote === true
+                    ? observed.refName === null
+                    : observed.refName === requestedBranch);
+                if (unchanged || requestedState) {
+                  ownedCheckoutState = observed;
+                }
+              }
+              const rollback = yield* rollbackOwnedCheckout();
+              if (rollback === "not_possible" || rollback === "failed") {
+                return yield* failure(
+                  "partial_failure",
+                  `Branch checkout failed and rollback was ${rollback === "failed" ? "unsuccessful" : "unsafe"}: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                  {
+                    workspacePath: targetWorkspacePath,
+                    recordedBranch: projection.thread.branch,
+                    actualBranch: Exit.isSuccess(afterFailedSwitchExit)
+                      ? afterFailedSwitchExit.value.refName
+                      : null,
+                    rollback: rollback === "failed" ? "failed" : "not_possible",
+                  },
+                );
+              }
+              return yield* failure(
+                "operation_failed",
+                `Unable to check out '${requestedBranch}': ${errorMessage(Cause.squash(switchExit.cause))}`,
+              );
+            }
+            resolvedBranch = switchExit.value.refName;
+            if (resolvedBranch === null && selectedRef?.isRemote !== true) {
+              return yield* failure(
+                "partial_failure",
+                `Git reported a detached checkout after selecting '${requestedBranch}'. The durable thread binding was not changed.`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: null,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            checkoutAction = createBranch ? "created" : "switched";
+          }
+
+          const actualExit = yield* Effect.exit(readWorkspaceStatus(targetWorkspacePath));
+          if (Exit.isFailure(actualExit)) {
+            const detail = errorMessage(Cause.squash(actualExit.cause));
+            if (checkoutAction === "switched" || checkoutAction === "created") {
+              return yield* failure(
+                "partial_failure",
+                `Git checkout completed but its resulting state could not be verified, so rollback was not attempted: ${detail}`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: null,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            return yield* failure(
+              "operation_failed",
+              `Unable to verify the selected checkout '${targetWorkspacePath}': ${detail}`,
+            );
+          }
+          const actual = actualExit.value;
+          if (
+            (checkoutAction === "switched" || checkoutAction === "created") &&
+            actual.refName !== resolvedBranch
+          ) {
+            return yield* failure(
+              "partial_failure",
+              `Git resolved '${requestedBranch}' to '${resolvedBranch}' but the checkout now reports '${actual.refName ?? "detached HEAD"}'. The durable thread binding was not changed.`,
+              {
+                workspacePath: targetWorkspacePath,
+                recordedBranch: projection.thread.branch,
+                actualBranch: actual.refName,
+                rollback: "not_possible",
+              },
+            );
+          }
+          if (checkoutAction === "switched" || checkoutAction === "created") {
+            const actualCommitExit = yield* Effect.exit(
+              gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to identify the selected checkout commit")),
+            );
+            if (Exit.isFailure(actualCommitExit)) {
+              return yield* failure(
+                "partial_failure",
+                "Git checkout completed but its commit identity could not be verified, so the durable binding was not changed and rollback was not attempted.",
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: actual.refName,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            if (
+              requestedTransitionCommit !== null &&
+              actualCommitExit.value.commitSha !== requestedTransitionCommit.commitSha
+            ) {
+              return yield* failure(
+                "partial_failure",
+                `Git selected '${requestedBranch}', but HEAD no longer matches the resolved target commit. The durable binding was not changed.`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: actual.refName,
+                  rollback: "not_possible",
+                },
+              );
+            }
+            ownedCheckoutState = {
+              refName: actual.refName,
+              hasWorkingTreeChanges: actual.hasWorkingTreeChanges,
+              commitSha: actualCommitExit.value.commitSha,
+            };
+          }
+          const nextBranch = actual.refName;
+          const workspaceChanged = targetWorkspacePath !== currentWorkspacePath;
+          const nextWorktreePath = workspaceChanged
+            ? targetWorkspacePath === projectWorktreeRoot
+              ? null
+              : targetWorkspacePath
+            : projection.thread.worktreePath;
+          const bindingChanged =
+            nextBranch !== projection.thread.branch ||
+            nextWorktreePath !== projection.thread.worktreePath;
C
cursorBotAug 30, 2026

Project-root binding not normalized

Medium Severity

A project_root target only writes worktreePath: null when the physical checkout changes. If the recorded path is a nested directory or alias of the project root, workspaceChanged is false and the durable binding keeps the old path. Later turns can keep using that nested cwd even though the caller asked to return to the main checkout.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 1d545798de018346d5a02146a79727b602844f6d. Configure here.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 69afd25d Β· right 1284

Original diff
@@ -759,7 +983,851 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActualBranch = yield* readWorkspaceBranchOrNull(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActualBranch,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActualBranch !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorkspaceBindingInventory(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (
+      Option.isSome(currentInventory) &&
+      inventory.repositoryCommonDir !== currentInventory.value.repositoryCommonDir
+    ) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = Option.isSome(currentInventory)
+      ? currentInventory.value.currentWorktreeRoot
+      : null;
+    if (projectWorktreeRoot === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project checkout.",
+      );
+    }
+    if (Option.isSome(currentInventory) && currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        Option.isNone(currentInventory)
+          ? Effect.succeed({
+              isRepo: false,
+              refName: null,
+              hasWorkingTreeChanges: false,
+            })
+          : readWorkspaceStatus(recordedWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        if (requestedBranch !== undefined) {
+          selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+          if (createBranch && localRefByName.has(requestedBranch)) {
+            return yield* failure(
+              "invalid_request",
+              `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+            );
+          }
+          if (!createBranch && selectedRef === undefined) {
+            return yield* failure(
+              "invalid_request",
+              `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the project root.`,
+            );
+          }
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? (currentWorkspacePath ?? recordedWorkspacePath)
+              : (selectedWorktreePath ??
+                (currentWorkspacePath === null ||
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true)
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      Effect.gen(function* () {
+        const recordedPath = yield* threadWorkspacePath(thread, projectWorktreeRoot);
+        const recordedInventory = yield* loadWorkspaceBindingInventory(recordedPath);
+        const workspacePath = Option.match(recordedInventory, {
+          onNone: () => recordedPath,
+          onSome: (candidateInventory) =>
+            candidateInventory.repositoryCommonDir === inventory.repositoryCommonDir &&
+            candidateInventory.currentWorktreeRoot !== null
+              ? candidateInventory.currentWorktreeRoot
+              : recordedPath,
+        });
+        return [thread, workspacePath] as const;
+      }),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = `workspace:${inventory.repositoryCommonDir}:${targetWorkspacePath}`;
+    return yield* Effect.uninterruptibleMask(() =>
M
macroscopeappBotAug 30, 2026

🟠 High mcp/WorktreeMcpService.ts:1284

performCheckout executes the entire guarded transition inside Effect.uninterruptibleMask without calling restore, so Git, status, inventory, and dispatch operations ignore cancellation. A stalled or abandoned request therefore keeps the per-thread and per-workspace guard entries until completion, causing subsequent calls to fail with checkout_in_progress. Use restore around the transition after acquiring the guard and registering its finalizer, as handoff does.

βœ… Resolved in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284

J
Julius MarmingeAug 30, 2026

Fixed in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284 with a narrow cancellation boundary. Projection, inventory, owner, status, and ref-resolution preflight readsβ€”including the final mutation-adjacent recheckβ€”are interruptible. Once that recheck passes, Git mutation, durable binding/continuation, and ownership-proven rollback remain uninterruptible. The Deferred regression requests interruption synchronously before releasing the read gate, proves zero Git mutation/dispatch, and proves a later checkout succeeds after both guards are released. The focused WorktreeMcpService suite passes 117/117.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 5ef4d4c3 Β· right 1329

Original diff
@@ -691,7 +880,744 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActual.refName,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActual.refName !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorktrees(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (inventory.repositoryCommonDir !== currentInventory.repositoryCommonDir) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = currentInventory.currentWorktreeRoot;
+    if (projectWorktreeRoot === null || currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project or thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        readWorkspaceStatus(currentWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? currentWorkspacePath
+              : (selectedWorktreePath ??
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      threadWorkspacePath(thread, projectWorktreeRoot).pipe(
+        Effect.map((workspacePath) => [thread, workspacePath] as const),
+      ),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = `workspace:${inventory.repositoryCommonDir}:${targetWorkspacePath}`;
+    return yield* Effect.uninterruptibleMask(() =>
+      Effect.suspend(() => {
+        if (workspaceTransitionsInFlight.has(workspaceGuardKey)) {
+          return Effect.fail(
+            failure(
+              "checkout_in_progress",
+              `Another workspace transition is already in progress for '${targetWorkspacePath}'.`,
+            ),
+          );
+        }
+        workspaceTransitionsInFlight.add(workspaceGuardKey);
+        return Effect.gen(function* () {
+          const [latestProjection, latestInventory, latestBindings, latestTargetBefore] =
+            yield* Effect.all(
+              [
+                loadThread(scope),
+                loadWorktrees(projectWorkspaceRoot),
+                loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                readWorkspaceStatus(targetWorkspacePath),
+              ],
+              { concurrency: 4 },
+            );
+          if (
+            latestProjection.thread.branch !== projection.thread.branch ||
+            latestProjection.thread.worktreePath !== projection.thread.worktreePath ||
+            latestProjection.thread.archivedAt !== projection.thread.archivedAt
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Thread '${scope.threadId}' changed workspace state while checkout was being prepared. Retry from its current binding.`,
+            );
+          }
+          if (
+            latestInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+            !latestInventory.worktrees.some((worktree) => worktree.path === targetWorkspacePath)
+          ) {
+            return yield* failure(
+              "scope_mismatch",
+              `Checkout target '${targetWorkspacePath}' is no longer registered in the calling thread's Git repository.`,
+            );
+          }
+          if (
+            latestTargetBefore.refName !== targetBefore.refName ||
+            latestTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Checkout '${targetWorkspacePath}' changed while the transition was being prepared. Retry from its current Git state.`,
+            );
+          }
+          if (shouldMutateCheckout && latestTargetBefore.hasWorkingTreeChanges) {
+            return yield* failure(
+              "dirty_workspace",
+              `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+            );
+          }
+          const latestOtherBindings = latestBindings
+            .filter(
+              ([thread, workspacePath]) =>
+                thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+            )
+            .map(([thread]) => thread);
+          const latestActiveBinding = latestOtherBindings.find(
+            (thread) => thread.activeRunId !== null,
+          );
+          if (
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestActiveBinding
+          ) {
+            return yield* failure(
+              "workspace_in_use",
+              `Checkout '${targetWorkspacePath}' is in use by active thread '${latestActiveBinding.id}' (${latestActiveBinding.title}).`,
+            );
+          }
+          if (
+            targetWorkspacePath !== projectWorktreeRoot &&
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `Worktree '${targetWorkspacePath}' is already bound to thread '${latestOtherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+            );
+          }
+          if (
+            targetWorkspacePath === projectWorktreeRoot &&
+            shouldMutateCheckout &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `The project root is also bound to thread '${latestOtherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+            );
+          }
+
+          let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+            targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+          let createdBranch: string | null = null;
+          let resolvedBranch: string | null = targetBefore.refName;
+
+          if (shouldMutateCheckout && requestedBranch !== undefined) {
+            if (createBranch) {
+              yield* gitWorkflow
+                .createRef({
+                  cwd: targetWorkspacePath,
+                  refName: requestedBranch,
+                  switchRef: false,
+                })
+                .pipe(asOperationFailed(`Unable to create branch '${requestedBranch}'`));
+              createdBranch = requestedBranch;
+            }
+            const switchExit = yield* Effect.exit(
+              gitWorkflow.switchRef({ cwd: targetWorkspacePath, refName: requestedBranch }),
+            );
+            if (Exit.isFailure(switchExit)) {
+              const afterFailedSwitchExit = yield* Effect.exit(
+                readWorkspaceStatus(targetWorkspacePath),
+              );
+              if (Exit.isFailure(afterFailedSwitchExit)) {
+                return yield* failure(
+                  "partial_failure",
+                  `Branch checkout failed and the resulting Git state could not be verified: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                  {
+                    workspacePath: targetWorkspacePath,
+                    recordedBranch: projection.thread.branch,
+                    actualBranch: null,
+                    rollback: "not_possible",
+                  },
+                );
+              }
+              const afterFailedSwitch = afterFailedSwitchExit.value;
+              const checkoutChanged = afterFailedSwitch.refName !== targetBefore.refName;
+              if (checkoutChanged && targetBefore.refName === null) {
+                return yield* failure(
+                  "partial_failure",
+                  `Branch checkout failed after changing Git state and the previous detached ref cannot be restored automatically: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                  {
+                    workspacePath: targetWorkspacePath,
+                    recordedBranch: projection.thread.branch,
+                    actualBranch: afterFailedSwitch.refName,
+                    rollback: "not_possible",
+                  },
+                );
+              }
+              const rollback = checkoutChanged
+                ? gitWorkflow.switchRef({
+                    cwd: targetWorkspacePath,
+                    refName: targetBefore.refName!,
+                  })
+                : Effect.void;
+              const cleanup = rollback.pipe(
+                Effect.andThen(
+                  createdBranch === null
+                    ? Effect.void
+                    : gitWorkflow.deleteLocalBranch({
+                        cwd: targetWorkspacePath,
+                        refName: createdBranch,
+                        force: true,
+                      }),
+                ),
+              );
+              const cleanupExit = yield* Effect.exit(cleanup);
+              if (Exit.isFailure(cleanupExit)) {
+                const actualBranch = yield* readWorkspaceBranchOrNull(targetWorkspacePath);
+                return yield* failure(
+                  "partial_failure",
+                  `Branch checkout failed and rollback also failed: ${errorMessage(Cause.squash(switchExit.cause))}`,
+                  {
+                    workspacePath: targetWorkspacePath,
+                    recordedBranch: projection.thread.branch,
+                    actualBranch,
+                    rollback: "failed",
+                  },
+                );
+              }
+              return yield* failure(
+                "operation_failed",
+                `Unable to check out '${requestedBranch}': ${errorMessage(Cause.squash(switchExit.cause))}`,
+              );
+            }
+            resolvedBranch = switchExit.value.refName;
+            if (resolvedBranch === null) {
+              return yield* failure(
+                "partial_failure",
+                `Git reported a detached checkout after selecting '${requestedBranch}'. The durable thread binding was not changed.`,
+                {
+                  workspacePath: targetWorkspacePath,
+                  recordedBranch: projection.thread.branch,
+                  actualBranch: null,
+                  rollback: "not_possible",
+                },
+              );
C
cursorBotAug 29, 2026

Detached remote checkout skips rollback

High Severity

Checking out a remote ref can succeed as a detached HEAD. switchRef then returns refName of null, which performCheckout reports as partial_failure with rollback: "not_possible" without restoring the previous branch. Git is left detached while the durable binding is unchanged. This is the documented real-Git outcome when origin/feature is requested and a same-named untracked local branch exists.

Reviewed by Cursor Bugbot for commit 5ef4d4c3499e4b6114850f44717f98ebc9d60a93. Configure here.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision 05f8e645 Β· right 728

Original diff
@@ -533,7 +725,7 @@ const make = Effect.gen(function* () {
         ? workspaceInventory.value.currentWorktreeRoot
         : null;
       const agreement =
-        !actual.isRepo && !workspaceExists
+        !actual.isRepo && !workspaceExists && Option.isNone(workspaceInventory)
C
cursorBotAug 29, 2026

List omits archived checkout owners

Medium Severity

listWorktrees builds bindings only from listProjectThreads, which reads snapshot.threads and ignores archivedThreads. Checkout now treats an archived binding as occupying the physical worktree, so list can report available with no owners for a checkout that t3_thread_checkout will reject as workspace_shared.

Reviewed by Cursor Bugbot for commit 05f8e6450db277e831f923c4c278f790716a9090. Configure here.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision a671e359 Β· right 776–776

Original diff
@@ -463,21 +631,654 @@ const make = Effect.gen(function* () {
       yield* requireCapability(scope);
       const projection = yield* loadThread(scope);
       const project = yield* loadProject(scope, projection.thread.projectId);
-
-      const defaultStartFromOrigin = yield* readDefaultStartFromOrigin;
+      const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+      const workspacePath = normalizePath(projection.thread.worktreePath ?? projectWorkspaceRoot);
+      const [defaultStartFromOrigin, actual, refs] = yield* Effect.all(
+        [
+          readDefaultStartFromOrigin,
+          readWorkspaceStatus(workspacePath),
+          loadRefs(projectWorkspaceRoot, "local"),
+        ],
+        { concurrency: 3 },
+      );
+      const knownWorkspacePaths = new Set([
+        projectWorkspaceRoot,
+        ...refs.flatMap((ref) =>
+          ref.isRemote === true || ref.worktreePath === null
+            ? []
+            : [normalizePath(ref.worktreePath)],
+        ),
+      ]);
+      const agreement = !knownWorkspacePaths.has(workspacePath)
+        ? "workspace_missing"
+        : !actual.isRepo
+          ? "not_repository"
+          : actual.refName !== projection.thread.branch
+            ? "branch_mismatch"
+            : "in_sync";
 
       const result: WorktreeMcpStatusResult = {
         attached: projection.thread.worktreePath !== null,
         worktreePath: projection.thread.worktreePath,
         branch: projection.thread.branch,
-        projectWorkspaceRoot: project.workspaceRoot,
+        projectWorkspaceRoot,
         defaultStartFromOrigin,
+        recordedWorkspace: {
+          branch: projection.thread.branch,
+          worktreePath: projection.thread.worktreePath,
+        },
+        actualWorkspace: {
+          workspacePath,
+          isRepo: actual.isRepo,
+          branch: actual.refName,
+          hasWorkingTreeChanges: actual.hasWorkingTreeChanges,
+        },
+        agreement,
       };
       return result;
     },
   );
 
-  return WorktreeMcpService.of({ handoff, status });
+  const listWorktrees: WorktreeMcpService["Service"]["listWorktrees"] = Effect.fn(
+    "WorktreeMcpService.listWorktrees",
+  )(function* (scope) {
+    yield* requireCapability(scope);
+    const projection = yield* loadThread(scope);
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+    const [refs, threads] = yield* Effect.all(
+      [loadRefs(projectWorkspaceRoot, "local"), loadProjectThreads(projection.thread.projectId)],
+      { concurrency: 2 },
+    );
+
+    const branchByWorkspacePath = new Map<string, string | null>([[projectWorkspaceRoot, null]]);
+    for (const ref of refs) {
+      if (ref.isRemote === true || ref.worktreePath === null) continue;
+      branchByWorkspacePath.set(normalizePath(ref.worktreePath), ref.name);
+    }
+
+    const worktrees = yield* Effect.forEach(
+      [...branchByWorkspacePath.entries()],
+      ([workspacePath, branch]) =>
+        readWorkspaceStatus(workspacePath).pipe(
+          Effect.map((actual) => ({
+            path: workspacePath,
+            branch,
+            actualBranch: actual.refName,
+            isRepo: actual.isRepo,
+            isProjectRoot: workspacePath === projectWorkspaceRoot,
+            hasWorkingTreeChanges: actual.hasWorkingTreeChanges,
+            bindings: threads
+              .filter(
+                (thread) => threadWorkspacePath(thread, projectWorkspaceRoot) === workspacePath,
+              )
+              .map((thread) => ({
+                threadId: thread.id,
+                title: thread.title,
+                status: thread.status,
+                recordedBranch: thread.branch,
+                recordedWorktreePath: thread.worktreePath,
+                active: thread.activeRunId !== null,
+                callingThread: thread.id === scope.threadId,
+              })),
+          })),
+        ),
+      { concurrency: 8 },
+    );
+
+    return {
+      projectWorkspaceRoot,
+      worktrees: worktrees.toSorted(
+        (left, right) =>
+          Number(right.isProjectRoot) - Number(left.isProjectRoot) ||
+          left.path.localeCompare(right.path),
+      ),
+    } satisfies WorktreeMcpListResult;
+  });
+
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = normalizePath(project.workspaceRoot);
+    const currentWorkspacePath = normalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActual = yield* readWorkspaceStatus(currentWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      const actual = yield* readWorkspaceStatus(handoff.worktreePath);

apps/server/src/mcp/WorktreeMcpService.ts Β· right 776–776

M
macroscopeappBotAug 29, 2026

🟠 High mcp/WorktreeMcpService.ts:776

checkout fails after performHandoff has durably created and bound the new worktree if this readWorkspaceStatus is interrupted or fails, so the caller receives no successful result even though the session is detached and the thread now points at the new worktree. Use the branch returned by performHandoff (or otherwise make this post-commit read best-effort) so status-read failure cannot turn a completed transition into a failed checkout.

βœ… Resolved in 98151e0c2ee5b55e702164a7ae31136b96d351d9

J
Julius MarmingeAug 29, 2026

Fixed in 98151e0c2e. The new-worktree checkout result now uses the completed handoff receipt branch and no longer performs a post-commit status read that could misreport a successful durable handoff as failed. Added a regression test where the next status read would fail.

apps/server/src/mcp/WorktreeMcpService.ts Β· Earlier revision d653ad1c Β· right 1657

Original diff
@@ -759,7 +1111,862 @@ const make = Effect.gen(function* () {
     } satisfies WorktreeMcpListResult;
   });
 
-  return WorktreeMcpService.of({ handoff, status, listWorktrees });
+  const performCheckout = Effect.fn("WorktreeMcpService.performCheckout")(function* (
+    scope: McpInvocationScope,
+    input: WorktreeMcpCheckoutInput,
+  ) {
+    const projection = yield* loadThread(scope);
+    if (projection.thread.archivedAt !== null) {
+      return yield* failure(
+        "invalid_request",
+        `Thread '${scope.threadId}' is archived and cannot change workspace.`,
+      );
+    }
+
+    const project = yield* loadProject(scope, projection.thread.projectId);
+    const projectWorkspaceRoot = yield* canonicalizePath(project.workspaceRoot);
+    const recordedWorkspacePath = yield* canonicalizePath(
+      projection.thread.worktreePath ?? projectWorkspaceRoot,
+    );
+    if (input.target.type === "new_worktree") {
+      const previousActualBranch = yield* readWorkspaceBranchOrNull(recordedWorkspacePath);
+      const handoff = yield* performHandoff(
+        scope,
+        {
+          branch: input.target.branch,
+          ...(input.target.baseRef === undefined ? {} : { baseRef: input.target.baseRef }),
+          ...(input.target.startFromOrigin === undefined
+            ? {}
+            : { startFromOrigin: input.target.startFromOrigin }),
+          ...(input.target.path === undefined ? {} : { path: input.target.path }),
+          ...(input.target.runSetupScript === undefined
+            ? {}
+            : { runSetupScript: input.target.runSetupScript }),
+          ...(input.continuationPrompt === undefined
+            ? {}
+            : { continuationPrompt: input.continuationPrompt }),
+        },
+        projection,
+      );
+      return {
+        previous: {
+          workspacePath: recordedWorkspacePath,
+          recordedBranch: projection.thread.branch,
+          recordedWorktreePath: projection.thread.worktreePath,
+          actualBranch: previousActualBranch,
+        },
+        current: {
+          workspacePath: handoff.worktreePath,
+          recordedBranch: handoff.branch,
+          recordedWorktreePath: handoff.worktreePath,
+          actualBranch: handoff.branch,
+        },
+        checkoutAction: "created",
+        workspaceChanged: true,
+        branchChanged: previousActualBranch !== handoff.branch,
+        continuation: handoff.continuation,
+        setupScript: handoff.setupScript,
+        callerTurnEnds: true,
+        note: handoff.note,
+      } satisfies WorktreeMcpCheckoutResult;
+    }
+    const [inventory, currentInventory] = yield* Effect.all(
+      [loadWorktrees(projectWorkspaceRoot), loadWorkspaceBindingInventory(recordedWorkspacePath)],
+      { concurrency: 2 },
+    );
+    if (
+      Option.isSome(currentInventory) &&
+      inventory.repositoryCommonDir !== currentInventory.value.repositoryCommonDir
+    ) {
+      return yield* failure(
+        "scope_mismatch",
+        `Thread workspace '${recordedWorkspacePath}' does not belong to the calling thread's project repository.`,
+      );
+    }
+    const projectWorktreeRoot = inventory.currentWorktreeRoot;
+    const currentWorkspacePath = Option.isSome(currentInventory)
+      ? currentInventory.value.currentWorktreeRoot
+      : null;
+    if (projectWorktreeRoot === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical project checkout.",
+      );
+    }
+    if (Option.isSome(currentInventory) && currentWorkspacePath === null) {
+      return yield* failure(
+        "invalid_request",
+        "Git could not resolve the physical thread checkout.",
+      );
+    }
+    const [refs, threads, previousActual] = yield* Effect.all(
+      [
+        loadRefs(projectWorkspaceRoot),
+        loadProjectThreads(projection.thread.projectId),
+        Option.isNone(currentInventory)
+          ? Effect.succeed({
+              isRepo: false,
+              refName: null,
+              hasWorkingTreeChanges: false,
+            })
+          : readWorkspaceStatus(recordedWorkspacePath),
+      ],
+      { concurrency: 3 },
+    );
+
+    const localRefs = refs.filter((ref) => ref.isRemote !== true);
+    const workspacePaths = new Set(inventory.worktrees.map((worktree) => worktree.path));
+    const localRefByName = new Map(localRefs.map((ref) => [ref.name, ref]));
+    const remoteRefByName = new Map(
+      refs.filter((ref) => ref.isRemote === true).map((ref) => [ref.name, ref]),
+    );
+
+    let targetWorkspacePath: string;
+    let requestedBranch: string | undefined;
+    let createBranch = false;
+    let selectedRef: VcsRef | undefined;
+
+    switch (input.target.type) {
+      case "worktree": {
+        const targetInventory = yield* loadWorktrees(input.target.path);
+        targetWorkspacePath =
+          targetInventory.currentWorktreeRoot ?? (yield* canonicalizePath(input.target.path));
+        if (
+          targetInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+          targetWorkspacePath === projectWorktreeRoot ||
+          !workspacePaths.has(targetWorkspacePath)
+        ) {
+          return yield* failure(
+            "scope_mismatch",
+            targetWorkspacePath === projectWorktreeRoot
+              ? "Use target.type='project_root' to return to the project's main checkout."
+              : `Worktree '${input.target.path}' does not belong to project '${projection.thread.projectId}'. Call t3_worktree_list and choose one of its paths.`,
+          );
+        }
+        break;
+      }
+      case "project_root": {
+        targetWorkspacePath = projectWorktreeRoot;
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        if (createBranch && requestedBranch === undefined) {
+          return yield* failure(
+            "invalid_request",
+            "target.create requires target.branch when checking out the project root.",
+          );
+        }
+        if (requestedBranch !== undefined) {
+          selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+          if (createBranch && localRefByName.has(requestedBranch)) {
+            return yield* failure(
+              "invalid_request",
+              `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+            );
+          }
+          if (!createBranch && selectedRef === undefined) {
+            return yield* failure(
+              "invalid_request",
+              `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the project root.`,
+            );
+          }
+        }
+        break;
+      }
+      case "branch": {
+        requestedBranch = input.target.branch;
+        createBranch = input.target.create ?? false;
+        selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+        if (createBranch && localRefByName.has(requestedBranch)) {
+          return yield* failure(
+            "invalid_request",
+            `Local branch '${requestedBranch}' already exists. Omit target.create to check it out.`,
+          );
+        }
+        if (!createBranch && selectedRef === undefined) {
+          return yield* failure(
+            "invalid_request",
+            `Branch or remote ref '${requestedBranch}' does not exist. Pass target.create=true to create a local branch from the current checkout.`,
+          );
+        }
+        const workspace = input.target.workspace ?? "auto";
+        const selectedWorktreePath =
+          selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+            ? null
+            : selectedRef.worktreePath;
+        targetWorkspacePath =
+          workspace === "project_root"
+            ? projectWorktreeRoot
+            : workspace === "current"
+              ? (currentWorkspacePath ?? recordedWorkspacePath)
+              : (selectedWorktreePath ??
+                (currentWorkspacePath === null ||
+                (projection.thread.worktreePath !== null && selectedRef?.isDefault === true)
+                  ? projectWorktreeRoot
+                  : currentWorkspacePath));
+        break;
+      }
+    }
+
+    if (!workspacePaths.has(targetWorkspacePath)) {
+      return yield* failure(
+        "scope_mismatch",
+        `Checkout target '${targetWorkspacePath}' is outside the calling thread's project worktrees.`,
+      );
+    }
+
+    const targetBefore =
+      targetWorkspacePath === currentWorkspacePath
+        ? previousActual
+        : yield* readWorkspaceStatus(targetWorkspacePath);
+    if (!targetBefore.isRepo) {
+      return yield* failure(
+        "invalid_request",
+        `Checkout target '${targetWorkspacePath}' is not a git repository.`,
+      );
+    }
+
+    if (input.target.type === "worktree") {
+      requestedBranch = targetBefore.refName ?? undefined;
+      selectedRef =
+        targetBefore.refName === null ? undefined : localRefByName.get(targetBefore.refName);
+    } else if (requestedBranch !== undefined) {
+      selectedRef = localRefByName.get(requestedBranch) ?? remoteRefByName.get(requestedBranch);
+    }
+
+    const selectedWorktreePath =
+      selectedRef?.isRemote === true || selectedRef?.worktreePath == null
+        ? null
+        : selectedRef.worktreePath;
+    if (
+      !createBranch &&
+      requestedBranch !== undefined &&
+      selectedWorktreePath !== null &&
+      selectedWorktreePath !== targetWorkspacePath
+    ) {
+      return yield* failure(
+        "workspace_in_use",
+        `Branch '${requestedBranch}' is checked out at '${selectedWorktreePath}'. Use target.type='worktree' with that path or target.workspace='auto' to reuse it.`,
+      );
+    }
+
+    const shouldMutateCheckout =
+      requestedBranch !== undefined &&
+      (createBranch || selectedRef?.isRemote === true || targetBefore.refName !== requestedBranch);
+    const threadWorkspaces = yield* Effect.forEach(threads, (thread) =>
+      Effect.gen(function* () {
+        const recordedPath = yield* threadWorkspacePath(thread, projectWorktreeRoot);
+        const recordedInventory = yield* loadWorkspaceBindingInventory(recordedPath);
+        const workspacePath = Option.match(recordedInventory, {
+          onNone: () => recordedPath,
+          onSome: (candidateInventory) =>
+            candidateInventory.repositoryCommonDir === inventory.repositoryCommonDir &&
+            candidateInventory.currentWorktreeRoot !== null
+              ? candidateInventory.currentWorktreeRoot
+              : recordedPath,
+        });
+        return [thread, workspacePath] as const;
+      }),
+    );
+    const otherBindings = threadWorkspaces
+      .filter(
+        ([thread, workspacePath]) =>
+          thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+      )
+      .map(([thread]) => thread);
+    const activeBinding = otherBindings.find((thread) => thread.activeRunId !== null);
+    if ((targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) && activeBinding) {
+      return yield* failure(
+        "workspace_in_use",
+        `Checkout '${targetWorkspacePath}' is in use by active thread '${activeBinding.id}' (${activeBinding.title}).`,
+      );
+    }
+    if (
+      targetWorkspacePath !== projectWorktreeRoot &&
+      (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `Worktree '${targetWorkspacePath}' is already bound to thread '${otherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+      );
+    }
+    if (
+      targetWorkspacePath === projectWorktreeRoot &&
+      shouldMutateCheckout &&
+      otherBindings.length > 0
+    ) {
+      return yield* failure(
+        "workspace_shared",
+        `The project root is also bound to thread '${otherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+      );
+    }
+    if (shouldMutateCheckout && targetBefore.hasWorkingTreeChanges) {
+      return yield* failure(
+        "dirty_workspace",
+        `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+      );
+    }
+
+    const ids = yield* transitionIds(scope, "checkout");
+    const workspaceGuardKey = physicalWorkspaceGuardKey(
+      inventory.repositoryCommonDir,
+      targetWorkspacePath,
+    );
+    return yield* Effect.uninterruptibleMask((restore) =>
+      Effect.suspend(() => {
+        if (workspaceTransitionsInFlight.has(workspaceGuardKey)) {
+          return Effect.fail(
+            failure(
+              "checkout_in_progress",
+              `Another workspace transition is already in progress for '${targetWorkspacePath}'.`,
+            ),
+          );
+        }
+        workspaceTransitionsInFlight.add(workspaceGuardKey);
+        return Effect.gen(function* () {
+          const [latestProjection, latestInventory, latestBindings, latestTargetBefore] =
+            yield* restore(
+              Effect.all(
+                [
+                  loadThread(scope),
+                  loadWorktrees(projectWorkspaceRoot),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  readWorkspaceStatus(targetWorkspacePath),
+                ],
+                { concurrency: 4 },
+              ),
+            );
+          if (
+            latestProjection.thread.branch !== projection.thread.branch ||
+            latestProjection.thread.worktreePath !== projection.thread.worktreePath ||
+            latestProjection.thread.archivedAt !== projection.thread.archivedAt
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Thread '${scope.threadId}' changed workspace state while checkout was being prepared. Retry from its current binding.`,
+            );
+          }
+          if (
+            latestInventory.repositoryCommonDir !== inventory.repositoryCommonDir ||
+            !latestInventory.worktrees.some((worktree) => worktree.path === targetWorkspacePath)
+          ) {
+            return yield* failure(
+              "scope_mismatch",
+              `Checkout target '${targetWorkspacePath}' is no longer registered in the calling thread's Git repository.`,
+            );
+          }
+          if (
+            latestTargetBefore.refName !== targetBefore.refName ||
+            latestTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+          ) {
+            return yield* failure(
+              "checkout_in_progress",
+              `Checkout '${targetWorkspacePath}' changed while the transition was being prepared. Retry from its current Git state.`,
+            );
+          }
+          if (shouldMutateCheckout && latestTargetBefore.hasWorkingTreeChanges) {
+            return yield* failure(
+              "dirty_workspace",
+              `Checkout '${targetWorkspacePath}' has uncommitted files. Commit or discard them before switching branches.`,
+            );
+          }
+          const latestOtherBindings = latestBindings
+            .filter(
+              ([thread, workspacePath]) =>
+                thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+            )
+            .map(([thread]) => thread);
+          const latestActiveBinding = latestOtherBindings.find(
+            (thread) => thread.activeRunId !== null,
+          );
+          if (
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestActiveBinding
+          ) {
+            return yield* failure(
+              "workspace_in_use",
+              `Checkout '${targetWorkspacePath}' is in use by active thread '${latestActiveBinding.id}' (${latestActiveBinding.title}).`,
+            );
+          }
+          if (
+            targetWorkspacePath !== projectWorktreeRoot &&
+            (targetWorkspacePath !== currentWorkspacePath || shouldMutateCheckout) &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `Worktree '${targetWorkspacePath}' is already bound to thread '${latestOtherBindings[0]!.id}'. Reusing it would make two threads share one mutable checkout.`,
+            );
+          }
+          if (
+            targetWorkspacePath === projectWorktreeRoot &&
+            shouldMutateCheckout &&
+            latestOtherBindings.length > 0
+          ) {
+            return yield* failure(
+              "workspace_shared",
+              `The project root is also bound to thread '${latestOtherBindings[0]!.id}'. Switching its branch would make that thread's recorded branch disagree with Git.`,
+            );
+          }
+
+          let checkoutAction: WorktreeMcpCheckoutResult["checkoutAction"] =
+            targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
+          let resolvedBranch: string | null = targetBefore.refName;
+          const targetBeforeCommit = shouldMutateCheckout
+            ? yield* restore(
+                gitWorkflow
+                  .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                  .pipe(asOperationFailed("Unable to record the checkout's current commit")),
+              )
+            : null;
+          const requestedTransitionCommit =
+            shouldMutateCheckout && requestedBranch !== undefined
+              ? createBranch
+                ? targetBeforeCommit
+                : yield* restore(
+                    gitWorkflow
+                      .resolveCommit({ cwd: targetWorkspacePath, revision: requestedBranch })
+                      .pipe(asOperationFailed(`Unable to resolve ref '${requestedBranch}'`)),
+                  )
+              : null;
+          let ownedCheckoutState: {
+            readonly refName: string | null;
+            readonly hasWorkingTreeChanges: boolean;
+            readonly commitSha: string;
+          } | null = null;
+
+          const captureCheckoutState = Effect.fn("WorktreeMcpService.captureCheckoutState")(
+            function* () {
+              const status = yield* readWorkspaceStatus(targetWorkspacePath);
+              const commit = yield* gitWorkflow
+                .resolveCommit({ cwd: targetWorkspacePath, revision: "HEAD" })
+                .pipe(asOperationFailed("Unable to identify the checkout's current commit"));
+              return {
+                refName: status.refName,
+                hasWorkingTreeChanges: status.hasWorkingTreeChanges,
+                commitSha: commit.commitSha,
+              };
+            },
+          );
+
+          const rollbackOwnedCheckout = Effect.fn("WorktreeMcpService.rollbackOwnedCheckout")(
+            function* () {
+              if (!shouldMutateCheckout) {
+                return "not_needed" as const;
+              }
+              if (ownedCheckoutState === null || targetBeforeCommit === null) {
+                return "not_possible" as const;
+              }
+              const [latestStateExit, latestBindings, callerProjectionExit] = yield* Effect.all(
+                [
+                  Effect.exit(captureCheckoutState()),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  Effect.exit(threadManagement.getThreadProjection(scope.threadId)),
+                ],
+                { concurrency: 3 },
+              );
+              const anotherOwner = latestBindings.some(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              );
+              const callerStillInitial =
+                Exit.isSuccess(callerProjectionExit) &&
+                callerProjectionExit.value.thread.branch === projection.thread.branch &&
+                callerProjectionExit.value.thread.worktreePath === projection.thread.worktreePath &&
+                callerProjectionExit.value.thread.archivedAt === projection.thread.archivedAt &&
+                callerProjectionExit.value.thread.deletedAt === projection.thread.deletedAt;
+              if (
+                Exit.isFailure(latestStateExit) ||
+                latestStateExit.value.refName !== ownedCheckoutState.refName ||
+                latestStateExit.value.hasWorkingTreeChanges ||
+                latestStateExit.value.hasWorkingTreeChanges !==
+                  ownedCheckoutState.hasWorkingTreeChanges ||
+                latestStateExit.value.commitSha !== ownedCheckoutState.commitSha ||
+                anotherOwner ||
+                !callerStillInitial
+              ) {
+                return "not_possible" as const;
+              }
+              const checkoutChanged =
+                latestStateExit.value.refName !== targetBefore.refName ||
+                latestStateExit.value.commitSha !== targetBeforeCommit.commitSha;
+              if (checkoutChanged && targetBefore.refName === null) {
+                return "not_possible" as const;
+              }
+              const rollbackExit = yield* Effect.exit(
+                checkoutChanged
+                  ? gitWorkflow.switchRef({
+                      cwd: targetWorkspacePath,
+                      refName: targetBefore.refName!,
+                    })
+                  : Effect.void,
+              );
+              return Exit.isSuccess(rollbackExit) ? ("rolled_back" as const) : ("failed" as const);
+            },
+          );
+
+          if (shouldMutateCheckout && requestedBranch !== undefined) {
+            const [mutationProjection, mutationBindings, mutationTargetBefore] = yield* restore(
+              Effect.all(
+                [
+                  loadThread(scope),
+                  loadActiveWorkspaceBindings(inventory.repositoryCommonDir),
+                  readWorkspaceStatus(targetWorkspacePath),
+                ],
+                { concurrency: 3 },
+              ),
+            );
+            if (
+              mutationProjection.thread.branch !== projection.thread.branch ||
+              mutationProjection.thread.worktreePath !== projection.thread.worktreePath ||
+              mutationProjection.thread.archivedAt !== projection.thread.archivedAt ||
+              mutationProjection.thread.deletedAt !== projection.thread.deletedAt ||
+              mutationTargetBefore.refName !== targetBefore.refName ||
+              mutationTargetBefore.hasWorkingTreeChanges !== targetBefore.hasWorkingTreeChanges
+            ) {
+              return yield* failure(
+                "checkout_in_progress",
+                `Thread or checkout state changed immediately before Git mutation. Retry from the current workspace state.`,
+              );
+            }
+            const mutationOtherBindings = mutationBindings
+              .filter(
+                ([thread, workspacePath]) =>
+                  thread.id !== scope.threadId && workspacePath === targetWorkspacePath,
+              )
+              .map(([thread]) => thread);
+            const mutationActiveBinding = mutationOtherBindings.find(
+              (thread) => thread.activeRunId !== null,
+            );
+            if (mutationActiveBinding !== undefined) {
+              return yield* failure(
+                "workspace_in_use",
+                `Checkout '${targetWorkspacePath}' became active for thread '${mutationActiveBinding.id}' (${mutationActiveBinding.title}) before Git mutation.`,
+              );
+            }
+            if (mutationOtherBindings.length > 0) {
+              return yield* failure(
+                "workspace_shared",
+                `Checkout '${targetWorkspacePath}' became bound to thread '${mutationOtherBindings[0]!.id}' before Git mutation.`,
+              );
+            }
+            if (createBranch) {
+              yield* gitWorkflow
+                .createRef({
+                  cwd: targetWorkspacePath,
+                  refName: requestedBranch,
M
macroscopeappBotAug 30, 2026

🟑 Medium mcp/WorktreeMcpService.ts:1657

A target.create=true request with branch: "--unset-upstream" executes git branch --unset-upstream, changing the current branch’s upstream configuration instead of creating a branch; the subsequent checkout then fails. Validate requestedBranch as a Git ref name and ensure the driver terminates options before passing user input to git branch.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 1657:
2
3A `target.create=true` request with `branch: "--unset-upstream"` executes `git branch --unset-upstream`, changing the current branch’s upstream configuration instead of creating a branch; the subsequent checkout then fails. Validate `requestedBranch` as a Git ref name and ensure the driver terminates options before passing user input to `git branch`.