| author | |
| committer | |
| log | 6266af3f1f688497ef9717b87b82eafc5240418c |
| tree | 5baf2b101d0ab2ee3ef286d99ee263bce5de6136 |
| parent | c77a7b7eebd6ea8a3a63bbcaaa7c1707fe65c86e |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 309 insertions(+), 1 deletions(-)
apps/server/src/git/GitWorkflowService.test.ts+48| ... | ... | @@ -6,6 +6,11 @@ import * as Option from "effect/Option"; |
| 6 | 6 | |
| 7 | 7 | import { VcsRepositoryDetectionError } from "@t3tools/contracts"; |
| 8 | 8 | |
| 9 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 10 | import * as FileSystem from "effect/FileSystem"; | |
| 11 | ||
| 12 | import * as ServerConfig from "../config.ts"; | |
| 13 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | |
| 9 | 14 | import * as GitManager from "./GitManager.ts"; |
| 10 | 15 | import * as GitWorkflowService from "./GitWorkflowService.ts"; |
| 11 | 16 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| ... | ... | @@ -220,4 +225,47 @@ describe("GitWorkflowService", () => { |
| 220 | 225 | ), |
| 221 | 226 | ); |
| 222 | 227 | }); |
| 228 | ||
| 229 | it.effect("finds a name among the refs of every namespace", () => | |
| 230 | Effect.gen(function* () { | |
| 231 | const fileSystem = yield* FileSystem.FileSystem; | |
| 232 | const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ref-named-" }); | |
| 233 | const workflow = yield* GitWorkflowService.GitWorkflowService; | |
| 234 | const driver = yield* GitVcsDriver.GitVcsDriver; | |
| 235 | for (const args of [ | |
| 236 | ["init", "-b", "main"], | |
| 237 | ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "--allow-empty", "-m", "init"], | |
| 238 | ["update-ref", "refs/remotes/upstream/feature/topic", "HEAD"], | |
| 239 | ["symbolic-ref", "refs/remotes/origin/alias", "refs/heads/main"], | |
| 240 | ["update-ref", "refs/mirror/origin/mirrored", "HEAD"], | |
| 241 | ]) { | |
| 242 | yield* driver.execute({ operation: "test.setupRepo", cwd, args }); | |
| 243 | } | |
| 244 | const found = yield* Effect.forEach( | |
| 245 | ["main", "feature/topic", "alias", "mirrored", "t3/renamed", "feature/*"], | |
| 246 | (refName) => workflow.hasRefNamed({ cwd, refName }), | |
| 247 | ); | |
| 248 | assert.deepEqual(found, [true, true, true, true, false, false]); | |
| 249 | }).pipe( | |
| 250 | Effect.scoped, | |
| 251 | Effect.provide( | |
| 252 | GitWorkflowService.layer.pipe( | |
| 253 | Layer.provide( | |
| 254 | Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ | |
| 255 | resolve: () => Effect.succeed({ kind: "git" } as VcsDriverRegistry.VcsDriverHandle), | |
| 256 | }), | |
| 257 | ), | |
| 258 | Layer.provide(Layer.mock(GitManager.GitManager)({})), | |
| 259 | Layer.provideMerge(GitVcsDriver.layer), | |
| 260 | Layer.provideMerge(VcsProcess.layer), | |
| 261 | Layer.provideMerge( | |
| 262 | ServerConfig.layerTest(process.cwd(), { prefix: "t3-ref-named-" }).pipe( | |
| 263 | Layer.provide(NodeServices.layer), | |
| 264 | ), | |
| 265 | ), | |
| 266 | Layer.provideMerge(NodeServices.layer), | |
| 267 | ), | |
| 268 | ), | |
| 269 | ), | |
| 270 | ); | |
| 223 | 271 | }); |
apps/server/src/git/GitWorkflowService.ts+28| ... | ... | @@ -40,6 +40,15 @@ export class GitWorkflowService extends Context.Service< |
| 40 | 40 | readonly cwd: string; |
| 41 | 41 | readonly refName: string; |
| 42 | 42 | }) => Effect.Effect<boolean, GitCommandError>; |
| 43 | /** | |
| 44 | * Whether a ref in any namespace ends in this name: a local branch, a | |
| 45 | * remote-tracking branch of any remote, a tag. Git falls back to these | |
| 46 | * when a branch name is not a revision by itself. | |
| 47 | */ | |
| 48 | readonly hasRefNamed: (input: { | |
| 49 | readonly cwd: string; | |
| 50 | readonly refName: string; | |
| 51 | }) => Effect.Effect<boolean, GitCommandError>; | |
| 43 | 52 | readonly status: ( |
| 44 | 53 | input: VcsStatusInput, |
| 45 | 54 | ) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; |
| ... | ... | @@ -300,6 +309,25 @@ export const make = Effect.gen(function* () { |
| 300 | 309 | ), |
| 301 | 310 | Effect.map((result) => result.exitCode === 0), |
| 302 | 311 | ), |
| 312 | hasRefNamed: (input) => | |
| 313 | // No ref name holds a pattern character, and one here would widen the match. | |
| 314 | /[*?[\\]/.test(input.refName) | |
| 315 | ? Effect.succeed(false) | |
| 316 | : ensureGitCommand("GitWorkflowService.hasRefNamed", input.cwd).pipe( | |
| 317 | Effect.andThen( | |
| 318 | git.execute({ | |
| 319 | operation: "GitWorkflowService.hasRefNamed", | |
| 320 | cwd: input.cwd, | |
| 321 | args: [ | |
| 322 | "for-each-ref", | |
| 323 | "--count=1", | |
| 324 | "--format=%(refname)", | |
| 325 | `refs/**/${input.refName}`, | |
| 326 | ], | |
| 327 | }), | |
| 328 | ), | |
| 329 | Effect.map((result) => result.stdout.trim().length > 0), | |
| 330 | ), | |
| 303 | 331 | status: (input) => |
| 304 | 332 | detectGitRepositoryForStatus("GitWorkflowService.status", input.cwd).pipe( |
| 305 | 333 | Effect.flatMap((isGitRepository) => |
apps/server/src/mcp/toolkits/project/handlers.test.ts+46| ... | ... | @@ -301,6 +301,7 @@ const clientLaunchHarness = (input: { |
| 301 | 301 | readonly runtimeModeCeiling: "approval-required" | "auto-accept-edits" | "auto" | "full-access"; |
| 302 | 302 | readonly launched: Array<ThreadLaunch.ThreadLaunchInput>; |
| 303 | 303 | readonly workspaceRoot?: string; |
| 304 | readonly checkWorktreeBase?: ThreadLaunch.ThreadLaunchService["Service"]["checkWorktreeBase"]; | |
| 304 | 305 | }) => { |
| 305 | 306 | const projectId = ProjectId.make("project:client-target"); |
| 306 | 307 | const modelSelection = { instanceId: ProviderInstanceId.make("claude"), model: "claude-opus" }; |
| ... | ... | @@ -320,6 +321,7 @@ const clientLaunchHarness = (input: { |
| 320 | 321 | }), |
| 321 | 322 | Layer.mock(ThreadManagement.ThreadManagementService)({}), |
| 322 | 323 | Layer.mock(ThreadLaunch.ThreadLaunchService)({ |
| 324 | checkWorktreeBase: input.checkWorktreeBase ?? (() => Effect.void), | |
| 323 | 325 | launch: (launch) => { |
| 324 | 326 | input.launched.push(launch); |
| 325 | 327 | return Effect.succeed({ |
| ... | ... | @@ -454,3 +456,47 @@ it.effect("a launch binds only an existing checkout that is one of the project's |
| 454 | 456 | }); |
| 455 | 457 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 456 | 458 | ); |
| 459 | ||
| 460 | it.effect("a launch into a new worktree creates nothing when its base ref has no commit", () => | |
| 461 | Effect.gen(function* () { | |
| 462 | const launched: Array<ThreadLaunch.ThreadLaunchInput> = []; | |
| 463 | const { projectId, dependencies } = clientLaunchHarness({ | |
| 464 | runtimeModeCeiling: "auto", | |
| 465 | launched, | |
| 466 | checkWorktreeBase: ({ projectId, workspaceStrategy }) => | |
| 467 | workspaceStrategy.type === "worktree" && workspaceStrategy.baseRef === "t3/renamed" | |
| 468 | ? Effect.fail( | |
| 469 | new ThreadLaunch.ThreadLaunchBaseRefError({ | |
| 470 | projectId, | |
| 471 | projectTitle: "Client target", | |
| 472 | workspaceRoot: "/projects/client-target", | |
| 473 | baseRef: workspaceStrategy.baseRef, | |
| 474 | }), | |
| 475 | ) | |
| 476 | : Effect.void, | |
| 477 | }); | |
| 478 | const toolkit = yield* ProjectToolkit.pipe( | |
| 479 | Effect.provide( | |
| 480 | McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe(Layer.provide(dependencies)), | |
| 481 | ), | |
| 482 | ); | |
| 483 | const launchFrom = (baseRef: string) => | |
| 484 | toolkit | |
| 485 | .handle("t3_thread_launch", { | |
| 486 | title: "Fix", | |
| 487 | projectId, | |
| 488 | workspaceStrategy: { type: "worktree", baseRef, startFromOrigin: false }, | |
| 489 | }) | |
| 490 | .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies)); | |
| 491 | ||
| 492 | const refused = (yield* launchFrom("t3/renamed")).at(-1)?.result; | |
| 493 | expect(refused).toMatchObject({ code: "invalid_request" }); | |
| 494 | expect((refused as { message: string }).message).toContain( | |
| 495 | 'Base ref "t3/renamed" does not resolve to a commit in project "Client target" (/projects/client-target). No thread was created.', | |
| 496 | ); | |
| 497 | expect(launched).toHaveLength(0); | |
| 498 | ||
| 499 | expect((yield* launchFrom("main")).at(-1)?.result).toMatchObject({ projectId }); | |
| 500 | expect(launched).toHaveLength(1); | |
| 501 | }), | |
| 502 | ); |
apps/server/src/mcp/toolkits/project/handlers.ts+21| ... | ... | @@ -2,6 +2,7 @@ import { MessageId, ThreadId, OrchestratorMcpFailure, ProjectId } from "@t3tools |
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | 4 | import * as Option from "effect/Option"; |
| 5 | import * as ThreadLaunch from "../../../orchestration-v2/ThreadLaunchService.ts"; | |
| 5 | 6 | import * as ThreadMessageIntake from "../../../orchestration-v2/ThreadMessageIntake.ts"; |
| 6 | 7 | import * as Claims from "../../../orchestration-v2/AttachmentClaims.ts"; |
| 7 | 8 | import * as Project from "../../../project/ProjectService.ts"; |
| ... | ... | @@ -103,6 +104,26 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { |
| 103 | 104 | }); |
| 104 | 105 | yield* assertProjectWorktree(project.workspaceRoot, input.workspaceStrategy.worktreePath); |
| 105 | 106 | } |
| 107 | // A launch is accepted before its worktree is provisioned, so a base | |
| 108 | // ref that cannot resolve is refused while the agent can still act on it. | |
| 109 | const strategy = input.workspaceStrategy; | |
| 110 | if (strategy?.type === "worktree") | |
| 111 | yield* ThreadLaunch.ThreadLaunchService.pipe( | |
| 112 | Effect.flatMap((launches) => | |
| 113 | launches.checkWorktreeBase({ projectId, workspaceStrategy: strategy }), | |
| 114 | ), | |
| 115 | Effect.mapError( | |
| 116 | (error) => | |
| 117 | new OrchestratorMcpFailure({ | |
| 118 | code: "invalid_request", | |
| 119 | message: `${error.message} No thread was created. Pass a branch, tag, or commit that exists there${ | |
| 120 | strategy.startFromOrigin === true | |
| 121 | ? "; the project has no origin remote to fetch it from." | |
| 122 | : ", or set startFromOrigin:true if it exists only on origin." | |
| 123 | }`, | |
| 124 | }), | |
| 125 | ), | |
| 126 | ); | |
| 106 | 127 | const modelSelection = |
| 107 | 128 | input.modelSelection ?? |
| 108 | 129 | caller?.modelSelection ?? |
apps/server/src/orchestration-v2/ThreadLaunchService.test.ts+105| ... | ... | @@ -107,6 +107,7 @@ interface HarnessOptions { |
| 107 | 107 | readonly generateBranchName?: TextGeneration.TextGeneration["Service"]["generateBranchName"]; |
| 108 | 108 | readonly serverSettings?: Parameters<typeof ServerSettings.layerTest>[0]; |
| 109 | 109 | readonly providers?: ReadonlyArray<ServerProvider>; |
| 110 | readonly git?: Partial<GitWorkflow.GitWorkflowService["Service"]>; | |
| 110 | 111 | } |
| 111 | 112 | |
| 112 | 113 | function makeHarness(options: HarnessOptions = {}) { |
| ... | ... | @@ -175,6 +176,7 @@ function makeHarness(options: HarnessOptions = {}) { |
| 175 | 176 | removeWorktree, |
| 176 | 177 | resolveRemoteTrackingCommit: () => |
| 177 | 178 | Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" }), |
| 179 | ...options.git, | |
| 178 | 180 | }), |
| 179 | 181 | Layer.succeed(ProjectSetupScriptRunner.ProjectSetupScriptRunner, { |
| 180 | 182 | runForThread: runSetup, |
| ... | ... | @@ -2024,6 +2026,7 @@ it.effect("shared intake preserves durable attachment bytes after a lost launch |
| 2024 | 2026 | }), |
| 2025 | 2027 | ), |
| 2026 | 2028 | ), |
| 2029 | checkWorktreeBase: launches.checkWorktreeBase, | |
| 2027 | 2030 | retryPreparation: launches.retryPreparation, |
| 2028 | 2031 | }), |
| 2029 | 2032 | Effect.flip, |
| ... | ... | @@ -2269,3 +2272,105 @@ it.effect.each([0, 1])("releases an async setup before its completion with exit |
| 2269 | 2272 | }).pipe(Effect.provide(harness.layer)); |
| 2270 | 2273 | }), |
| 2271 | 2274 | ); |
| 2275 | ||
| 2276 | it.effect.each([ | |
| 2277 | { name: "is missing", baseRef: "t3/renamed", startFromOrigin: false, origin: true }, | |
| 2278 | { | |
| 2279 | name: "has no origin to come from", | |
| 2280 | baseRef: "t3/renamed", | |
| 2281 | startFromOrigin: true, | |
| 2282 | origin: false, | |
| 2283 | }, | |
| 2284 | { | |
| 2285 | name: "is a previous checkout there never was", | |
| 2286 | baseRef: "-", | |
| 2287 | startFromOrigin: false, | |
| 2288 | origin: true, | |
| 2289 | }, | |
| 2290 | ])("refuses a new worktree whose base ref $name", (testCase) => | |
| 2291 | Effect.gen(function* () { | |
| 2292 | const harness = makeHarness({ | |
| 2293 | hasCommit: ({ refName }) => Effect.succeed(refName === "HEAD"), | |
| 2294 | git: { | |
| 2295 | remoteExists: () => Effect.succeed(testCase.origin), | |
| 2296 | hasRefNamed: () => Effect.succeed(false), | |
| 2297 | }, | |
| 2298 | }); | |
| 2299 | yield* Effect.gen(function* () { | |
| 2300 | const launches = yield* ThreadLaunch.ThreadLaunchService; | |
| 2301 | const error = yield* launches | |
| 2302 | .checkWorktreeBase({ | |
| 2303 | projectId, | |
| 2304 | workspaceStrategy: { | |
| 2305 | type: "worktree", | |
| 2306 | baseRef: testCase.baseRef, | |
| 2307 | startFromOrigin: testCase.startFromOrigin, | |
| 2308 | }, | |
| 2309 | }) | |
| 2310 | .pipe(Effect.flip); | |
| 2311 | assert.deepInclude(error, { | |
| 2312 | _tag: "ThreadLaunchBaseRefError", | |
| 2313 | projectId, | |
| 2314 | baseRef: testCase.baseRef, | |
| 2315 | }); | |
| 2316 | assert.equal( | |
| 2317 | error.message, | |
| 2318 | `Base ref "${testCase.baseRef}" does not resolve to a commit in project "Project" (/repo).`, | |
| 2319 | ); | |
| 2320 | }).pipe(Effect.provide(harness.layer)); | |
| 2321 | }), | |
| 2322 | ); | |
| 2323 | ||
| 2324 | it.effect.each([ | |
| 2325 | { name: "a base ref that resolves", baseRef: "main", commits: ["main", "HEAD"] }, | |
| 2326 | // Provisioning fetches it; the ref may exist nowhere else yet. | |
| 2327 | { | |
| 2328 | name: "a base ref only origin has", | |
| 2329 | baseRef: "remote-only", | |
| 2330 | startFromOrigin: true, | |
| 2331 | commits: ["HEAD"], | |
| 2332 | }, | |
| 2333 | // `git worktree add` starts from a remote-tracking branch of that name. | |
| 2334 | { | |
| 2335 | name: "a base ref a remote branch carries", | |
| 2336 | baseRef: "remote-only", | |
| 2337 | commits: ["HEAD"], | |
| 2338 | remoteBranch: true, | |
| 2339 | }, | |
| 2340 | // Provisioning runs such a repository without a worktree. | |
| 2341 | { name: "a repository with no commits yet", baseRef: "main", commits: [] }, | |
| 2342 | { name: "the previous checkout", baseRef: "-", commits: ["@{-1}", "HEAD"] }, | |
| 2343 | { name: "a commit search", baseRef: ":/fix", commits: ["HEAD"] }, | |
| 2344 | { name: "a folder that is not a repository", baseRef: "main", commits: null }, | |
| 2345 | ])("leaves $name to the launch", (testCase) => | |
| 2346 | Effect.gen(function* () { | |
| 2347 | const { commits } = testCase; | |
| 2348 | const harness = makeHarness({ | |
| 2349 | git: { hasRefNamed: () => Effect.succeed(testCase.remoteBranch === true) }, | |
| 2350 | hasCommit: ({ refName }) => | |
| 2351 | commits === null | |
| 2352 | ? Effect.fail( | |
| 2353 | new GitCommandError({ | |
| 2354 | operation: "GitWorkflowService.hasCommit", | |
| 2355 | command: "git rev-parse", | |
| 2356 | cwd: "/repo", | |
| 2357 | detail: "Not a Git repository.", | |
| 2358 | }), | |
| 2359 | ) | |
| 2360 | : Effect.succeed(commits.includes(refName)), | |
| 2361 | }); | |
| 2362 | yield* Effect.gen(function* () { | |
| 2363 | const launches = yield* ThreadLaunch.ThreadLaunchService; | |
| 2364 | yield* launches.checkWorktreeBase({ | |
| 2365 | projectId, | |
| 2366 | workspaceStrategy: { | |
| 2367 | type: "worktree", | |
| 2368 | baseRef: testCase.baseRef, | |
| 2369 | ...(testCase.startFromOrigin === undefined | |
| 2370 | ? {} | |
| 2371 | : { startFromOrigin: testCase.startFromOrigin }), | |
| 2372 | }, | |
| 2373 | }); | |
| 2374 | }).pipe(Effect.provide(harness.layer)); | |
| 2375 | }), | |
| 2376 | ); |
apps/server/src/orchestration-v2/ThreadLaunchService.ts+61-1| ... | ... | @@ -147,12 +147,35 @@ export class ThreadLaunchError extends Schema.TaggedError<ThreadLaunchError>()( |
| 147 | 147 | } |
| 148 | 148 | } |
| 149 | 149 | |
| 150 | /** A new worktree was asked to start from a ref that gives it no commit. */ | |
| 151 | export class ThreadLaunchBaseRefError extends Schema.TaggedError<ThreadLaunchBaseRefError>()( | |
| 152 | "ThreadLaunchBaseRefError", | |
| 153 | { | |
| 154 | projectId: ProjectId, | |
| 155 | projectTitle: Schema.String, | |
| 156 | workspaceRoot: Schema.String, | |
| 157 | baseRef: Schema.String, | |
| 158 | }, | |
| 159 | ) { | |
| 160 | override get message(): string { | |
| 161 | return `Base ref "${this.baseRef}" does not resolve to a commit in project "${this.projectTitle}" (${this.workspaceRoot}).`; | |
| 162 | } | |
| 163 | } | |
| 164 | ||
| 150 | 165 | export class ThreadLaunchService extends Context.Service< |
| 151 | 166 | ThreadLaunchService, |
| 152 | 167 | { |
| 153 | 168 | readonly launch: ( |
| 154 | 169 | input: ThreadLaunchInput, |
| 155 | 170 | ) => Effect.Effect<ThreadLaunchResult, ThreadLaunchError>; |
| 171 | /** | |
| 172 | * Fails when a new worktree's base ref cannot give provisioning a commit. | |
| 173 | * `launch` accepts such a launch and reports the failure inside the | |
| 174 | * thread; a caller that needs it before a thread exists checks first. | |
| 175 | */ | |
| 176 | readonly checkWorktreeBase: ( | |
| 177 | input: Pick<ThreadLaunchInput, "projectId" | "workspaceStrategy">, | |
| 178 | ) => Effect.Effect<void, ThreadLaunchBaseRefError>; | |
| 156 | 179 | /** Dispatches prepared-run.retry and prepares the run's workspace again. */ |
| 157 | 180 | readonly retryPreparation: ( |
| 158 | 181 | input: ThreadLaunchRetryInput, |
| ... | ... | @@ -229,6 +252,43 @@ const make = Effect.gen(function* () { |
| 229 | 252 | } |
| 230 | 253 | }); |
| 231 | 254 | |
| 255 | const checkWorktreeBase: ThreadLaunchService["Service"]["checkWorktreeBase"] = Effect.fn( | |
| 256 | "ThreadLaunchService.checkWorktreeBase", | |
| 257 | )(function* (input) { | |
| 258 | const strategy = input.workspaceStrategy; | |
| 259 | // `:/text` is a commit search, which cannot be asked for a commit type. | |
| 260 | if (strategy.type !== "worktree" || strategy.baseRef.startsWith(":")) return; | |
| 261 | const { baseRef } = strategy; | |
| 262 | // Anything this cannot establish is left for the launch to report: a | |
| 263 | // missing project, a folder that is not a repository, a failing Git. | |
| 264 | const unresolved = yield* Effect.gen(function* () { | |
| 265 | const project = Option.getOrUndefined(yield* projects.getById(input.projectId)); | |
| 266 | if (project === undefined) return undefined; | |
| 267 | const cwd = project.workspaceRoot; | |
| 268 | // `git worktree add` reads `-` as the previous checkout. | |
| 269 | if (yield* git.hasCommit({ cwd, refName: baseRef === "-" ? "@{-1}" : baseRef })) | |
| 270 | return undefined; | |
| 271 | // Provisioning fetches the base from origin when it can, and runs a | |
| 272 | // repository with no commits yet without a worktree. | |
| 273 | if ( | |
| 274 | strategy.startFromOrigin === true && | |
| 275 | (yield* git.remoteExists({ cwd, remoteName: "origin" })) | |
| 276 | ) | |
| 277 | return undefined; | |
| 278 | // `git worktree add` also starts from a remote-tracking branch of that | |
| 279 | // name; any ref carrying it is reason enough to let Git decide. | |
| 280 | if (yield* git.hasRefNamed({ cwd, refName: baseRef })) return undefined; | |
| 281 | return (yield* git.hasCommit({ cwd, refName: "HEAD" })) ? project : undefined; | |
| 282 | }).pipe(Effect.orElseSucceed(() => undefined)); | |
| 283 | if (unresolved !== undefined) | |
| 284 | return yield* new ThreadLaunchBaseRefError({ | |
| 285 | projectId: unresolved.id, | |
| 286 | projectTitle: unresolved.title, | |
| 287 | workspaceRoot: unresolved.workspaceRoot, | |
| 288 | baseRef, | |
| 289 | }); | |
| 290 | }); | |
| 291 | ||
| 232 | 292 | const prepareInBackground = Effect.fn("ThreadLaunchService.prepareInBackground")(function* ( |
| 233 | 293 | input: PreparationInput, |
| 234 | 294 | threadId: ThreadId, |
| ... | ... | @@ -977,7 +1037,7 @@ const make = Effect.gen(function* () { |
| 977 | 1037 | ); |
| 978 | 1038 | }; |
| 979 | 1039 | |
| 980 | return ThreadLaunchService.of({ launch, retryPreparation }); | |
| 1040 | return ThreadLaunchService.of({ launch, checkWorktreeBase, retryPreparation }); | |
| 981 | 1041 | }); |
| 982 | 1042 | |
| 983 | 1043 | export const layer = Layer.effect(ThreadLaunchService, make); |