Unified · Split
| @@ -69,6 +69,10 @@ export class GitWorkflowService extends Context.Service< · 68 unmodified lines | ||
| 69 | 69 | readonly cwd: string; |
| 70 | 70 | readonly remoteName: string; |
| 71 | 71 | }) => Effect.Effect<void, GitCommandError>; |
| 72 | + readonly remoteExists: (input: { | |
| 73 | + readonly cwd: string; | |
| 74 | + readonly remoteName: string; | |
| 75 | + }) => Effect.Effect<boolean, GitCommandError>; | |
| 72 | 76 | readonly resolveRemoteTrackingCommit: (input: { |
| 73 | 77 | readonly cwd: string; |
| 74 | 78 | readonly refName: string; |
| @@ -303,6 +307,10 @@ export const make = Effect.gen(function* () { · 228 unmodified lines | ||
| 303 | 307 | ensureGitCommand("GitWorkflowService.fetchRemote", input.cwd).pipe( |
| 304 | 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 | 314 | resolveRemoteTrackingCommit: (input) => |
| 307 | 315 | ensureGitCommand("GitWorkflowService.resolveRemoteTrackingCommit", input.cwd).pipe( |
| 308 | 316 | Effect.andThen(git.resolveRemoteTrackingCommit(input)), |
| @@ -7134,6 +7134,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 7133 unmodified lines | ||
| 7134 | 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 | 7144 | const fetchRemote = vi.fn( |
| 7138 | 7145 | (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) => |
| 7139 | 7146 | Effect.sync(() => { |
| @@ -7181,6 +7188,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 41 unmodified lines | ||
| 7181 | 7188 | yield* buildAppUnderTest({ |
| 7182 | 7189 | layers: { |
| 7183 | 7190 | gitVcsDriver: { |
| 7191 | + remoteExists, | |
| 7184 | 7192 | fetchRemote, |
| 7185 | 7193 | resolveRemoteTrackingCommit, |
| 7186 | 7194 | createWorktree, |
| @@ -7271,6 +7279,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 84 unmodified lines | ||
| 7271 | 7279 | fallbackRemoteName: "origin", |
| 7272 | 7280 | }); |
| 7273 | 7281 | assert.deepEqual(bootstrapGitOperations, [ |
| 7282 | + "remote-exists", | |
| 7274 | 7283 | "fetch", |
| 7275 | 7284 | "resolve-remote-commit", |
| 7276 | 7285 | "create-worktree", |
| @@ -7299,6 +7308,110 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 22 unmodified lines | ||
| 7299 | 7308 | }).pipe(Effect.provide(NodeHttpServer.layerTest)), |
| 7300 | 7309 | ); |
| 7301 | 7310 | |
| 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 | 7415 | it.effect("records setup-script failures without aborting bootstrap turn start", () => |
| 7303 | 7416 | Effect.gen(function* () { |
| 7304 | 7417 | const dispatchedCommands: Array<OrchestrationCommand> = []; |
| @@ -168,6 +168,11 @@ export interface GitFetchRemoteInput { · 167 unmodified lines | ||
| 168 | 168 | remoteName: string; |
| 169 | 169 | } |
| 170 | 170 | |
| 171 | +export interface GitRemoteExistsInput { | |
| 172 | + cwd: string; | |
| 173 | + remoteName: string; | |
| 174 | +} | |
| 175 | + | |
| 171 | 176 | export interface GitResolveRemoteTrackingCommitInput { |
| 172 | 177 | cwd: string; |
| 173 | 178 | refName: string; |
| @@ -243,6 +248,7 @@ export class GitVcsDriver extends Context.Service< · 69 unmodified lines | ||
| 243 | 248 | readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>; |
| 244 | 249 | readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>; |
| 245 | 250 | readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>; |
| 251 | + readonly remoteExists: (input: GitRemoteExistsInput) => Effect.Effect<boolean, GitCommandError>; | |
| 246 | 252 | readonly resolveRemoteTrackingCommit: ( |
| 247 | 253 | input: GitResolveRemoteTrackingCommitInput, |
| 248 | 254 | ) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>; |
| @@ -1284,11 +1284,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1283 unmodified lines | ||
| 1284 | 1284 | }, |
| 1285 | 1285 | ).pipe(Effect.map((result) => result.exitCode === 0)); |
| 1286 | 1286 | |
| 1287 | − const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> => | |
| 1288 | − executeGit("GitVcsDriver.originRemoteExists", cwd, ["remote", "get-url", "origin"], { | |
| 1287 | + const remoteExists: GitVcsDriver.GitVcsDriver["Service"]["remoteExists"] = (input) => | |
| 1288 | + executeGit("GitVcsDriver.remoteExists", input.cwd, ["remote", "get-url", input.remoteName], { | |
| 1289 | 1289 | allowNonZeroExit: true, |
| 1290 | 1290 | }).pipe(Effect.map((result) => result.exitCode === 0)); |
| 1291 | 1291 | |
| 1292 | + const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> => | |
| 1293 | + remoteExists({ cwd, remoteName: "origin" }); | |
| 1294 | + | |
| 1292 | 1295 | const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> => |
| 1293 | 1296 | runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe( |
| 1294 | 1297 | Effect.map(parseRemoteNamesInGitOrder), |
| @@ -3071,6 +3074,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1776 unmodified lines | ||
| 3071 | 3074 | ensureRemote: (input) => withListRefsInvalidation(input.cwd, ensureRemote(input)), |
| 3072 | 3075 | resolvePrimaryRemoteName, |
| 3073 | 3076 | fetchRemote: (input) => withListRefsInvalidation(input.cwd, fetchRemote(input)), |
| 3077 | + remoteExists, | |
| 3074 | 3078 | resolveRemoteTrackingCommit, |
| 3075 | 3079 | fetchRemoteBranch: (input) => withListRefsInvalidation(input.cwd, fetchRemoteBranch(input)), |
| 3076 | 3080 | fetchRemoteTrackingBranch: (input) => |
| @@ -908,7 +908,16 @@ const makeWsRpcLayer = ( · 907 unmodified lines | ||
| 908 | 908 | |
| 909 | 909 | if (bootstrap?.prepareWorktree) { |
| 910 | 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 | 921 | yield* gitWorkflow.fetchRemote({ |
| 913 | 922 | cwd: bootstrap.prepareWorktree.projectCwd, |
| 914 | 923 | remoteName: "origin", |