Part V · The work lifecycleWorktree topology
Chapter 22source checked

Current checkout versus isolated worktrees

A new thread may operate in the project's current checkout or, for a Git repository, receive a newly created branch in a linked Git worktree; both routes retain Git as the authority for branch occupancy, filesystem state, and cleanup.

What this chapter resolves
  • Distinguish current-checkout execution from an isolated linked Git worktree.
  • Trace branch/base selection, optional origin refresh, worktree creation, setup, and thread metadata update.
  • Identify Git-owned invariants: a branch's worktree occupancy, dirty-state removal failure, and non-Git fallback.
  • Read bootstrap failure recovery as a bounded orchestration cleanup, not a transaction that reverses Git.

A workspace choice is a choice about where this thread may change files and checkout state. worktree means a new linked Git checkout when the project is a Git repository. local means do not create a new worktree. It usually uses the project checkout, but it is not a promise that the thread has no stored workspace path: the mobile bootstrap can retain a selected existing worktreePath for a local request. Later runtime workspace resolution prefers that stored thread path and only then falls back to the project’s root. It is not a generic container mode, and the current web send path deliberately downgrades the request to local for a non-Git project.

apps/web/src/components/ChatView.logic.ts:322–327 ↗verbatim · typescript · f7e6d731
export function resolveSendEnvMode(input: {
  requestedEnvMode: DraftThreadEnvMode;
  isGitRepo: boolean;
}): DraftThreadEnvMode {
  return input.isGitRepo ? input.requestedEnvMode : "local";
}
Read this as: The web send path preserves the requested mode only for a Git repository.

One repository can expose several checkout paths

Git’s ref list is also the occupancy map T3 presents to the user. The driver asks Git for worktree list --porcelain -z, parses branch/path pairs, normalizes them, filters paths that no longer exist, and attaches the surviving linked path to local branch refs. A branch marked with a worktreePath is therefore not an abstract availability hint: another checkout has that branch checked out.

apps/server/src/vcs/GitVcsDriverCore.ts:2463–2559 ↗verbatim · typescript · 0d568abc
  const readGitRefsSnapshot = Effect.fn("readGitRefsSnapshot")(function* (gitCommonDir: string) {
    const fetchCwd =
      path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
    const gitDirArgs = ["--git-dir", gitCommonDir] as const;
    const [refsResult, defaultRefResult, worktreeListResult, remoteNamesResult] = yield* Effect.all(
      [
        executeGitWithStableDiagnostics(
          "GitVcsDriver.listRefs.snapshotRefs",
          fetchCwd,
          [
            ...gitDirArgs,
            "for-each-ref",
            "--format=%(refname)%09%(committerdate:unix)%09%(symref)",
            "refs/heads",
            "refs/remotes",
          ],
          {
            timeoutMs: 30_000,
            maxOutputBytes: 16 * 1024 * 1024,
            fallbackErrorDetail: "Git ref snapshot enumeration failed.",
          },
        ),
        executeGit(
          "GitVcsDriver.listRefs.defaultRef",
          fetchCwd,
          [...gitDirArgs, "symbolic-ref", "refs/remotes/origin/HEAD"],
          {
            timeoutMs: 5_000,
            allowNonZeroExit: true,
          },
        ),
        executeGit(
          "GitVcsDriver.listRefs.worktreeList",
          fetchCwd,
          [...gitDirArgs, "worktree", "list", "--porcelain", "-z"],
          {
            timeoutMs: 30_000,
            allowNonZeroExit: true,
            maxOutputBytes: 16 * 1024 * 1024,
          },
        ),
        executeGit("GitVcsDriver.listRefs.remoteNames", fetchCwd, [...gitDirArgs, "remote"], {
          timeoutMs: 5_000,
          allowNonZeroExit: true,
        }),
      ],
      { concurrency: 2 },
    );
 
    const remoteNames =
      remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
    if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
      yield* Effect.logWarning(
        `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${gitCommonDir}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
      );
    }
    const defaultBranch =
      defaultRefResult.exitCode === 0
        ? defaultRefResult.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
        : null;
    const parsedWorktreeEntries =
      worktreeListResult.exitCode === 0
        ? [...parseWorktreeBranchPaths(worktreeListResult.stdout)].map(
            ([branchName, worktreePath]) =>
              [branchName, path.normalize(path.resolve(worktreePath))] as const,
          )
        : [];
    const existingWorktreeEntries = yield* Effect.filter(
      parsedWorktreeEntries,
      ([, worktreePath]) =>
        fileSystem.stat(worktreePath).pipe(
          Effect.as(true),
          Effect.orElseSucceed(() => false),
        ),
      { concurrency: 16 },
    );
    const worktreeMap = new Map(existingWorktreeEntries);
    const localBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
    const remoteBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
 
    for (const line of refsResult.stdout.split("\n")) {
      if (line.length === 0) continue;
      const [fullRefName, lastCommitRaw, symbolicTarget] = line.split("\t");
      if (!fullRefName || symbolicTarget) continue;
      const parsedLastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
      const lastCommit = Number.isFinite(parsedLastCommit) ? parsedLastCommit : 0;
 
      if (fullRefName.startsWith("refs/heads/")) {
        const name = fullRefName.slice("refs/heads/".length);
        localBranches.push({
          ref: {
            name,
            current: false,
            isRemote: false,
            isDefault: name === defaultBranch,
            worktreePath: worktreeMap.get(name) ?? null,
          },
Read this as: Local branch refs are decorated from Git's porcelain worktree listing after path existence checks.

The RPC contract exposes the relevant choices directly: creation accepts a base refName, optional new branch name and base reference metadata, plus an optional explicit path; removal accepts the path and an optional force flag. The result returns a worktree path and ref name. A pull-request preparation result separately admits isOnPullRequestHead: false, documenting that an existing linked checkout can be intentionally retained even when it could not safely be moved to the requested head.

packages/contracts/src/git.ts:124–165 ↗verbatim · typescript · 38f837fc
export const VcsListRefsInput = Schema.Struct({
  cwd: TrimmedNonEmptyStringSchema,
  query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))),
  cursor: Schema.optional(NonNegativeInt),
  includeMatchingRemoteRefs: Schema.optional(Schema.Boolean),
  refKind: Schema.optional(Schema.Literals(["all", "local", "remote"])),
  refresh: Schema.optional(Schema.Boolean),
  limit: Schema.optional(
    PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)),
  ),
});
export type VcsListRefsInput = typeof VcsListRefsInput.Type;
 
export const VcsCreateWorktreeInput = Schema.Struct({
  cwd: TrimmedNonEmptyStringSchema,
  refName: TrimmedNonEmptyStringSchema,
  newRefName: Schema.optional(TrimmedNonEmptyStringSchema),
  baseRefName: Schema.optional(TrimmedNonEmptyStringSchema),
  path: Schema.NullOr(TrimmedNonEmptyStringSchema),
});
export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type;
 
export const GitPullRequestRefInput = Schema.Struct({
  cwd: TrimmedNonEmptyStringSchema,
  reference: GitPullRequestReference,
});
export type GitPullRequestRefInput = typeof GitPullRequestRefInput.Type;
 
export const GitPreparePullRequestThreadInput = Schema.Struct({
  cwd: TrimmedNonEmptyStringSchema,
  reference: GitPullRequestReference,
  mode: GitPreparePullRequestThreadMode,
  threadId: Schema.optional(ThreadId),
});
export type GitPreparePullRequestThreadInput = typeof GitPreparePullRequestThreadInput.Type;
 
export const VcsRemoveWorktreeInput = Schema.Struct({
  cwd: TrimmedNonEmptyStringSchema,
  path: TrimmedNonEmptyStringSchema,
  force: Schema.optional(Schema.Boolean),
});
export type VcsRemoveWorktreeInput = typeof VcsRemoveWorktreeInput.Type;
Read this as: The wire contract carries worktree creation and removal inputs.
packages/contracts/src/git.ts:264–294 ↗verbatim · typescript · be9e1709
export const VcsListRefsResult = Schema.Struct({
  refs: Schema.Array(VcsRef),
  isRepo: Schema.Boolean,
  hasPrimaryRemote: Schema.Boolean,
  nextCursor: NonNegativeInt.pipe(Schema.NullOr),
  totalCount: NonNegativeInt,
});
export type VcsListRefsResult = typeof VcsListRefsResult.Type;
 
export const VcsCreateWorktreeResult = Schema.Struct({
  worktree: VcsWorktree,
});
export type VcsCreateWorktreeResult = typeof VcsCreateWorktreeResult.Type;
 
export const GitResolvePullRequestResult = Schema.Struct({
  pullRequest: GitResolvedPullRequest,
});
export type GitResolvePullRequestResult = typeof GitResolvePullRequestResult.Type;
 
export const GitPreparePullRequestThreadResult = Schema.Struct({
  pullRequest: GitResolvedPullRequest,
  branch: TrimmedNonEmptyStringSchema,
  worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
  /**
   * False when the checkout could not be brought to the pull request head — a reused worktree
   * holding local commits or uncommitted changes keeps its own state, so the code being handed
   * over is older than the pull request.
   */
  isOnPullRequestHead: Schema.Boolean,
});
export type GitPreparePullRequestThreadResult = typeof GitPreparePullRequestThreadResult.Type;
Read this as: The results return linked worktree data and explicitly qualify a reused PR checkout's head freshness.

Current checkout and new worktree diverge at bootstrap

For an ordinary local request, no git worktree add call appears in the bootstrap branch. Its execution cwd is the thread’s already-stored worktreePath, if one is present, otherwise the project root. A selected new-worktree branch instead follows this ordered path:

  1. Start with the selected base branch.
  2. If “start from origin” is requested and an origin remote exists, fetch origin and resolve the selected base to a remote-tracking commit.
  3. Ask the Git workflow to create a worktree from that base, with the requested new branch name and the selected base as merge-base metadata.
  4. Save the returned branch/path onto the thread, refresh Git status at that linked path, run any opted-in setup script there, then start the turn.
apps/server/src/ws.ts:979–1022 ↗verbatim · typescript · d243bf01
            if (bootstrap?.prepareWorktree) {
              let worktreeBaseRef = bootstrap.prepareWorktree.baseBranch;
              // "Start from origin" is a stored default; repos without an
              // origin remote fall back to the local base branch instead of
              // failing the whole bootstrap on `git fetch origin`.
              const startFromOrigin =
                bootstrap.prepareWorktree.startFromOrigin === true &&
                (yield* gitWorkflow.remoteExists({
                  cwd: bootstrap.prepareWorktree.projectCwd,
                  remoteName: "origin",
                }));
              if (startFromOrigin) {
                yield* gitWorkflow.fetchRemote({
                  cwd: bootstrap.prepareWorktree.projectCwd,
                  remoteName: "origin",
                });
                const resolvedRemoteBase = yield* gitWorkflow.resolveRemoteTrackingCommit({
                  cwd: bootstrap.prepareWorktree.projectCwd,
                  refName: bootstrap.prepareWorktree.baseBranch,
                  fallbackRemoteName: "origin",
                });
                worktreeBaseRef = resolvedRemoteBase.commitSha;
              }
              const worktree = yield* gitWorkflow.createWorktree({
                cwd: bootstrap.prepareWorktree.projectCwd,
                refName: worktreeBaseRef,
                newRefName: bootstrap.prepareWorktree.branch,
                baseRefName: bootstrap.prepareWorktree.baseBranch,
                path: null,
              });
              targetWorktreePath = worktree.worktree.path;
              yield* dispatchFromClient({
                type: "thread.meta.update",
                commandId: yield* serverCommandId("bootstrap-thread-meta-update"),
                threadId: command.threadId,
                branch: worktree.worktree.refName,
                worktreePath: targetWorktreePath,
              });
              yield* refreshGitStatus(targetWorktreePath);
            }
 
            yield* runSetupProgram();
 
            return yield* dispatchFromClient(finalTurnStartCommand);
Read this as: The bootstrap conditionally refreshes origin, creates the linked checkout, records it on the thread, then starts setup and the turn.

The distinction between a request and a cwd matters on mobile. Its builder sets a new-worktree request’s initial path to null, but leaves the supplied path intact for a local request. The runtime resolver then gives any non-null thread path priority over the project root. Thus “local” is best read as “no new worktree creation,” not as “this thread can never point at an existing linked checkout.”

The important qualification is step 2: the source checks for origin first. A stored preference to start from origin does not make an origin-less repository fail; the path stays on the selected local base. That is a practical fallback, not an assertion that the local base equals a server’s latest commit.

apps/server/src/vcs/GitVcsDriverCore.test.ts:1505–1565 ↗verbatim · typescript · 30794855
    it.effect("creates a worktree from the latest fetched remote commit", () =>
      Effect.gen(function* () {
        const cwd = yield* makeTmpDir();
        const remote = yield* makeTmpDir("git-remote-");
        const peer = yield* makeTmpDir("git-peer-");
        const { initialBranch } = yield* initRepoWithCommit(cwd);
        yield* git(remote, ["init", "--bare"]);
        yield* git(cwd, ["remote", "add", "origin", remote]);
        yield* git(cwd, ["push", "-u", "origin", initialBranch]);
        yield* git(remote, ["symbolic-ref", "HEAD", `refs/heads/${initialBranch}`]);
        const beforeFetch = yield* git(cwd, ["rev-parse", `refs/remotes/origin/${initialBranch}`]);
 
        yield* git(peer, ["clone", remote, "."]);
        yield* git(peer, ["config", "user.email", "test@test.com"]);
        yield* git(peer, ["config", "user.name", "Test"]);
        yield* writeTextFile(peer, "remote-change.txt", "remote\n");
        yield* git(peer, ["add", "remote-change.txt"]);
        yield* git(peer, ["commit", "-m", "remote change"]);
        yield* git(peer, ["push", "origin", initialBranch]);
        const remoteHead = yield* git(peer, ["rev-parse", "HEAD"]);
        assert.notEqual(beforeFetch, remoteHead);
 
        const driver = yield* GitVcsDriver.GitVcsDriver;
        yield* driver.fetchRemote({ cwd, remoteName: "origin" });
 
        const resolvedBase = yield* driver.resolveRemoteTrackingCommit({
          cwd,
          refName: initialBranch,
          fallbackRemoteName: "origin",
        });
        const explicitlyResolvedBase = yield* driver.resolveRemoteTrackingCommit({
          cwd,
          refName: `origin/${initialBranch}`,
          fallbackRemoteName: "origin",
        });
 
        assert.deepEqual(resolvedBase, {
          commitSha: remoteHead,
          remoteRefName: `origin/${initialBranch}`,
        });
        assert.deepEqual(explicitlyResolvedBase, resolvedBase);
        assert.equal(yield* git(cwd, ["rev-parse", initialBranch]), beforeFetch);
 
        const pathService = yield* Path.Path;
        const worktreePath = pathService.join(
          yield* makeTmpDir("git-fetched-worktrees-"),
          "fetched-origin",
        );
        yield* driver.createWorktree({
          cwd,
          path: worktreePath,
          refName: resolvedBase.commitSha,
          newRefName: "t3code/fetched-origin",
          baseRefName: resolvedBase.remoteRefName,
        });
 
        assert.equal(yield* git(worktreePath, ["rev-parse", "HEAD"]), remoteHead);
        assert.equal(
          yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.gh-merge-base"),
          initialBranch,
        );
Read this as: The driver test demonstrates an origin fetch, remote commit resolution, a worktree created at that commit, and merge-base metadata.

At the Git boundary, a new branch uses git worktree add -b <new> <path> <base>; an existing ref omits -b. Absent an explicit path, the driver creates a path below its configured worktrees directory using repository name and a slash-sanitized branch name. When both a new branch and base-ref metadata are supplied, it sets branch.<new>.gh-merge-base using the branch portion of a remote ref when it can parse one.

apps/server/src/vcs/GitVcsDriverCore.ts:2763–2799 ↗verbatim · typescript · 4c15353d
  const createWorktree: GitVcsDriver.GitVcsDriver["Service"]["createWorktree"] = Effect.fn(
    "createWorktree",
  )(function* (input) {
    const targetBranch = input.newRefName ?? input.refName;
    const sanitizedBranch = targetBranch.replace(/\//g, "-");
    const repoName = path.basename(input.cwd);
    const worktreePath = input.path ?? path.join(worktreesDir, repoName, sanitizedBranch);
    const args = input.newRefName
      ? ["worktree", "add", "-b", input.newRefName, worktreePath, input.refName]
      : ["worktree", "add", worktreePath, input.refName];
 
    yield* executeGit("GitVcsDriver.createWorktree", input.cwd, args, {
      fallbackErrorDetail: "git worktree add failed",
      timeoutMs: WORKTREE_ADD_TIMEOUT_MS,
    });
 
    if (input.newRefName && input.baseRefName) {
      const remoteNames = yield* listRemoteNames(input.cwd).pipe(Effect.orElseSucceed(() => []));
      const parsedBaseRef = parseRemoteRefWithRemoteNames(
        input.baseRefName,
        remoteNames.toSorted((left, right) => right.length - left.length),
      );
      const baseBranch = parsedBaseRef?.branchName ?? input.baseRefName;
      yield* runGit("GitVcsDriver.createWorktree.configureBaseRef", input.cwd, [
        "config",
        `branch.${input.newRefName}.gh-merge-base`,
        baseBranch,
      ]);
    }
 
    return {
      worktree: {
        path: worktreePath,
        refName: targetBranch,
      },
    };
  });
Read this as: Creation is an actual Git worktree add command with a bounded timeout and optional base-ref configuration.

Setup sees a worktree cwd and two path labels

After creation, the setup runner finds the project, chooses its first runOnWorktreeCreate script, opens a terminal at the linked worktree path, and writes the command. It supplies T3CODE_PROJECT_ROOT for the original project root and T3CODE_WORKTREE_PATH for the linked checkout. A missing selected script returns no-script; terminal failures become structured runner errors. In the bootstrap path, a setup launch failure records a setup-script.failed activity and warning; a successful launch records requested/started activities. These outcomes are observability around the selected workspace, rather than a hidden switch back to a different checkout.

apps/server/src/project/ProjectSetupScriptRunner.ts:123–183 ↗verbatim · typescript · cff81f3c
    if (!project) {
      return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);
    }
 
    const script = setupProjectScript(project.scripts);
    if (!script) {
      return {
        status: "no-script",
      } as const;
    }
 
    const terminalId = input.preferredTerminalId ?? `setup-${script.id}`;
    const cwd = input.worktreePath;
    const env = projectScriptRuntimeEnv({
      project: { cwd: project.workspaceRoot },
      worktreePath: input.worktreePath,
    });
 
    yield* terminalManager
      .open({
        threadId: input.threadId,
        terminalId,
        cwd,
        worktreePath: input.worktreePath,
        env,
      })
      .pipe(
        Effect.mapError(
          (cause) =>
            new ProjectSetupScriptOperationError({
              ...errorContext,
              operation: "openTerminal",
              cause,
            }),
        ),
      );
    yield* terminalManager
      .write({
        threadId: input.threadId,
        terminalId,
        data: `${script.command}\r`,
      })
      .pipe(
        Effect.mapError(
          (cause) =>
            new ProjectSetupScriptOperationError({
              ...errorContext,
              operation: "writeCommand",
              cause,
            }),
        ),
      );
 
    return {
      status: "started",
      scriptId: script.id,
      scriptName: script.name,
      terminalId,
      cwd,
    } as const;
  });
Read this as: The setup terminal runs at the linked checkout and receives explicit project-root/worktree path labels.
apps/server/src/project/ProjectSetupScriptRunner.test.ts:93–152 ↗verbatim · typescript · 97cc43e3
  it.effect(
    "opens the deterministic setup terminal with worktree env and writes the command",
    () => {
      const open = vi.fn(() =>
        Effect.succeed({
          threadId: "thread-1",
          terminalId: "setup-setup",
          cwd: "/repo/worktrees/a",
          worktreePath: "/repo/worktrees/a",
          status: "running" as const,
          pid: 123,
          history: "",
          exitCode: null,
          exitSignal: null,
          label: "setup-setup",
          updatedAt: "2026-01-01T00:00:00.000Z",
        }),
      );
      const write = vi.fn(() => Effect.void);
      const project = makeProject([
        {
          id: "setup",
          name: "Setup",
          command: "bun install",
          icon: "configure",
          runOnWorktreeCreate: true,
        },
      ]);
 
      return Effect.gen(function* () {
        const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
        const result = yield* runner.runForThread({
          threadId: "thread-1",
          projectCwd: "/repo/project",
          worktreePath: "/repo/worktrees/a",
        });
 
        expect(result).toEqual({
          status: "started",
          scriptId: "setup",
          scriptName: "Setup",
          terminalId: "setup-setup",
          cwd: "/repo/worktrees/a",
        });
        expect(open).toHaveBeenCalledWith({
          threadId: "thread-1",
          terminalId: "setup-setup",
          cwd: "/repo/worktrees/a",
          worktreePath: "/repo/worktrees/a",
          env: {
            T3CODE_PROJECT_ROOT: "/repo/project",
            T3CODE_WORKTREE_PATH: "/repo/worktrees/a",
          },
        });
        expect(write).toHaveBeenCalledWith({
          threadId: "thread-1",
          terminalId: "setup-setup",
          data: "bun install\r",
        });
      }).pipe(Effect.provide(testLayer(project, { open, write })));
Read this as: The test asserts deterministic setup terminal creation, cwd, both environment labels, and command write.
Figure 22.1 · Current checkout and isolated worktree are different topologiesonly one branch creates a linked path
Thread workspace topologyDiagram loading

Zoom with the controls, +/, or Ctrl/ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.

Thread workspace topology
Text equivalent

The local route connects a local request to no-new-worktree creation, then resolves the existing stored thread path if present or the project root before a turn starts. The worktree route connects the project root through an optional origin fetch and remote base resolution, then Git worktree creation, then a thread worktree-path update and a setup terminal at the linked checkout, then the turn. A failure arrow from the bootstrap goes to thread cleanup only; Git remains the authority for any linked directory it created.

Figure 22.1. A local request creates no linked Git checkout; its runtime cwd resolves from a stored thread path first, then the project root. Worktree mode first confirms Git, optionally refreshes origin, creates a new branch in a linked path, records the returned path on the thread, then runs setup there. Both modes ultimately dispatch the turn, but no arrow claims that Git creation and durable thread updates are one transaction.

Thread deletion and worktree cleanup are separate operations

Durably deleting a thread is not itself a Git worktree removal command. The server’s post-delete reactor attempts to stop that thread’s provider session and close its terminal with terminal-history deletion. It does not call the Git worktree driver in this reactor. This is important even for a thread that has a worktreePath: deleting the conversation’s durable record and cleaning an on-disk checkout have different owners and failure modes.

The web client can add a second, explicitly confirmed cleanup step. When the deleted thread is the only thread linked to that path and a local API is available, it offers to delete the orphaned worktree too. Only after the durable thread deletion succeeds does it call removeWorktree with force: true. If that Git removal or the subsequent refresh fails, the client reports “Thread deleted, but worktree removal failed”; the durable thread stays deleted while the worktree can remain for manual inspection or later cleanup.

Explicit worktree removal is a Git command, while bootstrap recovery is narrower

The explicit removal route invokes git worktree remove <path> and adds --force only if requested. A driver test confirms a created linked path disappears after normal removal. It does not delete the branch in the shown implementation, and it does not turn a dirty-tree refusal into success. Treat force as an explicit Git operation, not an automatic recovery action.

apps/server/src/vcs/GitVcsDriverCore.ts:2982–2994 ↗verbatim · typescript · af8c3225
  const removeWorktree: GitVcsDriver.GitVcsDriver["Service"]["removeWorktree"] = Effect.fn(
    "removeWorktree",
  )(function* (input) {
    const args = ["worktree", "remove"];
    if (input.force) {
      args.push("--force");
    }
    args.push(input.path);
    yield* executeGit("GitVcsDriver.removeWorktree", input.cwd, args, {
      timeoutMs: 15_000,
      fallbackErrorDetail: "git worktree remove failed",
    });
  });
Read this as: Removal delegates to Git and forwards force only when the input asked for it.
apps/server/src/vcs/GitVcsDriverCore.test.ts:1377–1403 ↗verbatim · typescript · 7a00307d
    it.effect("creates and removes a worktree for a new refName", () =>
      Effect.gen(function* () {
        const cwd = yield* makeTmpDir();
        const { initialBranch } = yield* initRepoWithCommit(cwd);
        const pathService = yield* Path.Path;
        const worktreePath = pathService.join(
          yield* makeTmpDir("git-worktrees-"),
          "feature-worktree",
        );
        const driver = yield* GitVcsDriver.GitVcsDriver;
 
        const created = yield* driver.createWorktree({
          cwd,
          path: worktreePath,
          refName: initialBranch,
          newRefName: "feature/worktree",
        });
 
        assert.equal(created.worktree.path, worktreePath);
        assert.equal(created.worktree.refName, "feature/worktree");
        assert.equal(yield* git(worktreePath, ["branch", "--show-current"]), "feature/worktree");
 
        yield* driver.removeWorktree({ cwd, path: worktreePath });
        const fileSystem = yield* FileSystem.FileSystem;
        assert.equal(yield* fileSystem.exists(worktreePath), false);
      }),
    );
Read this as: The focused driver test creates a new-ref worktree, observes its branch, removes it, and observes that its path is gone.

Bootstrap failure has a different purpose. If a turn-start bootstrap fails after it created its thread, the server attempts an uninterruptible thread.delete cleanup and reports whether that cleanup happened. The observed code does not issue a matching git worktree remove in this catch path. Therefore a failed bootstrap may leave Git evidence that needs an operator’s inspection; claiming atomic rollback of both durable and filesystem changes would be false.

apps/server/src/ws.ts:1025–1053 ↗verbatim · typescript · d2c16f4d
          return yield* bootstrapProgram.pipe(
            Effect.catchCause((cause) => {
              const dispatchError = toBootstrapDispatchCommandCauseError(cause);
              if (Cause.hasInterruptsOnly(cause)) {
                return Effect.fail(dispatchError);
              }
              return Effect.uninterruptible(cleanupCreatedThread()).pipe(
                Effect.matchCauseEffect({
                  onFailure: (cleanupCause) =>
                    Effect.logWarning("bootstrap thread cleanup failed", {
                      threadId: command.threadId,
                      detail: Cause.pretty(cleanupCause),
                    }).pipe(Effect.flatMap(() => Effect.fail(dispatchError))),
                  onSuccess: (threadDeleted) =>
                    Effect.fail(
                      threadDeleted
                        ? new OrchestrationDispatchCommandError({
                            message: dispatchError.message,
                            ...(dispatchError.cause !== undefined
                              ? { cause: dispatchError.cause }
                              : {}),
                            bootstrapThreadDisposition: "deleted",
                          })
                        : dispatchError,
                    ),
                }),
              );
            }),
          );
Read this as: Failure recovery cleans up a bootstrap-created thread and returns a disposition; this range contains no Git worktree deletion.

Work the topology by hand

Choose a route below. The only animation is the short, user-triggered transition between explanations; reduced-motion users receive the same state update without motion, and the complete ledger remains available for printing or without script.

Animated topology lab

Choose where a thread will run

Each activation advances only once. There is no autonomous playback: you control the topology and the motion.

Selected topology: Local request: no new worktree

  1. No Git worktree creation
  2. Stored thread path, if present
  3. Otherwise project root
  4. Thread runs there

Local request: no new worktree

This mode creates no linked worktree. A newly local thread commonly runs at the project root, but a local request can retain an already-selected workspace path; runtime resolution uses that stored thread path before the project root.

Recovery boundary: If the path is not a Git repository, this is the only effective thread workspace mode. “Local” describes creation, not a ban on an existing workspace path.

All topology outcomes
  1. Local request: no new worktree

    This mode creates no linked worktree. A newly local thread commonly runs at the project root, but a local request can retain an already-selected workspace path; runtime resolution uses that stored thread path before the project root.

    Recovery boundary: If the path is not a Git repository, this is the only effective thread workspace mode. “Local” describes creation, not a ban on an existing workspace path.

  2. New worktree from local base

    The bootstrap asks Git to create a new branch at the selected base and checks it out in a separate path. The thread is then updated with that branch and worktree path.

    Recovery boundary: A Git creation failure aborts the bootstrap; the server cleans up a thread it created during that bootstrap, but does not promise to remove every filesystem side effect Git may already have made.

  3. New worktree from origin

    When the option is enabled and origin exists, T3 fetches origin and resolves the requested base to a remote-tracking commit before creating the local branch/worktree.

    Recovery boundary: If origin is absent, the bootstrap falls back to the local selected base instead of failing solely because origin is unavailable.

  4. Delete thread + optional orphan cleanup

    Thread deletion is not a worktree deletion. The server stops the provider session and closes terminal history; the web client may then offer to force-remove a worktree only when this was its sole linked thread and a local API is available.

    Recovery boundary: If the optional Git removal or refresh fails, the thread remains deleted and the worktree can remain. The web client surfaces that partial failure for inspection or later cleanup.

  5. Remove linked worktree

    Removal calls Git with the selected linked path. The optional force flag is forwarded only when requested.

    Recovery boundary: Git remains the authority: a dirty or otherwise protected worktree can make removal fail, so inspect its state before retrying or forcing cleanup.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.