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.
export function resolveSendEnvMode(input: {
requestedEnvMode: DraftThreadEnvMode;
isGitRepo: boolean;
}): DraftThreadEnvMode {
return input.isGitRepo ? input.requestedEnvMode : "local";
}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.
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,
},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.
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;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;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:
- Start with the selected base branch.
- If “start from origin” is requested and an
originremote exists, fetch origin and resolve the selected base to a remote-tracking commit. - 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.
- 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.
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);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.
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,
);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.
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,
},
};
});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.
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;
}); 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 })));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.
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.
apps/web/src/components/ChatView.logic.ts:322–327 ↗apps/mobile/src/lib/projectThreadStartTurn.ts:50–88 ↗apps/server/src/checkpointing/Utils.ts:12–27 ↗apps/server/src/ws.ts:979–1022 ↗apps/server/src/vcs/GitVcsDriverCore.ts:2763–2799 ↗apps/server/src/project/ProjectSetupScriptRunner.ts:123–183 ↗apps/server/src/ws.ts:846–959 ↗apps/server/src/ws.ts:1025–1053 ↗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.
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",
});
}); 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);
}),
);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.
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,
),
}),
);
}),
);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.
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
- No Git worktree creation
- Stored thread path, if present
- Otherwise project root
- 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
- 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.
- 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.
- 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.
- 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.
- 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.