Part VI · The work lifecycleWorktree topology
Chapter 37source 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 · 3434eaae
  if (!modelSelectionChanged && !branchChanged) {
    return null;
  }
  return {
    ...(modelSelectionChanged ? { modelSelection: nextModelSelection } : {}),
    ...(branchChanged ? { branch: input.nextBranch, worktreePath: null } : {}),
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 · d5269dca
    }
 
    const [realRepositoryRoot, realTarget] = yield* Effect.all([
      fileSystem.realPath(repositoryRoot),
      fileSystem.realPath(requestedPath),
    ]).pipe(
      Effect.mapError((cause) =>
        fileError("fs.realPath", `Could not resolve diff file '${input.newPath}'.`, cause),
      ),
    );
    if (!isPathWithinRoot(realRepositoryRoot, realTarget)) {
      return yield* fileError(
        "fs.realPath",
        `Diff file '${input.newPath}' resolves outside the review workspace.`,
      );
    }
 
    const info = yield* fileSystem
      .stat(realTarget)
      .pipe(
        Effect.mapError((cause) =>
          fileError("fs.stat", `Could not inspect diff file '${input.newPath}'.`, cause),
        ),
      );
    if (info.type !== "File") {
      return yield* fileError("fs.stat", `Diff path '${input.newPath}' is not a file.`);
    }
    if (info.size > BigInt(REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES)) {
      return yield* fileError(
        "fs.stat",
        `Diff file '${input.newPath}' exceeds the 1 MB expansion limit.`,
      );
    }
 
    const bytes = yield* fileSystem
      .readFile(realTarget)
      .pipe(
        Effect.mapError((cause) =>
          fileError("fs.readFile", `Could not read diff file '${input.newPath}'.`, cause),
        ),
      );
    if (bytes.includes(0)) {
      return yield* fileError("fs.readFile", `Cannot expand binary file '${input.newPath}'.`);
    }
    return new TextDecoder("utf-8").decode(bytes);
  });
 
  const getReviewDiffFileContents = Effect.fn("getReviewDiffFileContents")(function* (
    input: ReviewDiffFileContentsInput,
  ) {
    if (input.sourceKind === "working-tree") {
      const repositoryRoot = yield* runGitStdout(
        "GitVcsDriver.getReviewDiffFileContents.repositoryRoot",
        input.cwd,
        ["rev-parse", "--show-toplevel"],
      ).pipe(Effect.map((value) => value.trim()));
      if (repositoryRoot.length === 0) {
        return yield* reviewDiffFileError(input, "Could not resolve the Git repository root.");
      }
      const [oldContents, newContents] = yield* Effect.all(
        [
          input.changeType === "new"
            ? Effect.succeed("")
            : readReviewFileAtRevision(input, input.baseRef ?? "HEAD", input.oldPath),
          input.changeType === "deleted"
            ? Effect.succeed("")
            : readWorkingTreeReviewFile(input, repositoryRoot),
        ],
        { concurrency: 2 },
      );
      return { oldContents, newContents };
    }
 
    if (!input.baseRef || !input.headRef) {
      return yield* reviewDiffFileError(
        input,
        "Branch diff file expansion requires both base and head refs.",
      );
    }
    const mergeBase = yield* runGitStdout(
      "GitVcsDriver.getReviewDiffFileContents.mergeBase",
      input.cwd,
      ["merge-base", input.baseRef, input.headRef],
    ).pipe(Effect.map((value) => value.trim()));
    if (mergeBase.length === 0) {
      return yield* reviewDiffFileError(input, "Could not resolve the branch comparison base.");
    }
    const [oldContents, newContents] = yield* Effect.all(
      [
        input.changeType === "new"
          ? Effect.succeed("")
          : readReviewFileAtRevision(input, mergeBase, input.oldPath),
        input.changeType === "deleted"
          ? Effect.succeed("")
          : readReviewFileAtRevision(input, input.headRef, input.newPath),
      ],
      { concurrency: 2 },
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 · eb666ab2
});
export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type;
 
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,
Read this as: The wire contract carries worktree creation and removal inputs.
packages/contracts/src/git.ts:264–294 ↗verbatim · typescript · e05212a7
]);
export type VcsStatusStreamEvent = typeof VcsStatusStreamEvent.Type;
 
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.
   */
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 · 111e1c08
        Effect.gen(function* () {
          const bootstrap = command.bootstrap;
          const { bootstrap: _bootstrap, ...finalTurnStartCommand } = command;
          let createdThread = false;
          let targetProjectId = bootstrap?.createThread?.projectId;
          let targetProjectCwd = bootstrap?.prepareWorktree?.projectCwd;
          let targetWorktreePath = bootstrap?.createThread?.worktreePath ?? null;
 
          const cleanupCreatedThread = () =>
            createdThread
              ? serverCommandId("bootstrap-thread-delete").pipe(
                  Effect.flatMap((commandId) =>
                    dispatchFromClient({
                      type: "thread.delete",
                      commandId,
                      threadId: command.threadId,
                    }),
                  ),
                  Effect.as(true),
                )
              : Effect.succeed(false);
 
          const recordSetupScriptLaunchFailure = (input: {
            readonly error: ProjectSetupScriptRunner.ProjectSetupScriptRunnerError;
            readonly requestedAt: string;
            readonly worktreePath: string;
          }) => {
            const detail = projectSetupScriptCompatibilityDetail(input.error);
            return appendSetupScriptActivity({
              threadId: command.threadId,
              kind: "setup-script.failed",
              summary: "Setup script failed to start",
              createdAt: input.requestedAt,
              payload: {
                detail,
                worktreePath: input.worktreePath,
              },
              tone: "error",
            }).pipe(
              Effect.ignoreCause({ log: false }),
              Effect.flatMap(() =>
                Effect.logWarning("bootstrap turn start failed to launch setup script", {
                  threadId: command.threadId,
                  worktreePath: input.worktreePath,
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 · 3825dad1
          yield* fileSystem.exists(pathService.join(worktreePath, "shared", "SHARED.md")),
          true,
        );
      }),
    );
 
    it.effect("still creates the worktree when submodule checkout fails", () =>
      Effect.gen(function* () {
        const fileSystem = yield* FileSystem.FileSystem;
        const pathService = yield* Path.Path;
 
        const cwd = yield* makeTmpDir();
        const { initialBranch } = yield* initRepoWithCommit(cwd);
        // Points at a repository that does not exist, so the checkout fails the
        // way an unreachable private remote would. Creation must still succeed.
        yield* writeTextFile(
          cwd,
          ".gitmodules",
          '[submodule "missing"]\n\tpath = missing\n\turl = /nonexistent/repo.git\n',
        );
        yield* git(cwd, ["add", "."]);
        yield* git(cwd, ["commit", "-m", "add unreachable submodule"]);
 
        const worktreePath = pathService.join(
          yield* makeTmpDir("git-worktrees-"),
          "broken-submodule-worktree",
        );
        const driver = yield* GitVcsDriver.GitVcsDriver;
        const created = yield* driver.createWorktree({
          cwd,
          path: worktreePath,
          refName: initialBranch,
          newRefName: "feature/broken-submodules",
        });
 
        assert.equal(created.worktree.path, worktreePath);
        assert.equal(yield* fileSystem.exists(worktreePath), true);
      }),
    );
 
    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");
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 · 6a1953b4
    refresh: boolean,
  ) {
    while (true) {
      const generation = currentListRefsGeneration(gitCommonDir);
      const currentEpoch = listRefsEpochByCommonDir.get(gitCommonDir);
      const snapshot =
        refresh || currentEpoch === undefined
          ? // The refresh cache owns the complete snapshot read, rather than only the
            // epoch bump. Slow repositories therefore remain singleflight for the
            // entire Git scan even when more refresh requests arrive after the
            // coalescing TTL would otherwise have elapsed.
            yield* Cache.get(
              listRefsRefreshSnapshotCache,
              new GitRefsRefreshCacheKey({ gitCommonDir, generation }),
            )
          : yield* Cache.get(
              listRefsSnapshotCache,
              new GitRefsSnapshotCacheKey({ gitCommonDir, epoch: currentEpoch }),
            );
      if (currentListRefsGeneration(gitCommonDir) === generation) {
        return snapshot;
      }
    }
  });
  const invalidateListRefsSnapshot = Effect.fn("invalidateListRefsSnapshot")(function* (
    cwd: string,
  ) {
    const repositoryPathsCacheKey = normalizeRepositoryPathsCacheKey(cwd);
    const repositoryPaths = yield* Cache.get(repositoryPathsCache, repositoryPathsCacheKey);
    if (repositoryPaths === null) return;
    const previousGeneration = currentListRefsGeneration(repositoryPaths.gitCommonDir);
    bumpListRefsGeneration(repositoryPaths.gitCommonDir);
    bumpListRefsEpoch(repositoryPaths.gitCommonDir);
    yield* Cache.invalidate(
      listRefsRefreshSnapshotCache,
      new GitRefsRefreshCacheKey({
        gitCommonDir: repositoryPaths.gitCommonDir,
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 · 601e9978
                  operation: "resolveProject",
                  cause,
                }),
            ),
          )
        : null);
 
    if (!project) {
      return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);
    }
 
    const settings = yield* serverSettings.getSettings.pipe(
      Effect.mapError(
        (cause) =>
          new ProjectSetupScriptOperationError({
            ...errorContext,
            operation: "readSettings",
            cause,
          }),
      ),
    );
    const script = setupProjectScript(resolveProjectScripts(settings, project));
    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(
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 · a44de0a0
        exitSignal: null,
        label: "setup-default-setup",
        updatedAt: "2026-01-01T00:00:00.000Z",
      }),
    );
    const write = vi.fn(() => Effect.void);
    return Effect.gen(function* () {
      const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
      const result = yield* runner.runForThread({
        threadId: "thread-1",
        projectId: "project-1",
        worktreePath: "/repo/worktrees/a",
      });
      expect(result).toMatchObject({ status: "started", scriptId: "default-setup" });
      expect(open).toHaveBeenCalledWith({
        threadId: "thread-1",
        terminalId: "setup-default-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-default-setup",
        data: "npm install\r",
      });
    }).pipe(
      Effect.provide(
        testLayer(
          makeProject([]),
          { open, write },
          ServerSettings.layerTest({
            defaultProjectScripts: [
              {
                id: "default-setup",
                name: "Setup",
                command: "npm install",
                icon: "configure",
                runOnWorktreeCreate: true,
              },
            ],
          }),
        ),
      ),
    );
  });
 
  it.effect("returns no-script when no setup script exists", () => {
    const open = vi.fn(() => Effect.die("unexpected open"));
    const write = vi.fn(() => Effect.die("unexpected write"));
    const project = makeProject([]);
 
    return Effect.gen(function* () {
      const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
      const result = yield* runner.runForThread({
        threadId: "thread-1",
        projectId: "project-1",
        worktreePath: "/repo/worktrees/a",
      });
Read this as: The test asserts deterministic setup terminal creation, cwd, both environment labels, and command write.
Figure 37.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 37.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 · a2729d8d
      const { commitSha: headCommit } = yield* resolveCommit({ cwd: input.cwd, revision: "HEAD" });
      if (headCommit === input.targetCommit) {
        return { headCommit, moved: false, onTarget: true };
      }
 
      const worktreeChanges = yield* runGitStdout(
        "GitVcsDriver.refreshCheckedOutBranch.status",
        input.cwd,
        ["status", "--porcelain"],
      );
      if (worktreeChanges.trim().length > 0) {
        return { headCommit, moved: false, onTarget: false };
      }
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 · c616bd3d
 
        const refs = yield* driver.listRefs({ cwd });
        const remoteDefault = refs.refs.find((ref) => ref.name === `origin/${initialBranch}`);
        assert.equal(remoteDefault?.isRemote, true);
        assert.equal(remoteDefault?.isDefault, true);
      }),
    );
 
    it.effect("creates, checks out, renames, and lists refs", () =>
      Effect.gen(function* () {
        const cwd = yield* makeTmpDir();
        yield* initRepoWithCommit(cwd);
        const driver = yield* GitVcsDriver.GitVcsDriver;
 
        yield* driver.createRef({ cwd, refName: "feature/original" });
        const switchRef = yield* driver.switchRef({ cwd, refName: "feature/original" });
        assert.equal(switchRef.refName, "feature/original");
 
        const renamed = yield* driver.renameBranch({
          cwd,
          oldBranch: "feature/original",
          newBranch: "feature/renamed",
        });
        assert.equal(renamed.branch, "feature/renamed");
        assert.equal(yield* git(cwd, ["branch", "--show-current"]), "feature/renamed");
 
        const refs = yield* driver.listRefs({ cwd });
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 · 00a9cb17
              ),
            );
          };
 
          const recordSetupScriptStarted = (input: {
            readonly requestedAt: string;
            readonly worktreePath: string;
            readonly scriptId: string;
            readonly scriptName: string;
            readonly terminalId: string;
          }) =>
            Effect.gen(function* () {
              const startedAt = yield* nowIso;
              const payload = {
                scriptId: input.scriptId,
                scriptName: input.scriptName,
                terminalId: input.terminalId,
                worktreePath: input.worktreePath,
              };
              yield* Effect.all([
                appendSetupScriptActivity({
                  threadId: command.threadId,
                  kind: "setup-script.requested",
                  summary: "Starting setup script",
                  createdAt: input.requestedAt,
                  payload,
                  tone: "info",
                }),
                appendSetupScriptActivity({
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 859304b78 · 10 Sept 2026
Book search

Find a concept, module, or source path

Type two or more characters.