refactor: unify worktree creation workflow (#636)

Route app, MCP, and agent worktree creation through one workflow boundary. Move branch auto-naming into workspace creation and keep agent metadata title-only.
This commit is contained in:
Mohamed Boudra
2026-04-30 21:37:17 +08:00
committed by GitHub
parent 93c6c3e5f8
commit c2ad4a02ef
15 changed files with 1328 additions and 569 deletions
+53 -84
View File
@@ -27,7 +27,7 @@ import {
appendTimelineItemIfAgentKnown,
emitLiveTimelineItemIfAgentKnown,
} from "./timeline-append.js";
import { getPaseoWorktreesRoot, type WorktreeConfig } from "../../utils/worktree.js";
import { getPaseoWorktreesRoot } from "../../utils/worktree.js";
import {
archivePaseoWorktree,
killTerminalsUnderPath,
@@ -39,7 +39,12 @@ import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.js";
import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../path-utils.js";
import type { TerminalManager } from "../../terminal/terminal-manager.js";
import { captureTerminalLines } from "../../terminal/terminal.js";
import { runAsyncWorktreeBootstrap } from "../worktree-bootstrap.js";
import type {
AgentWorktreeSetupContinuation,
CreatePaseoWorktreeSetupContinuationInput,
CreatePaseoWorktreeWorkflowFn,
CreatePaseoWorktreeWorkflowResult,
} from "../worktree-session.js";
import type { ScheduleService } from "../schedule/service.js";
import { ScheduleSummarySchema, StoredScheduleSchema } from "../schedule/types.js";
import type { ProviderDefinition } from "./provider-registry.js";
@@ -61,11 +66,7 @@ import {
} from "./mcp-shared.js";
import type { GitHubService } from "../../services/github-service.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
import type {
CreatePaseoWorktreeFn,
CreatePaseoWorktreeInput,
CreatePaseoWorktreeResult,
} from "../paseo-worktree-service.js";
import type { CreatePaseoWorktreeInput } from "../paseo-worktree-service.js";
import { toWorktreeRequestError } from "../worktree-errors.js";
import { join } from "node:path";
@@ -83,7 +84,7 @@ export interface AgentMcpServerOptions {
markWorkspaceArchiving?: ArchivePaseoWorktreeDependencies["markWorkspaceArchiving"];
clearWorkspaceArchiving?: ArchivePaseoWorktreeDependencies["clearWorkspaceArchiving"];
emitSessionMessage?: ArchivePaseoWorktreeDependencies["emit"];
createPaseoWorktree?: CreatePaseoWorktreeFn;
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
paseoHome?: string;
/**
* ID of the agent that is connecting to this MCP server.
@@ -625,8 +626,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
notifyOnFinish: boolean;
resolvedCwd: string;
resolvedMode: string | undefined;
worktreeConfig: WorktreeConfig | undefined;
shouldBootstrapWorktree: boolean | undefined;
setupContinuation: AgentWorktreeSetupContinuation | undefined;
}
const resolveCallerCreateAgentArgs = (
@@ -661,8 +661,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
notifyOnFinish: callerArgs.notifyOnFinish ?? false,
resolvedCwd,
resolvedMode,
worktreeConfig: undefined,
shouldBootstrapWorktree: undefined,
setupContinuation: undefined,
};
};
@@ -673,11 +672,11 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const resolvedProviderModel = resolveRequiredProviderModel(topLevelArgs.provider);
const { cwd, mode, worktreeName, baseBranch, refName, action, githubPrNumber } = topLevelArgs;
let resolvedCwd = expandUserPath(cwd);
let worktreeConfig: WorktreeConfig | undefined;
let shouldBootstrapWorktree: boolean | undefined;
let setupContinuation: AgentWorktreeSetupContinuation | undefined;
if (worktreeName) {
if (!baseBranch && !refName && !action && githubPrNumber === undefined) {
const shouldCreateWorktree = Boolean(worktreeName || refName || action || githubPrNumber);
if (shouldCreateWorktree) {
if (worktreeName && !baseBranch && !refName && !action && githubPrNumber === undefined) {
throw new Error("baseBranch is required when creating a worktree");
}
const createdWorktree = await createMcpWorktree({
@@ -687,17 +686,32 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
refName,
action,
githubPrNumber,
nameContext: topLevelArgs.initialPrompt,
runSetup: false,
paseoHome: options.paseoHome,
},
createPaseoWorktree: options.createPaseoWorktree,
resolveDefaultBranch: baseBranch ? async () => baseBranch : undefined,
workspaceGitService: options.workspaceGitService,
logger: options.logger,
setupContinuation: {
kind: "agent",
terminalManager: terminalManager ?? null,
appendTimelineItem: ({ agentId, item }) =>
appendTimelineItemIfAgentKnown({
agentManager,
agentId,
item,
}),
emitLiveTimelineItem: ({ agentId, item }) =>
emitLiveTimelineItemIfAgentKnown({
agentManager,
agentId,
item,
}),
logger: childLogger,
},
});
resolvedCwd = createdWorktree.worktree.worktreePath;
worktreeConfig = createdWorktree.worktree;
shouldBootstrapWorktree = createdWorktree.created;
setupContinuation = createdWorktree.setupContinuation;
}
return {
@@ -711,8 +725,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
notifyOnFinish: topLevelArgs.notifyOnFinish ?? false,
resolvedCwd,
resolvedMode: mode,
worktreeConfig,
shouldBootstrapWorktree,
setupContinuation,
};
};
@@ -755,8 +768,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
notifyOnFinish,
resolvedCwd,
resolvedMode,
worktreeConfig,
shouldBootstrapWorktree,
setupContinuation,
} = resolved;
const childAgentDefaultLabels = callerContext?.childAgentDefaultLabels;
@@ -778,27 +790,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
Object.keys(mergedLabels).length > 0 ? { labels: mergedLabels } : undefined,
);
if (worktreeConfig) {
void runAsyncWorktreeBootstrap({
agentId: snapshot.id,
worktree: worktreeConfig,
shouldBootstrap: shouldBootstrapWorktree,
terminalManager: terminalManager ?? null,
appendTimelineItem: (item) =>
appendTimelineItemIfAgentKnown({
agentManager,
agentId: snapshot.id,
item,
}),
emitLiveTimelineItem: (item) =>
emitLiveTimelineItemIfAgentKnown({
agentManager,
agentId: snapshot.id,
item,
}),
logger: childLogger,
});
}
setupContinuation?.startAfterAgentCreate({
agentId: snapshot.id,
});
const trimmedPrompt = initialPrompt.trim();
scheduleAgentMetadataGeneration({
@@ -809,11 +803,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
explicitTitle: snapshot.config.title,
paseoHome: options.paseoHome,
logger: childLogger,
deps: options.workspaceGitService
? {
workspaceGitService: options.workspaceGitService,
}
: undefined,
});
try {
@@ -1779,6 +1768,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
.optional()
.describe("Optional repository cwd. Defaults to the caller agent cwd."),
branchName: z.string().optional(),
nameContext: z.string().optional(),
baseBranch: z.string().optional(),
refName: z.string().min(1).optional(),
action: z.enum(["branch-off", "checkout"]).optional(),
@@ -1789,15 +1779,18 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
worktreePath: z.string(),
},
},
async ({ cwd, branchName, baseBranch, refName, action, githubPrNumber }) => {
if (!branchName && !refName && githubPrNumber === undefined) {
throw new Error("create_worktree requires branchName, refName, or githubPrNumber");
async ({ cwd, branchName, nameContext, baseBranch, refName, action, githubPrNumber }) => {
if (!branchName && !nameContext && !refName && githubPrNumber === undefined) {
throw new Error(
"create_worktree requires branchName, nameContext, refName, or githubPrNumber",
);
}
const repoRoot = resolveScopedCwd(cwd, { required: true });
const createdWorktree = await createMcpWorktree({
input: {
cwd: repoRoot,
worktreeSlug: branchName,
nameContext,
refName,
action,
githubPrNumber,
@@ -1806,8 +1799,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
createPaseoWorktree: options.createPaseoWorktree,
resolveDefaultBranch: baseBranch ? async () => baseBranch : undefined,
workspaceGitService: options.workspaceGitService,
logger: options.logger,
});
const { worktree } = createdWorktree;
@@ -2050,46 +2041,24 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
interface CreateMcpWorktreeOptions {
input: CreatePaseoWorktreeInput;
createPaseoWorktree: CreatePaseoWorktreeFn | undefined;
createPaseoWorktree: CreatePaseoWorktreeWorkflowFn | undefined;
resolveDefaultBranch?: (repoRoot: string) => Promise<string>;
workspaceGitService?: Pick<WorkspaceGitService, "getSnapshot">;
logger: Logger;
setupContinuation?: CreatePaseoWorktreeSetupContinuationInput;
}
async function createMcpWorktree(
options: CreateMcpWorktreeOptions,
): Promise<CreatePaseoWorktreeResult> {
): Promise<CreatePaseoWorktreeWorkflowResult> {
try {
if (!options.createPaseoWorktree) {
throw new Error("Paseo worktree service is not configured");
}
const result = await options.createPaseoWorktree(options.input, {
resolveDefaultBranch: options.resolveDefaultBranch,
...(options.resolveDefaultBranch
? { resolveDefaultBranch: options.resolveDefaultBranch }
: {}),
...(options.setupContinuation ? { setupContinuation: options.setupContinuation } : {}),
});
if (options.workspaceGitService) {
const refreshResults = await Promise.allSettled([
options.workspaceGitService.getSnapshot(options.input.cwd, {
force: true,
reason: "create-worktree",
}),
options.workspaceGitService.getSnapshot(result.worktree.worktreePath, {
force: true,
reason: "create-worktree",
}),
]);
for (const [index, refreshResult] of refreshResults.entries()) {
if (refreshResult.status === "fulfilled") {
continue;
}
options.logger.warn(
{
err: refreshResult.reason,
cwd: index === 0 ? options.input.cwd : result.worktree.worktreePath,
},
"Failed to force-refresh workspace git snapshot after creating worktree",
);
}
}
return result;
} catch (error) {
throw toWorktreeRequestError(error);