From 85147a4fbd8e0f9224519ea483c8000f1dfc71ea Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:15:59 +1100 Subject: [PATCH] fix(server): worktree handoff follows the project's start-from-origin setting Co-Authored-By: Claude Opus 5.5 --- .../server/src/mcp/WorktreeMcpService.test.ts | 22 ++++++++++++++++++- apps/server/src/mcp/WorktreeMcpService.ts | 18 ++++++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/server/src/mcp/WorktreeMcpService.test.ts b/apps/server/src/mcp/WorktreeMcpService.test.ts index 51c86f3e3a165..eb8b4b5b1e31c 100644 --- a/apps/server/src/mcp/WorktreeMcpService.test.ts +++ b/apps/server/src/mcp/WorktreeMcpService.test.ts @@ -96,6 +96,7 @@ interface HarnessOptions { readonly currentBranch?: string | null; readonly notARepo?: boolean; readonly newWorktreesStartFromOrigin?: boolean; + readonly projectNewWorktreesStartFromOrigin?: boolean; readonly setupScript?: "started" | "no-script" | "fails" | "dies"; readonly dispatchFails?: boolean; readonly dispatchDies?: boolean; @@ -302,6 +303,15 @@ const makeHarness = (options: HarnessOptions = {}) => { } satisfies Partial), ServerSettings.layerTest({ newWorktreesStartFromOrigin: options.newWorktreesStartFromOrigin ?? false, + ...(options.projectNewWorktreesStartFromOrigin === undefined + ? {} + : { + projectSettingsOverrides: { + [projectId]: { + newWorktreesStartFromOrigin: options.projectNewWorktreesStartFromOrigin, + }, + }, + }), }), Layer.mock(GitWorkflowService.GitWorkflowService)({ listRefs, @@ -522,6 +532,15 @@ describe("t3_worktree_handoff", () => { }); }); + it.effect("defaults startFromOrigin to the project's override", () => { + const harness = makeHarness({ projectNewWorktreesStartFromOrigin: true }); + return Effect.gen(function* () { + const result = yield* runHandoff(harness, { branch: "feature/project-origin" }); + expect(harness.fetchWorktreeBaseCommit).toHaveBeenCalled(); + expect(result.startedFromOrigin).toBe(true); + }); + }); + it.effect("removes the created worktree when the thread update dies with a defect", () => { const harness = makeHarness({ dispatchDies: true }); return Effect.gen(function* () { @@ -679,6 +698,7 @@ describe("t3_worktree_status", () => { worktreePath: "/worktrees/project/existing", branch: "feature/existing", }, + projectNewWorktreesStartFromOrigin: true, }); return Effect.gen(function* () { const result = yield* runStatus(harness); @@ -686,7 +706,7 @@ describe("t3_worktree_status", () => { attached: true, worktreePath: "/worktrees/project/existing", branch: "feature/existing", - defaultStartFromOrigin: false, + defaultStartFromOrigin: true, }); }); }); diff --git a/apps/server/src/mcp/WorktreeMcpService.ts b/apps/server/src/mcp/WorktreeMcpService.ts index 8a2231f6e4df4..bffae891d5f23 100644 --- a/apps/server/src/mcp/WorktreeMcpService.ts +++ b/apps/server/src/mcp/WorktreeMcpService.ts @@ -9,6 +9,7 @@ import { type WorktreeMcpSetupScriptStatus, type WorktreeMcpStatusResult, } from "@t3tools/contracts"; +import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as SecureRandom from "effect/crypto/SecureRandom"; @@ -116,10 +117,14 @@ const make = Effect.gen(function* () { ), ); - const readDefaultStartFromOrigin = serverSettings.getSettings.pipe( - Effect.map((settings) => settings.newWorktreesStartFromOrigin), - asOperationFailed("Unable to read server settings"), - ); + const readDefaultStartFromOrigin = (projectId: ProjectId) => + serverSettings.getSettings.pipe( + Effect.map( + (settings) => + resolveProjectSettings(settings, projectId).settings.newWorktreesStartFromOrigin, + ), + asOperationFailed("Unable to read server settings"), + ); const handoffIds = (scope: McpThreadInvocationScope) => random.uuidV4().pipe( @@ -205,7 +210,8 @@ const make = Effect.gen(function* () { baseRef = localStatus.refName; } - const startFromOrigin = input.startFromOrigin ?? (yield* readDefaultStartFromOrigin); + const startFromOrigin = + input.startFromOrigin ?? (yield* readDefaultStartFromOrigin(project.id)); let worktreeBaseRef = baseRef; let baseRefName = baseRef; @@ -415,7 +421,7 @@ const make = Effect.gen(function* () { const projection = yield* loadThread(scope); const project = yield* loadProject(scope, projection.thread.projectId); - const defaultStartFromOrigin = yield* readDefaultStartFromOrigin; + const defaultStartFromOrigin = yield* readDefaultStartFromOrigin(project.id); const result: WorktreeMcpStatusResult = { attached: projection.thread.worktreePath !== null,