authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-06 20:13:10-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-06 20:13:10-04:00
log331c6dce7f6745863752b1b423fc76d24014deec
tree47b28933c5e42cc60f431c8f1df40792402ed205
parent4f5834ba72c5905a318c00456dd21271b2fa9d6f
signature Signed by PGP key B5690EEEBB952194

fix(server): skip origin fetch when creating worktrees in repos without an origin remote (#5556)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

5 files changed, 143 insertions(+), 3 deletions(-)

apps/server/src/git/GitWorkflowService.ts+8
...@@ -69,6 +69,10 @@ export class GitWorkflowService extends Context.Service<...@@ -69,6 +69,10 @@ export class GitWorkflowService extends Context.Service<
69 readonly cwd: string;69 readonly cwd: string;
70 readonly remoteName: string;70 readonly remoteName: string;
71 }) => Effect.Effect<void, GitCommandError>;71 }) => Effect.Effect<void, GitCommandError>;
72 readonly remoteExists: (input: {
73 readonly cwd: string;
74 readonly remoteName: string;
75 }) => Effect.Effect<boolean, GitCommandError>;
72 readonly resolveRemoteTrackingCommit: (input: {76 readonly resolveRemoteTrackingCommit: (input: {
73 readonly cwd: string;77 readonly cwd: string;
74 readonly refName: string;78 readonly refName: string;
...@@ -303,6 +307,10 @@ export const make = Effect.gen(function* () {...@@ -303,6 +307,10 @@ export const make = Effect.gen(function* () {
303 ensureGitCommand("GitWorkflowService.fetchRemote", input.cwd).pipe(307 ensureGitCommand("GitWorkflowService.fetchRemote", input.cwd).pipe(
304 Effect.andThen(git.fetchRemote(input)),308 Effect.andThen(git.fetchRemote(input)),
305 ),309 ),
310 remoteExists: (input) =>
311 ensureGitCommand("GitWorkflowService.remoteExists", input.cwd).pipe(
312 Effect.andThen(git.remoteExists(input)),
313 ),
306 resolveRemoteTrackingCommit: (input) =>314 resolveRemoteTrackingCommit: (input) =>
307 ensureGitCommand("GitWorkflowService.resolveRemoteTrackingCommit", input.cwd).pipe(315 ensureGitCommand("GitWorkflowService.resolveRemoteTrackingCommit", input.cwd).pipe(
308 Effect.andThen(git.resolveRemoteTrackingCommit(input)),316 Effect.andThen(git.resolveRemoteTrackingCommit(input)),
apps/server/src/server.test.ts+113
...@@ -7134,6 +7134,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -7134,6 +7134,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
7134 pr: null,7134 pr: null,
7135 }),7135 }),
7136 );7136 );
7137 const remoteExists = vi.fn(
7138 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
7139 Effect.sync(() => {
7140 bootstrapGitOperations.push("remote-exists");
7141 return true;
7142 }),
7143 );
7137 const fetchRemote = vi.fn(7144 const fetchRemote = vi.fn(
7138 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) =>7145 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) =>
7139 Effect.sync(() => {7146 Effect.sync(() => {
...@@ -7181,6 +7188,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -7181,6 +7188,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
7181 yield* buildAppUnderTest({7188 yield* buildAppUnderTest({
7182 layers: {7189 layers: {
7183 gitVcsDriver: {7190 gitVcsDriver: {
7191 remoteExists,
7184 fetchRemote,7192 fetchRemote,
7185 resolveRemoteTrackingCommit,7193 resolveRemoteTrackingCommit,
7186 createWorktree,7194 createWorktree,
...@@ -7271,6 +7279,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -7271,6 +7279,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
7271 fallbackRemoteName: "origin",7279 fallbackRemoteName: "origin",
7272 });7280 });
7273 assert.deepEqual(bootstrapGitOperations, [7281 assert.deepEqual(bootstrapGitOperations, [
7282 "remote-exists",
7274 "fetch",7283 "fetch",
7275 "resolve-remote-commit",7284 "resolve-remote-commit",
7276 "create-worktree",7285 "create-worktree",
...@@ -7299,6 +7308,110 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -7299,6 +7308,110 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
7299 }).pipe(Effect.provide(NodeHttpServer.layerTest)),7308 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
7300 );7309 );
73017310
7311 it.effect(
7312 "falls back to the local base branch when startFromOrigin is set but no origin remote exists",
7313 () =>
7314 Effect.gen(function* () {
7315 const dispatchedCommands: Array<OrchestrationCommand> = [];
7316 const remoteExists = vi.fn(
7317 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
7318 Effect.succeed(false),
7319 );
7320 const fetchRemote = vi.fn(
7321 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) => Effect.void,
7322 );
7323 const resolveRemoteTrackingCommit = vi.fn(
7324 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
7325 Effect.succeed({
7326 commitSha: "0123456789abcdef0123456789abcdef01234567",
7327 remoteRefName: "origin/main",
7328 }),
7329 );
7330 const createWorktree = vi.fn(
7331 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
7332 Effect.succeed({
7333 worktree: {
7334 refName: "t3code/bootstrap-refName",
7335 path: "/tmp/bootstrap-worktree",
7336 },
7337 }),
7338 );
7339
7340 yield* buildAppUnderTest({
7341 layers: {
7342 gitVcsDriver: {
7343 remoteExists,
7344 fetchRemote,
7345 resolveRemoteTrackingCommit,
7346 createWorktree,
7347 },
7348 orchestrationEngine: {
7349 dispatch: (command) =>
7350 Effect.sync(() => {
7351 dispatchedCommands.push(command);
7352 return { sequence: dispatchedCommands.length };
7353 }),
7354 readEvents: () => Stream.empty,
7355 },
7356 },
7357 });
7358
7359 const createdAt = "2026-01-01T00:00:00.000Z";
7360 const wsUrl = yield* getWsServerUrl("/ws");
7361 yield* Effect.scoped(
7362 withWsRpcClient(wsUrl, (client) =>
7363 client[ORCHESTRATION_WS_METHODS.dispatchCommand]({
7364 type: "thread.turn.start",
7365 commandId: CommandId.make("cmd-bootstrap-turn-start-no-origin"),
7366 threadId: ThreadId.make("thread-bootstrap-no-origin"),
7367 message: {
7368 messageId: MessageId.make("msg-bootstrap-no-origin"),
7369 role: "user",
7370 text: "hello",
7371 attachments: [],
7372 },
7373 modelSelection: defaultModelSelection,
7374 runtimeMode: "full-access",
7375 interactionMode: "default",
7376 bootstrap: {
7377 createThread: {
7378 projectId: defaultProjectId,
7379 title: "Bootstrap Thread",
7380 modelSelection: defaultModelSelection,
7381 runtimeMode: "full-access",
7382 interactionMode: "default",
7383 branch: "main",
7384 worktreePath: null,
7385 createdAt,
7386 },
7387 prepareWorktree: {
7388 projectCwd: "/tmp/project",
7389 baseBranch: "main",
7390 branch: "t3code/bootstrap-refName",
7391 startFromOrigin: true,
7392 },
7393 },
7394 createdAt,
7395 }),
7396 ),
7397 );
7398
7399 assert.deepEqual(remoteExists.mock.calls[0]?.[0], {
7400 cwd: "/tmp/project",
7401 remoteName: "origin",
7402 });
7403 assert.equal(fetchRemote.mock.calls.length, 0);
7404 assert.equal(resolveRemoteTrackingCommit.mock.calls.length, 0);
7405 assert.deepEqual(createWorktree.mock.calls[0]?.[0], {
7406 cwd: "/tmp/project",
7407 refName: "main",
7408 newRefName: "t3code/bootstrap-refName",
7409 baseRefName: "main",
7410 path: null,
7411 });
7412 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
7413 );
7414
7302 it.effect("records setup-script failures without aborting bootstrap turn start", () =>7415 it.effect("records setup-script failures without aborting bootstrap turn start", () =>
7303 Effect.gen(function* () {7416 Effect.gen(function* () {
7304 const dispatchedCommands: Array<OrchestrationCommand> = [];7417 const dispatchedCommands: Array<OrchestrationCommand> = [];
apps/server/src/vcs/GitVcsDriver.ts+6
...@@ -168,6 +168,11 @@ export interface GitFetchRemoteInput {...@@ -168,6 +168,11 @@ export interface GitFetchRemoteInput {
168 remoteName: string;168 remoteName: string;
169}169}
170170
171export interface GitRemoteExistsInput {
172 cwd: string;
173 remoteName: string;
174}
175
171export interface GitResolveRemoteTrackingCommitInput {176export interface GitResolveRemoteTrackingCommitInput {
172 cwd: string;177 cwd: string;
173 refName: string;178 refName: string;
...@@ -243,6 +248,7 @@ export class GitVcsDriver extends Context.Service<...@@ -243,6 +248,7 @@ export class GitVcsDriver extends Context.Service<
243 readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>;248 readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>;
244 readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>;249 readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>;
245 readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>;250 readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>;
251 readonly remoteExists: (input: GitRemoteExistsInput) => Effect.Effect<boolean, GitCommandError>;
246 readonly resolveRemoteTrackingCommit: (252 readonly resolveRemoteTrackingCommit: (
247 input: GitResolveRemoteTrackingCommitInput,253 input: GitResolveRemoteTrackingCommitInput,
248 ) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>;254 ) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>;
apps/server/src/vcs/GitVcsDriverCore.ts+6-2
...@@ -1284,11 +1284,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1284,11 +1284,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1284 },1284 },
1285 ).pipe(Effect.map((result) => result.exitCode === 0));1285 ).pipe(Effect.map((result) => result.exitCode === 0));
12861286
1287 const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>1287 const remoteExists: GitVcsDriver.GitVcsDriver["Service"]["remoteExists"] = (input) =>
1288 executeGit("GitVcsDriver.originRemoteExists", cwd, ["remote", "get-url", "origin"], {1288 executeGit("GitVcsDriver.remoteExists", input.cwd, ["remote", "get-url", input.remoteName], {
1289 allowNonZeroExit: true,1289 allowNonZeroExit: true,
1290 }).pipe(Effect.map((result) => result.exitCode === 0));1290 }).pipe(Effect.map((result) => result.exitCode === 0));
12911291
1292 const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>
1293 remoteExists({ cwd, remoteName: "origin" });
1294
1292 const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>1295 const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>
1293 runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(1296 runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(
1294 Effect.map(parseRemoteNamesInGitOrder),1297 Effect.map(parseRemoteNamesInGitOrder),
...@@ -3071,6 +3074,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -3071,6 +3074,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
3071 ensureRemote: (input) => withListRefsInvalidation(input.cwd, ensureRemote(input)),3074 ensureRemote: (input) => withListRefsInvalidation(input.cwd, ensureRemote(input)),
3072 resolvePrimaryRemoteName,3075 resolvePrimaryRemoteName,
3073 fetchRemote: (input) => withListRefsInvalidation(input.cwd, fetchRemote(input)),3076 fetchRemote: (input) => withListRefsInvalidation(input.cwd, fetchRemote(input)),
3077 remoteExists,
3074 resolveRemoteTrackingCommit,3078 resolveRemoteTrackingCommit,
3075 fetchRemoteBranch: (input) => withListRefsInvalidation(input.cwd, fetchRemoteBranch(input)),3079 fetchRemoteBranch: (input) => withListRefsInvalidation(input.cwd, fetchRemoteBranch(input)),
3076 fetchRemoteTrackingBranch: (input) =>3080 fetchRemoteTrackingBranch: (input) =>
apps/server/src/ws.ts+10-1
...@@ -908,7 +908,16 @@ const makeWsRpcLayer = (...@@ -908,7 +908,16 @@ const makeWsRpcLayer = (
908908
909 if (bootstrap?.prepareWorktree) {909 if (bootstrap?.prepareWorktree) {
910 let worktreeBaseRef = bootstrap.prepareWorktree.baseBranch;910 let worktreeBaseRef = bootstrap.prepareWorktree.baseBranch;
911 if (bootstrap.prepareWorktree.startFromOrigin) {911 // "Start from origin" is a stored default; repos without an
912 // origin remote fall back to the local base branch instead of
913 // failing the whole bootstrap on `git fetch origin`.
914 const startFromOrigin =
915 bootstrap.prepareWorktree.startFromOrigin === true &&
916 (yield* gitWorkflow.remoteExists({
917 cwd: bootstrap.prepareWorktree.projectCwd,
918 remoteName: "origin",
919 }));
920 if (startFromOrigin) {
912 yield* gitWorkflow.fetchRemote({921 yield* gitWorkflow.fetchRemote({
913 cwd: bootstrap.prepareWorktree.projectCwd,922 cwd: bootstrap.prepareWorktree.projectCwd,
914 remoteName: "origin",923 remoteName: "origin",