From 2a0c57695d15718115e3b3bc6d7e6667207a21c7 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 6 Aug 2026 17:07:49 -0700 Subject: [PATCH] fix(server): skip origin fetch when creating worktrees in repos without an origin remote Worktree bootstrap ran `git fetch origin` whenever the "start from origin" default was on, which failed outright in repos that have no origin remote. Check for the remote first and fall back to the local base branch when it is missing. Co-Authored-By: Claude Fable 5 --- apps/server/src/git/GitWorkflowService.ts | 8 ++ apps/server/src/server.test.ts | 113 ++++++++++++++++++++++ apps/server/src/vcs/GitVcsDriver.ts | 6 ++ apps/server/src/vcs/GitVcsDriverCore.ts | 8 +- apps/server/src/ws.ts | 11 ++- 5 files changed, 143 insertions(+), 3 deletions(-) diff --git a/apps/server/src/git/GitWorkflowService.ts b/apps/server/src/git/GitWorkflowService.ts index 100b9beadba..da22794951f 100644 --- a/apps/server/src/git/GitWorkflowService.ts +++ b/apps/server/src/git/GitWorkflowService.ts @@ -69,6 +69,10 @@ export class GitWorkflowService extends Context.Service< readonly cwd: string; readonly remoteName: string; }) => Effect.Effect; + readonly remoteExists: (input: { + readonly cwd: string; + readonly remoteName: string; + }) => Effect.Effect; readonly resolveRemoteTrackingCommit: (input: { readonly cwd: string; readonly refName: string; @@ -303,6 +307,10 @@ export const make = Effect.gen(function* () { ensureGitCommand("GitWorkflowService.fetchRemote", input.cwd).pipe( Effect.andThen(git.fetchRemote(input)), ), + remoteExists: (input) => + ensureGitCommand("GitWorkflowService.remoteExists", input.cwd).pipe( + Effect.andThen(git.remoteExists(input)), + ), resolveRemoteTrackingCommit: (input) => ensureGitCommand("GitWorkflowService.resolveRemoteTrackingCommit", input.cwd).pipe( Effect.andThen(git.resolveRemoteTrackingCommit(input)), diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 8628aeef314..a403e228b06 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -7134,6 +7134,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => { pr: null, }), ); + const remoteExists = vi.fn( + (_: Parameters[0]) => + Effect.sync(() => { + bootstrapGitOperations.push("remote-exists"); + return true; + }), + ); const fetchRemote = vi.fn( (_: Parameters[0]) => Effect.sync(() => { @@ -7181,6 +7188,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { yield* buildAppUnderTest({ layers: { gitVcsDriver: { + remoteExists, fetchRemote, resolveRemoteTrackingCommit, createWorktree, @@ -7271,6 +7279,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { fallbackRemoteName: "origin", }); assert.deepEqual(bootstrapGitOperations, [ + "remote-exists", "fetch", "resolve-remote-commit", "create-worktree", @@ -7299,6 +7308,110 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + it.effect( + "falls back to the local base branch when startFromOrigin is set but no origin remote exists", + () => + Effect.gen(function* () { + const dispatchedCommands: Array = []; + const remoteExists = vi.fn( + (_: Parameters[0]) => + Effect.succeed(false), + ); + const fetchRemote = vi.fn( + (_: Parameters[0]) => Effect.void, + ); + const resolveRemoteTrackingCommit = vi.fn( + (_: Parameters[0]) => + Effect.succeed({ + commitSha: "0123456789abcdef0123456789abcdef01234567", + remoteRefName: "origin/main", + }), + ); + const createWorktree = vi.fn( + (_: Parameters[0]) => + Effect.succeed({ + worktree: { + refName: "t3code/bootstrap-refName", + path: "/tmp/bootstrap-worktree", + }, + }), + ); + + yield* buildAppUnderTest({ + layers: { + gitVcsDriver: { + remoteExists, + fetchRemote, + resolveRemoteTrackingCommit, + createWorktree, + }, + orchestrationEngine: { + dispatch: (command) => + Effect.sync(() => { + dispatchedCommands.push(command); + return { sequence: dispatchedCommands.length }; + }), + readEvents: () => Stream.empty, + }, + }, + }); + + const createdAt = "2026-01-01T00:00:00.000Z"; + const wsUrl = yield* getWsServerUrl("/ws"); + yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-bootstrap-turn-start-no-origin"), + threadId: ThreadId.make("thread-bootstrap-no-origin"), + message: { + messageId: MessageId.make("msg-bootstrap-no-origin"), + role: "user", + text: "hello", + attachments: [], + }, + modelSelection: defaultModelSelection, + runtimeMode: "full-access", + interactionMode: "default", + bootstrap: { + createThread: { + projectId: defaultProjectId, + title: "Bootstrap Thread", + modelSelection: defaultModelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: "main", + worktreePath: null, + createdAt, + }, + prepareWorktree: { + projectCwd: "/tmp/project", + baseBranch: "main", + branch: "t3code/bootstrap-refName", + startFromOrigin: true, + }, + }, + createdAt, + }), + ), + ); + + assert.deepEqual(remoteExists.mock.calls[0]?.[0], { + cwd: "/tmp/project", + remoteName: "origin", + }); + assert.equal(fetchRemote.mock.calls.length, 0); + assert.equal(resolveRemoteTrackingCommit.mock.calls.length, 0); + assert.deepEqual(createWorktree.mock.calls[0]?.[0], { + cwd: "/tmp/project", + refName: "main", + newRefName: "t3code/bootstrap-refName", + baseRefName: "main", + path: null, + }); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + it.effect("records setup-script failures without aborting bootstrap turn start", () => Effect.gen(function* () { const dispatchedCommands: Array = []; diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 192efe5a7d0..f256a7dd4e1 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -168,6 +168,11 @@ export interface GitFetchRemoteInput { remoteName: string; } +export interface GitRemoteExistsInput { + cwd: string; + remoteName: string; +} + export interface GitResolveRemoteTrackingCommitInput { cwd: string; refName: string; @@ -243,6 +248,7 @@ export class GitVcsDriver extends Context.Service< readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect; readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect; readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect; + readonly remoteExists: (input: GitRemoteExistsInput) => Effect.Effect; readonly resolveRemoteTrackingCommit: ( input: GitResolveRemoteTrackingCommitInput, ) => Effect.Effect; diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index abcb10a8c9a..d39817c0ee1 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1284,11 +1284,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }, ).pipe(Effect.map((result) => result.exitCode === 0)); - const originRemoteExists = (cwd: string): Effect.Effect => - executeGit("GitVcsDriver.originRemoteExists", cwd, ["remote", "get-url", "origin"], { + const remoteExists: GitVcsDriver.GitVcsDriver["Service"]["remoteExists"] = (input) => + executeGit("GitVcsDriver.remoteExists", input.cwd, ["remote", "get-url", input.remoteName], { allowNonZeroExit: true, }).pipe(Effect.map((result) => result.exitCode === 0)); + const originRemoteExists = (cwd: string): Effect.Effect => + remoteExists({ cwd, remoteName: "origin" }); + const listRemoteNames = (cwd: string): Effect.Effect, GitCommandError> => runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe( Effect.map(parseRemoteNamesInGitOrder), @@ -3071,6 +3074,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ensureRemote: (input) => withListRefsInvalidation(input.cwd, ensureRemote(input)), resolvePrimaryRemoteName, fetchRemote: (input) => withListRefsInvalidation(input.cwd, fetchRemote(input)), + remoteExists, resolveRemoteTrackingCommit, fetchRemoteBranch: (input) => withListRefsInvalidation(input.cwd, fetchRemoteBranch(input)), fetchRemoteTrackingBranch: (input) => diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index fc65602679a..a04fce3fd2c 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -908,7 +908,16 @@ const makeWsRpcLayer = ( if (bootstrap?.prepareWorktree) { let worktreeBaseRef = bootstrap.prepareWorktree.baseBranch; - if (bootstrap.prepareWorktree.startFromOrigin) { + // "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",