diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md
index 378a0070ea..b49e8309db 100644
--- a/docs/agent-lifecycle.md
+++ b/docs/agent-lifecycle.md
@@ -71,6 +71,8 @@ normal root agent immediately, and closing its still-open tab archives it.
Runtime ownership is resolved from explicit workspace ID and caller context, never from `cwd`. Workspace creation is a separate operation with `local | worktree` isolation; agent creation reuses that workspace unless a new workspace is explicitly requested.
+Setup belongs to the workspace, including combined workspace-plus-agent creation and the supported agent `worktree` request. Run setup through the workspace workflow before launching configured terminals or scripts. Setup output is read in the Setup panel from per-command snapshots; never produce setup chat timeline rows or concatenate command logs. Legacy agent `git`/`worktreeName` inputs and hidden MCP worktree placement are rejected with workspace-creation guidance. Existing wire schemas retain an empty combined-log string for parsing compatibility.
+
Users can also detach an existing subagent from the subagents track. Detach is deliberately a manual lifecycle gesture, not an agent-facing MCP tool. It removes the parent and open-tab lifecycle labels: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive.
`notifyOnFinish` defaults to `true` for agent-scoped creation and prompt follow-ups because most delegated work needs to report back to the creating agent. Set it to `false` only for truly fire-and-forget agents or prompts.
diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx
index d67eb15f26..a10bd7b0c6 100644
--- a/packages/app/src/components/tool-call-details.tsx
+++ b/packages/app/src/components/tool-call-details.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, type ReactNode } from "react";
+import { useMemo, type ReactNode } from "react";
import {
View,
Text,
@@ -186,48 +186,6 @@ function ShellDetailSection({ command, output, ds }: ShellDetailProps) {
);
}
-interface WorktreeSetupDetailProps {
- log: string;
- branchName: string;
- worktreePath: string;
- ds: DetailStyles;
-}
-
-function WorktreeSetupDetailSection({
- log,
- branchName,
- worktreePath,
- ds,
-}: WorktreeSetupDetailProps) {
- const setupLog = log.replace(/^\n+/, "");
- const hasLog = setupLog.length > 0;
- return (
-
-
-
-
-
-
- {hasLog ? setupLog : `Preparing worktree ${branchName} at ${worktreePath}`}
-
-
-
-
-
-
- );
-}
-
function resolveSubAgentFallbackHeader(
subAgentType: string | null | undefined,
description: string | null | undefined,
@@ -678,17 +636,7 @@ function buildDetailSections(
,
];
}
- if (detail.type === "worktree_setup") {
- return [
- ,
- ];
- }
+ if (detail.type === "worktree_setup") return [];
if (detail.type === "sub_agent") {
return [
;
manuallyCollapsed: Set;
snapshotError: string | null | undefined;
}
function buildCommandRowState(args: BuildCommandRowPropsArgs) {
- const { command, autoExpandIndex, log, expandedIndices, manuallyCollapsed, snapshotError } = args;
+ const { command, autoExpandIndex, expandedIndices, manuallyCollapsed, snapshotError } = args;
const isExpanded = expandedIndices.has(command.index);
const hasError = command.status === "failed" && Boolean(snapshotError);
- const commandLog = resolveCommandLog(command, autoExpandIndex, log);
+ const commandLog = command.log;
const hasLog = commandLog.trim().length > 0;
const isExpandable = command.status !== "running" || hasLog || hasError;
const isAutoExpanded = command.index === autoExpandIndex && !manuallyCollapsed.has(command.index);
@@ -285,9 +272,7 @@ function SetupPanel() {
);
const commands = snapshot?.detail.commands ?? EMPTY_COMMANDS;
- const log = snapshot?.detail.log ?? "";
- const hasNoSetupCommands =
- snapshot?.status === "completed" && commands.length === 0 && log.trim().length === 0;
+ const hasNoSetupCommands = snapshot?.status === "completed" && commands.length === 0;
const isWaiting = !snapshot || (snapshot.status === "running" && commands.length === 0);
const { expandedIndices, manuallyCollapsed, toggleExpanded } = useExpandedSetupCommands();
@@ -323,7 +308,6 @@ function SetupPanel() {
workspaceId={target.workspaceId}
snapshot={snapshot}
commands={commands}
- log={log}
isWaiting={isWaiting}
hasNoSetupCommands={hasNoSetupCommands}
autoExpandIndex={autoExpandIndex}
@@ -341,7 +325,6 @@ interface SetupPanelBodyProps {
workspaceId: string;
snapshot: WorkspaceSetupSnapshot | null;
commands: SetupCommand[];
- log: string;
isWaiting: boolean;
hasNoSetupCommands: boolean;
autoExpandIndex: number | null;
@@ -352,7 +335,7 @@ interface SetupPanelBodyProps {
function SetupPanelBody(props: SetupPanelBodyProps) {
const { t } = useTranslation();
- const { snapshot, commands, log } = props;
+ const { snapshot, commands } = props;
if (snapshot?.status === "blocked" && snapshot.blockedSource) {
return (
);
})}
-
);
@@ -523,25 +504,6 @@ function SetupCommandChevron({ showDetail }: { showDetail: boolean }) {
);
}
-function StandaloneLogView({ commands, log }: { commands: SetupCommand[]; log: string }) {
- const { t } = useTranslation();
- if (commands.length !== 0 || log.trim().length === 0) return null;
- return (
-
-
- {log}
-
-
- );
-}
-
function TopLevelSetupError({
snapshotError,
commands,
diff --git a/packages/app/src/utils/tool-call-detail-state.test.ts b/packages/app/src/utils/tool-call-detail-state.test.ts
index 1cf7d624dc..ddfed7c803 100644
--- a/packages/app/src/utils/tool-call-detail-state.test.ts
+++ b/packages/app/src/utils/tool-call-detail-state.test.ts
@@ -77,3 +77,16 @@ describe("tool-call detail state", () => {
assert.strictEqual(hasMeaningfulToolCallDetail(detail), true);
});
});
+
+it("keeps historical setup rows readable as history without a chat output view", () => {
+ assert.strictEqual(
+ hasMeaningfulToolCallDetail({
+ type: "worktree_setup",
+ branchName: "old",
+ worktreePath: "/workspace",
+ log: "historical output",
+ commands: [],
+ }),
+ false,
+ );
+});
diff --git a/packages/app/src/utils/tool-call-detail-state.ts b/packages/app/src/utils/tool-call-detail-state.ts
index de9e151427..e2d983d8cb 100644
--- a/packages/app/src/utils/tool-call-detail-state.ts
+++ b/packages/app/src/utils/tool-call-detail-state.ts
@@ -40,12 +40,6 @@ function hasMeaningfulFetchDetail(detail: Extract,
-): boolean {
- return Boolean(detail.branchName || detail.worktreePath || detail.log);
-}
-
function hasMeaningfulSubAgentDetail(
detail: Extract,
): boolean {
@@ -81,7 +75,7 @@ export function hasMeaningfulToolCallDetail(detail: ToolCallDetail | undefined):
case "fetch":
return hasMeaningfulFetchDetail(detail);
case "worktree_setup":
- return hasMeaningfulWorktreeSetupDetail(detail);
+ return false;
case "sub_agent":
return hasMeaningfulSubAgentDetail(detail);
case "plain_text":
@@ -98,6 +92,7 @@ export function isPendingToolCallDetail(params: {
status: "executing" | "running" | "completed" | "failed" | "canceled";
error: unknown;
}): boolean {
+ if (params.detail?.type === "worktree_setup") return false;
const isRunning = params.status === "running" || params.status === "executing";
return isRunning && params.error == null && !hasMeaningfulToolCallDetail(params.detail);
}
diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts
index 7469470ad1..46d446790c 100644
--- a/packages/client/src/daemon-client.test.ts
+++ b/packages/client/src/daemon-client.test.ts
@@ -3335,62 +3335,6 @@ test("sends create_agent_request with workspace and caller identity", async () =
await expect(createPromise).rejects.toThrow("compat test sentinel");
});
-test("sends worktree target and autoArchive in create_agent_request", async () => {
- const logger = createMockLogger();
- const mock = createMockTransport();
-
- const client = new DaemonClient({
- url: "ws://test",
- clientId: "clsk_unit_test",
- logger,
- reconnect: { enabled: false },
- transportFactory: () => mock.transport,
- });
- clients.push(client);
-
- const connectPromise = client.connect();
- mock.triggerOpen();
- await connectPromise;
-
- const createPromise = client.createAgent({
- provider: "codex",
- cwd: "/tmp/project",
- worktree: {
- mode: "branch-off",
- newBranch: "agent-lifecycle-dispatch",
- base: "main",
- },
- autoArchive: true,
- });
-
- expect(mock.sent).toHaveLength(1);
- const request = parseSentFrame(mock.sent[0]);
- expect(request).toEqual(
- expect.objectContaining({
- type: "create_agent_request",
- worktree: {
- mode: "branch-off",
- newBranch: "agent-lifecycle-dispatch",
- base: "main",
- },
- autoArchive: true,
- }),
- );
-
- mock.triggerMessage(
- wrapSessionMessage({
- type: "status",
- payload: {
- status: "agent_create_failed",
- requestId: request.requestId,
- error: "worktree auto archive sentinel",
- },
- }),
- );
-
- await expect(createPromise).rejects.toThrow("worktree auto archive sentinel");
-});
-
test("sends internal in create_agent_request when the host advertises internal agents", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
@@ -3609,120 +3553,6 @@ test("sends structured attachments with create_agent_request", async () => {
await expect(createPromise).rejects.toThrow("attachment test sentinel");
});
-test("sends worktree base-ref fields in create_agent_request git options", async () => {
- const logger = createMockLogger();
- const mock = createMockTransport();
-
- const client = new DaemonClient({
- url: "ws://test",
- clientId: "clsk_unit_test",
- logger,
- reconnect: { enabled: false },
- transportFactory: () => mock.transport,
- });
- clients.push(client);
-
- const connectPromise = client.connect();
- mock.triggerOpen();
- await connectPromise;
-
- const createPromise = client.createAgent({
- provider: "codex",
- cwd: "/tmp/project",
- requestId: "req-agent-ref",
- git: {
- createWorktree: true,
- worktreeSlug: "review-pr-123",
- refName: "feature/worktree-base-ref",
- action: "checkout",
- githubPrNumber: 123,
- },
- });
-
- expect(mock.sent).toHaveLength(1);
- const request = parseSentFrame(mock.sent[0]);
- expect(request.git).toEqual({
- createWorktree: true,
- worktreeSlug: "review-pr-123",
- refName: "feature/worktree-base-ref",
- action: "checkout",
- githubPrNumber: 123,
- });
-
- mock.triggerMessage(
- wrapSessionMessage({
- type: "status",
- payload: {
- status: "agent_create_failed",
- requestId: request.requestId,
- error: "git ref fields sentinel",
- },
- }),
- );
-
- await expect(createPromise).rejects.toThrow("git ref fields sentinel");
-});
-
-test("omitting create_agent_request worktree base-ref fields preserves legacy wire shape", async () => {
- const logger = createMockLogger();
- const mock = createMockTransport();
-
- const client = new DaemonClient({
- url: "ws://test",
- clientId: "clsk_unit_test",
- logger,
- reconnect: { enabled: false },
- transportFactory: () => mock.transport,
- });
- clients.push(client);
-
- const connectPromise = client.connect();
- mock.triggerOpen();
- await connectPromise;
-
- const createPromise = client.createAgent({
- provider: "codex",
- cwd: "/tmp/project",
- requestId: "req-agent-legacy",
- git: {
- createWorktree: true,
- worktreeSlug: "feature-a",
- },
- });
-
- expect(assertStr(mock.sent[0])).toBe(
- JSON.stringify({
- type: "session",
- message: {
- type: "create_agent_request",
- config: {
- provider: "codex",
- cwd: "/tmp/project",
- },
- git: {
- createWorktree: true,
- worktreeSlug: "feature-a",
- },
- labels: {},
- requestId: "req-agent-legacy",
- },
- }),
- );
-
- mock.triggerMessage(
- wrapSessionMessage({
- type: "status",
- payload: {
- status: "agent_create_failed",
- requestId: "req-agent-legacy",
- error: "legacy git shape sentinel",
- },
- }),
- );
-
- await expect(createPromise).rejects.toThrow("legacy git shape sentinel");
-});
-
test("sends structured first-agent context attachments with create_paseo_worktree_request", async () => {
const logger = createMockLogger();
const mock = createMockTransport();
@@ -7844,3 +7674,59 @@ test("measures an outbound message in UTF-8 bytes against the daemon's message l
expect(mock.sent).toHaveLength(1);
expect(client.getConnectionState().status).toBe("connected");
});
+
+test("sends worktree target and autoArchive in create_agent_request", async () => {
+ const logger = createMockLogger();
+ const mock = createMockTransport();
+
+ const client = new DaemonClient({
+ url: "ws://test",
+ clientId: "clsk_unit_test",
+ logger,
+ reconnect: { enabled: false },
+ transportFactory: () => mock.transport,
+ });
+ clients.push(client);
+
+ const connectPromise = client.connect();
+ mock.triggerOpen();
+ await connectPromise;
+
+ const createPromise = client.createAgent({
+ provider: "codex",
+ cwd: "/tmp/project",
+ worktree: {
+ mode: "branch-off",
+ newBranch: "agent-lifecycle-dispatch",
+ base: "main",
+ },
+ autoArchive: true,
+ });
+
+ expect(mock.sent).toHaveLength(1);
+ const request = parseSentFrame(mock.sent[0]);
+ expect(request).toEqual(
+ expect.objectContaining({
+ type: "create_agent_request",
+ worktree: {
+ mode: "branch-off",
+ newBranch: "agent-lifecycle-dispatch",
+ base: "main",
+ },
+ autoArchive: true,
+ }),
+ );
+
+ mock.triggerMessage(
+ wrapSessionMessage({
+ type: "status",
+ payload: {
+ status: "agent_create_failed",
+ requestId: request.requestId,
+ error: "worktree auto archive sentinel",
+ },
+ }),
+ );
+
+ await expect(createPromise).rejects.toThrow("worktree auto archive sentinel");
+});
diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts
index 647e1fddf6..c74a705d52 100644
--- a/packages/client/src/daemon-client.ts
+++ b/packages/client/src/daemon-client.ts
@@ -459,12 +459,12 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
outputSchema?: Record;
images?: CreateAgentRequestMessage["images"];
attachments?: CreateAgentRequestMessage["attachments"];
+ /** @deprecated Use createWorkspace({ source, agent, idempotencyKey }); rejected by createAgent. */
git?: GitSetupOptions;
worktree?: CreateAgentRequestMessage["worktree"];
autoArchive?: CreateAgentRequestMessage["autoArchive"];
background?: CreateAgentRequestMessage["background"];
- // COMPAT(createAgentWorktree): low-level old callers may still send the
- // create-agent worktree field. Added in v0.2.0; remove after 2027-01-17.
+ /** @deprecated Use createWorkspace({ source, agent, idempotencyKey }); rejected by createAgent. */
worktreeName?: string;
requestId?: string;
labels?: Record;
@@ -2924,6 +2924,11 @@ export class DaemonClient {
}
async createAgent(options: CreateAgentRequestOptions): Promise {
+ if (options.git !== undefined || options.worktreeName !== undefined) {
+ throw new Error(
+ "Legacy git/worktreeName agent placement was removed. Use createWorkspace({ source, agent, idempotencyKey }) or create a workspace first and pass workspaceId.",
+ );
+ }
if (options.background !== undefined) this.requireBackgroundWorkspaces();
const result = await this.creations.createAgent({
...options,
diff --git a/packages/server/src/server/agent/agent-mcp.e2e.test.ts b/packages/server/src/server/agent/agent-mcp.e2e.test.ts
index cf9711a9df..b123fa30c3 100644
--- a/packages/server/src/server/agent/agent-mcp.e2e.test.ts
+++ b/packages/server/src/server/agent/agent-mcp.e2e.test.ts
@@ -787,7 +787,7 @@ describe("agent MCP end-to-end (offline)", () => {
}
}, 30_000);
- test("create_agent with worktree is async and boots terminals only after setup success", async () => {
+ test("workspace creation owns async setup and terminal bootstrap before agent attachment", async () => {
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-worktree-repo-"));
@@ -843,21 +843,30 @@ describe("agent MCP end-to-end (offline)", () => {
stdio: "pipe",
});
- const result = await withTimeout({
+ const workspaceResult = await withTimeout({
promise: client.callTool({
- name: "create_agent",
+ name: "create_workspace",
args: {
- cwd: repoRoot,
- title: "MCP worktree setup terminals",
- provider: "claude/claude-test-model",
- mode: "bypassPermissions",
- initialPrompt: "say done and stop",
- worktreeName: "mcp-worktree-setup-test",
+ isolation: "worktree",
+ path: repoRoot,
+ worktreeSlug: "mcp-worktree-setup-test",
baseBranch: "main",
},
}),
timeoutMs: 2500,
- label: "create_agent should not block on setup",
+ label: "create_workspace should not block on setup",
+ });
+ const workspace = getStructuredContent(workspaceResult);
+ if (typeof workspace?.workspaceId !== "string") throw new Error("Expected created workspace");
+ const result = await client.callTool({
+ name: "create_agent",
+ args: {
+ workspaceId: workspace.workspaceId,
+ title: "MCP worktree setup terminals",
+ provider: "claude/claude-test-model",
+ settings: { modeId: "bypassPermissions" },
+ initialPrompt: "say done and stop",
+ },
});
const payload = getStructuredContent(result);
diff --git a/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts
index 554bb9869e..3547d226b0 100644
--- a/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts
+++ b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts
@@ -1,3 +1,4 @@
+import type { CreatePaseoWorktreeInput } from "../paseo-worktree-service.js";
import { randomUUID } from "node:crypto";
import type pino from "pino";
@@ -7,11 +8,13 @@ import { archiveByScope, type ActiveWorkspaceRef } from "../workspace-archive-se
import type {
CreatePaseoWorktreeWorkflowFn,
CreatePaseoWorktreeWorkflowResult,
- CreatePaseoWorktreeSetupContinuationInput,
- NormalizedGitOptions,
} from "../worktree-session.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
-import type { FirstAgentContext, SessionOutboundMessage } from "../messages.js";
+import type {
+ FirstAgentContext,
+ CreateAgentWorktreeTarget,
+ SessionOutboundMessage,
+} from "../messages.js";
import type { AgentManager, AgentSubscriber, SubscribeOptions } from "./agent-manager.js";
import type { AgentStorage } from "./agent-storage.js";
@@ -57,33 +60,40 @@ export class CreateAgentLifecycleDispatch {
async createWorktreeForRequest(input: {
cwd: string;
- target: NormalizedGitOptions | null;
+ target: CreateAgentWorktreeTarget | undefined;
firstAgentContext: FirstAgentContext;
- setupContinuation?: CreatePaseoWorktreeSetupContinuationInput;
background?: boolean;
callerWorkspaceId?: string;
}): Promise {
const target = input.target;
- if (!target?.createWorktree) return null;
+ if (!target) return null;
+ const placement: CreatePaseoWorktreeInput = { cwd: input.cwd };
+ switch (target.mode) {
+ case "branch-off":
+ placement.action = "branch-off";
+ placement.worktreeSlug = target.newBranch;
+ placement.refName = target.base;
+ break;
+ case "checkout-branch":
+ placement.action = "checkout";
+ placement.refName = target.branch;
+ break;
+ case "checkout-pr":
+ placement.githubPrNumber = target.prNumber;
+ break;
+ }
+ const base = target.mode === "branch-off" ? target.base : undefined;
return this.dependencies.createPaseoWorktreeWorkflow(
{
cwd: input.cwd,
+ ...placement,
background: input.background,
callerWorkspaceId: input.callerWorkspaceId,
firstAgentContext: input.firstAgentContext,
- worktreeSlug: target.worktreeSlug,
- refName: target.refName,
- action: target.action,
- checkoutSource: target.checkoutSource,
- githubPrNumber: target.githubPrNumber,
- runSetup: false,
paseoHome: this.dependencies.paseoHome,
worktreesRoot: this.dependencies.worktreesRoot,
},
- {
- ...(target.baseBranch ? { resolveDefaultBranch: async () => target.baseBranch! } : {}),
- setupContinuation: input.setupContinuation,
- },
+ base ? { resolveDefaultBranch: async () => base } : undefined,
);
}
diff --git a/packages/server/src/server/agent/create-agent/create.test.ts b/packages/server/src/server/agent/create-agent/create.test.ts
index f12309da2a..5d2c9fddc1 100644
--- a/packages/server/src/server/agent/create-agent/create.test.ts
+++ b/packages/server/src/server/agent/create-agent/create.test.ts
@@ -11,7 +11,6 @@ import { AgentStorage } from "../agent-storage.js";
import type { CreatePaseoWorktreeWorkflowResult } from "../../worktree-session.js";
import { createAgentCommand } from "./create.js";
import type { ManagedAgent } from "../agent-manager.js";
-import type { AgentSessionConfig } from "../agent-sdk-types.js";
const logger = createTestLogger();
@@ -53,7 +52,6 @@ function fakeWorktreeCreator(args: { repoRoot: string; createdWorkspaceId: strin
workspace: { workspaceId: args.createdWorkspaceId, cwd: workspaceCwd },
repoRoot: args.repoRoot,
created: true,
- setupContinuation: { kind: "agent" as const, startAfterAgentCreate: () => {} },
}) as unknown as CreatePaseoWorktreeWorkflowResult;
}
@@ -88,7 +86,6 @@ test("session create forwards clientMessageId to the initial prompt run options"
labels: {},
provisionalTitle: null,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({ sessionConfig: config }),
});
expect(streamAgent).toHaveBeenCalledWith("agent-1", "hello from create", {
@@ -125,7 +122,6 @@ test("session create validates the requested mode against the provider's modes",
labels: {},
provisionalTitle: null,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({ sessionConfig: config }),
}),
).rejects.toThrow("Invalid mode 'plan'");
@@ -169,7 +165,6 @@ test("session create applies the resolved mode from the provider create config",
labels: {},
provisionalTitle: null,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({ sessionConfig: config }),
});
expect(createAgent).toHaveBeenCalledWith(
@@ -247,7 +242,6 @@ test("session create stamps the requested workspaceId when no worktree setup run
labels: {},
provisionalTitle: null,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({ sessionConfig: config }),
},
);
@@ -258,41 +252,6 @@ test("session create stamps the requested workspaceId when no worktree setup run
}
});
-test("session create stamps the new worktree's workspaceId when a setup continuation runs", async () => {
- const workdir = mkdtempSync(join(tmpdir(), "create-agent-test-"));
- const storage = new AgentStorage(join(workdir, "agents"), logger);
- const agentManager = createRealAgentManager(storage);
-
- try {
- const { snapshot } = await createAgentCommand(
- {
- agentManager,
- agentStorage: storage,
- logger,
- providerSnapshotManager: createProviderSnapshotManagerStub().manager,
- },
- {
- kind: "session",
- config: { provider: "codex", cwd: workdir },
- workspaceId: "ws-source",
- labels: {},
- provisionalTitle: null,
- firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({
- sessionConfig: config,
- setupContinuation: { kind: "agent", startAfterAgentCreate: () => {} },
- createdWorkspaceId: "ws-new-worktree",
- }),
- },
- );
-
- const stored = await storage.get(snapshot.id);
- expect(stored?.workspaceId).toBe("ws-new-worktree");
- } finally {
- await removeRealAgentManagerWorkdir({ agentManager, storage, workdir });
- }
-});
-
test("mcp create stamps the new worktree's workspaceId, not the parent's", async () => {
const workdir = mkdtempSync(join(tmpdir(), "create-agent-test-"));
const storage = new AgentStorage(join(workdir, "agents"), logger);
@@ -309,7 +268,6 @@ test("mcp create stamps the new worktree's workspaceId, not the parent's", async
labels: {},
provisionalTitle: null,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({ sessionConfig: config }),
},
);
@@ -416,7 +374,6 @@ test("session create keeps the prompt title after the initial prompt settles", a
labels: {},
provisionalTitle: title,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({ sessionConfig: config }),
},
);
@@ -454,7 +411,6 @@ test("session create keeps an explicit title after the initial prompt settles",
labels: {},
provisionalTitle: title,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config) => ({ sessionConfig: config }),
},
);
@@ -492,7 +448,6 @@ test("session create with an internal config skips provider session persistence"
labels: {},
provisionalTitle: null,
firstAgentContext: { attachments: [] },
- buildSessionConfig: async (config: AgentSessionConfig) => ({ sessionConfig: config }),
};
await createAgentCommand(dependencies, {
diff --git a/packages/server/src/server/agent/create-agent/create.ts b/packages/server/src/server/agent/create-agent/create.ts
index 05f98285e1..fb9fdee07c 100644
--- a/packages/server/src/server/agent/create-agent/create.ts
+++ b/packages/server/src/server/agent/create-agent/create.ts
@@ -6,12 +6,10 @@ import type { CreatePaseoWorktreeInput } from "../../paseo-worktree-service.js";
import { expandUserPath, resolvePathFromBase } from "../../path-utils.js";
import { toWorktreeRequestError } from "../../worktree-errors.js";
import type {
- AgentWorktreeSetupContinuation,
- CreatePaseoWorktreeSetupContinuationInput,
CreatePaseoWorktreeWorkflowFn,
CreatePaseoWorktreeWorkflowResult,
} from "../../worktree-session.js";
-import type { AgentAttachment, FirstAgentContext, GitSetupOptions } from "../../messages.js";
+import type { AgentAttachment, FirstAgentContext } from "../../messages.js";
import type { AgentManager, CreateAgentOptions, ManagedAgent } from "../agent-manager.js";
import type { AgentPromptInput, AgentRunOptions, AgentSessionConfig } from "../agent-sdk-types.js";
import type { AgentStorage } from "../agent-storage.js";
@@ -22,20 +20,8 @@ import { resolveCreateAgentTitles } from "../create-agent-title.js";
import { buildAgentPrompt } from "../prompt-attachments.js";
import { normalizeClientMessageId, resolveClientMessageId } from "../../client-message-id.js";
import { resolveRequiredProviderModel, type ResolvedProviderModel } from "../mcp-shared.js";
-import {
- appendTimelineItemIfAgentKnown,
- emitLiveTimelineItemIfAgentKnown,
-} from "../timeline-append.js";
import { resolveCreateAgentIntent } from "./intent.js";
-export interface CreateAgentSessionWorktreeResult {
- sessionConfig: AgentSessionConfig;
- setupContinuation?: AgentWorktreeSetupContinuation;
- // Set when this build created a fresh worktree workspace. The agent must be
- // stamped with it so workspaceId-scoped archive can find the agent later.
- createdWorkspaceId?: string;
-}
-
export interface CreateAgentCommandDependencies {
agentManager: AgentManager;
agentStorage: AgentStorage;
@@ -61,24 +47,16 @@ export interface CreateAgentFromSessionInput {
agentId?: string;
config: AgentSessionConfig;
workspaceId: string;
- worktreeName?: string;
initialPrompt?: string;
source?: AgentPromptSource;
clientMessageId?: string;
outputSchema?: Record;
images?: Array<{ data: string; mimeType: string }>;
attachments?: AgentAttachment[];
- git?: GitSetupOptions;
labels: Record;
env?: Record;
provisionalTitle: string | null;
firstAgentContext: FirstAgentContext;
- buildSessionConfig: (
- config: AgentSessionConfig,
- gitOptions?: GitSetupOptions,
- legacyWorktreeName?: string,
- firstAgentContext?: FirstAgentContext,
- ) => Promise;
}
export interface CreateAgentFromMcpInput {
@@ -111,6 +89,7 @@ export interface CreateAgentFromMcpInput {
allowCustomCwd?: boolean;
childAgentDefaultLabels?: Record;
} | null;
+
worktree?: {
worktreeName?: string;
branchName?: string;
@@ -167,7 +146,6 @@ interface ResolvedCreateAgent {
prompt?: AgentPromptInput;
source?: AgentPromptSource;
runOptions?: AgentRunOptions;
- setupContinuation?: AgentWorktreeSetupContinuation;
promptFailure: CreateAgentPromptFailureMode;
promptLogger?: Logger;
createdWorktree?: CreatePaseoWorktreeWorkflowResult;
@@ -200,7 +178,7 @@ export async function createAgentCommand(
input.kind === "mcp" &&
input.notifyOnFinish &&
input.callerAgentId &&
- (resolved.prompt !== undefined || resolved.setupContinuation !== undefined)
+ resolved.prompt !== undefined
? setupFinishNotification({
agentManager: dependencies.agentManager,
agentStorage: dependencies.agentStorage,
@@ -211,9 +189,6 @@ export async function createAgentCommand(
})
: undefined;
- resolved.setupContinuation?.startAfterAgentCreate({
- agentId: snapshot.id,
- });
if (resolved.prompt !== undefined) {
const sendResult = await sendInitialPrompt(dependencies, resolved, snapshot).catch((error) => {
stopNotification?.();
@@ -238,29 +213,7 @@ async function resolveSessionCreateAgent(
input: CreateAgentFromSessionInput,
): Promise {
const trimmedPrompt = input.initialPrompt?.trim();
- const {
- sessionConfig: builtSessionConfig,
- setupContinuation,
- createdWorkspaceId,
- } = await input.buildSessionConfig(
- input.config,
- input.git,
- input.worktreeName,
- input.firstAgentContext,
- );
- // Validate the requested mode against the provider's modes for the resolved
- // cwd. The app remembers mode preferences globally, so a saved mode can be
- // stale for a workspace whose provider config no longer defines it — reject
- // it here instead of letting the provider fail mid-turn.
- //
- // This runs after buildSessionConfig, which may already have created a
- // worktree and/or workspace record — cwd (required to resolve modes) is
- // only known once that step completes. If validation throws, any
- // worktree/workspace buildSessionConfig created is the caller's
- // responsibility to clean up (session.ts's handleCreateAgentRequest does
- // this for the worktree path via cleanupCreatedWorktreeAfterFailedAgentCreate;
- // this is a pre-existing gap for directory-only workspace creates, not
- // introduced by this validation).
+ const builtSessionConfig = input.config;
const resolvedCreateConfig = await dependencies.providerSnapshotManager.resolveCreateConfig({
cwd: builtSessionConfig.cwd,
provider: builtSessionConfig.provider,
@@ -284,7 +237,7 @@ async function resolveSessionCreateAgent(
...(clientMessageId ? { clientMessageId } : {}),
}
: undefined;
- const workspaceId = setupContinuation ? createdWorkspaceId : input.workspaceId;
+ const workspaceId = input.workspaceId;
return {
config: sessionConfig,
@@ -296,15 +249,11 @@ async function resolveSessionCreateAgent(
// Paseo never persists an internal agent, so nothing could resume its
// provider session either. Same as the daemon's own ephemeral helpers.
...(sessionConfig.internal ? { persistSession: false } : {}),
- // A legacy git/worktreeName worktree creates a fresh workspace, so the
- // agent belongs to that workspace, not the source one. createdWorkspaceId
- // is the freshly created worktree's workspace.
workspaceId: requireResolvedWorkspaceId(workspaceId),
},
prompt: hasPromptContent ? prompt : undefined,
source: input.source,
runOptions,
- setupContinuation,
// Registration commits creation. First-turn failures belong to the created agent.
promptFailure: "return-error",
promptLogger: dependencies.logger.child({
@@ -323,18 +272,16 @@ async function resolveMcpCreateAgent(
? requireParentAgent(dependencies.agentManager, input.callerAgentId)
: null;
const cwd = resolveMcpInitialCwd(input, parentAgent);
- const { resolvedCwd, setupContinuation, createdWorkspaceId, createdWorktree } =
- await resolveMcpCwd({
- dependencies,
- cwd,
- worktree: input.worktree,
- initialPrompt: input.initialPrompt ?? "",
- callerWorkspaceId: parentAgent?.workspaceId,
- });
+ const { resolvedCwd, createdWorkspaceId, createdWorktree } = await resolveMcpCwd({
+ dependencies,
+ cwd,
+ worktree: input.worktree,
+ initialPrompt: input.initialPrompt ?? "",
+ callerWorkspaceId: parentAgent?.workspaceId,
+ });
if (createdWorktree) input.onWorktreeCreated?.(createdWorktree);
-
const intent = await resolveCreateAgentIntent({
- explicitWorkspaceId: setupContinuation ? createdWorkspaceId : input.workspaceId,
+ explicitWorkspaceId: createdWorkspaceId ?? input.workspaceId,
caller: parentAgent
? { id: parentAgent.id, cwd: parentAgent.cwd, workspaceId: parentAgent.workspaceId }
: null,
@@ -383,7 +330,6 @@ async function resolveMcpCreateAgent(
source: input.callerAgentId
? { kind: "agent-message", agentId: input.callerAgentId }
: undefined,
- setupContinuation,
createdWorktree,
promptFailure: input.promptFailure ?? "log",
};
@@ -535,6 +481,28 @@ function resolveChildAgentCwd(params: {
return resolvePathFromBase(params.parentCwd, requestedCwd);
}
+interface CreateMcpWorktreeOptions {
+ input: CreatePaseoWorktreeInput;
+ createPaseoWorktree: CreatePaseoWorktreeWorkflowFn | undefined;
+ resolveDefaultBranch?: (repoRoot: string) => Promise;
+}
+
+async function createMcpWorktree(
+ options: CreateMcpWorktreeOptions,
+): Promise {
+ try {
+ if (!options.createPaseoWorktree) {
+ throw new Error("Paseo worktree service is not configured");
+ }
+ return await options.createPaseoWorktree(
+ options.input,
+ options.resolveDefaultBranch ? { resolveDefaultBranch: options.resolveDefaultBranch } : {},
+ );
+ } catch (error) {
+ throw toWorktreeRequestError(error);
+ }
+}
+
async function resolveMcpCwd(params: {
dependencies: CreateAgentCommandDependencies;
cwd: string;
@@ -543,7 +511,6 @@ async function resolveMcpCwd(params: {
worktree: CreateAgentFromMcpInput["worktree"];
}): Promise<{
resolvedCwd: string;
- setupContinuation?: AgentWorktreeSetupContinuation;
createdWorkspaceId?: string;
createdWorktree?: CreatePaseoWorktreeWorkflowResult;
}> {
@@ -577,59 +544,16 @@ async function resolveMcpCwd(params: {
action: worktree.action,
githubPrNumber: worktree.githubPrNumber,
firstAgentContext: { prompt: params.initialPrompt },
- runSetup: false,
+
paseoHome: dependencies.paseoHome,
worktreesRoot: dependencies.worktreesRoot,
},
createPaseoWorktree: dependencies.createPaseoWorktree,
resolveDefaultBranch: baseBranch ? async () => baseBranch : undefined,
- setupContinuation: {
- kind: "agent",
- terminalManager: dependencies.terminalManager ?? null,
- appendTimelineItem: ({ agentId, item }) =>
- appendTimelineItemIfAgentKnown({
- agentManager: dependencies.agentManager,
- agentId,
- item,
- }),
- emitLiveTimelineItem: ({ agentId, item }) =>
- emitLiveTimelineItemIfAgentKnown({
- agentManager: dependencies.agentManager,
- agentId,
- item,
- }),
- logger: dependencies.logger,
- },
});
return {
resolvedCwd: createdWorktree.workspace.cwd,
- setupContinuation: createdWorktree.setupContinuation,
createdWorkspaceId: createdWorktree.workspace.workspaceId,
createdWorktree,
};
}
-
-interface CreateMcpWorktreeOptions {
- input: CreatePaseoWorktreeInput;
- createPaseoWorktree: CreatePaseoWorktreeWorkflowFn | undefined;
- resolveDefaultBranch?: (repoRoot: string) => Promise;
- setupContinuation?: CreatePaseoWorktreeSetupContinuationInput;
-}
-
-async function createMcpWorktree(
- options: CreateMcpWorktreeOptions,
-): Promise {
- try {
- if (!options.createPaseoWorktree) {
- throw new Error("Paseo worktree service is not configured");
- }
- return await options.createPaseoWorktree(options.input, {
- ...(options.resolveDefaultBranch
- ? { resolveDefaultBranch: options.resolveDefaultBranch }
- : {}),
- ...(options.setupContinuation ? { setupContinuation: options.setupContinuation } : {}),
- });
- } catch (error) {
- throw toWorktreeRequestError(error);
- }
-}
diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts
index 6e093a18a4..e774820dee 100644
--- a/packages/server/src/server/agent/mcp-server.test.ts
+++ b/packages/server/src/server/agent/mcp-server.test.ts
@@ -72,7 +72,6 @@ import { MutableDaemonConfigSchema, type AgentProfile } from "@getpaseo/protocol
import type { DaemonConfigStore } from "../daemon-config-store.js";
import type { BrowserToolsBroker, BrowserToolsExecuteInput } from "../browser-tools/broker.js";
import type { BrowserToolsResponsePayload } from "../browser-tools/errors.js";
-import { readPaseoWorktreeMetadata } from "../../utils/worktree-metadata.js";
import { createWorkspaceProvisioningService } from "../session/workspace-provisioning/workspace-provisioning-service.js";
const REPO_CWD = resolvePath("/tmp/repo");
@@ -180,20 +179,6 @@ async function waitForWorkspaceTitle(
});
}
-async function waitForWorkspaceBranch(
- workspaceRecords: Map,
- workspaceId: string,
- branch: string,
-): Promise {
- await vi.waitFor(() => expect(workspaceRecords.get(workspaceId)?.branch).toBe(branch), {
- timeout: 5_000,
- });
-}
-
-async function waitForUnexpectedWorkspaceNamingSideEffects(): Promise {
- await new Promise((resolve) => setTimeout(resolve, 25));
-}
-
async function removeTempDir(path: string): Promise {
await rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
@@ -693,7 +678,6 @@ function createPaseoWorktreeForMcpTest(options: {
createdWorkspaceIds?: string[];
workspaceRecords?: Map;
generateWorkspaceName?: () => Promise;
- setupContinuations?: Array<"workspace" | "agent" | undefined>;
startedAgentSetupIds?: string[];
}): CreatePaseoWorktreeWorkflowFn {
const projects = new Map();
@@ -790,7 +774,6 @@ function createPaseoWorktreeForMcpTest(options: {
});
return async (input, serviceOptions) => {
- options.setupContinuations?.push(serviceOptions?.setupContinuation?.kind);
const result = await createPaseoWorktreeWorkflow(
{
paseoHome: options.paseoHome,
@@ -824,17 +807,6 @@ function createPaseoWorktreeForMcpTest(options: {
);
options.broadcasts.push(result.workspace.workspaceId);
options.createdWorkspaceIds?.push(result.workspace.workspaceId);
- if (serviceOptions?.setupContinuation?.kind === "agent") {
- return {
- ...result,
- setupContinuation: {
- kind: "agent",
- startAfterAgentCreate: ({ agentId }) => {
- options.startedAgentSetupIds?.push(agentId);
- },
- },
- };
- }
return result;
};
}
@@ -1466,16 +1438,6 @@ describe("create_agent MCP tool", () => {
relationship: { kind: "detached" as const },
workspace: { kind: "create" as const, source: { kind: "directory" as const, path } },
});
- const detachedWorktreeWorkspace = (
- cwd: string,
- target:
- | { kind: "branch-off"; worktreeSlug?: string; branchName?: string; baseBranch?: string }
- | { kind: "checkout-branch"; branch: string }
- | { kind: "checkout-pr"; githubPrNumber: number },
- ) => ({
- relationship: { kind: "detached" as const },
- workspace: { kind: "create" as const, source: { kind: "worktree" as const, cwd, target } },
- });
const subagentCurrentWorkspace = (cwd?: string) => ({
relationship: { kind: "subagent" as const },
workspace: { kind: "current" as const, ...(cwd ? { cwd } : {}) },
@@ -1902,29 +1864,6 @@ describe("create_agent MCP tool", () => {
).rejects.toThrow("Unrecognized key");
});
- it("accepts worktree workspace intent in create_agent input validation", async () => {
- const { agentManager, agentStorage } = createTestDeps();
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- logger,
- });
- const tool = registeredTool(server, "create_agent");
-
- const parsed = await tool.inputSchema.safeParseAsync({
- ...detachedWorktreeWorkspace(existingCwd, {
- kind: "checkout-pr",
- githubPrNumber: 42,
- }),
- title: "Short title",
- provider: "codex/gpt-5.4",
- initialPrompt: "test",
- });
-
- expect(parsed.success).toBe(true);
- });
-
it("exposes workspace tools instead of worktree tools", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({
@@ -2082,444 +2021,6 @@ describe("create_agent MCP tool", () => {
);
});
- it("registers and broadcasts a workspace when create_agent creates a worktree", async () => {
- const { agentManager, agentStorage, spies } = createTestDeps();
- const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-worktree-"));
- const repoDir = join(tempDir, "repo");
- const paseoHome = join(tempDir, ".paseo");
- const broadcasts: string[] = [];
- const createdWorkspaceIds: string[] = [];
- const setupContinuations: Array<"workspace" | "agent" | undefined> = [];
- const startedAgentSetupIds: string[] = [];
-
- try {
- execFileSync("git", ["init", repoDir], { stdio: "pipe" });
- execFileSync("git", ["config", "user.email", "test@example.com"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["config", "commit.gpgsign", "false"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- await writeFile(join(repoDir, "README.md"), "hello\n");
- execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["commit", "-m", "init"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["branch", "-M", "main"], { cwd: repoDir, stdio: "pipe" });
-
- spies.agentManager.createAgent.mockImplementation(async (config: { cwd: string }) => ({
- id: "agent-with-worktree",
- cwd: config.cwd,
- lifecycle: "idle",
- currentModeId: null,
- availableModes: [],
- config: { title: "Worktree agent" },
- }));
-
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- paseoHome,
- createPaseoWorktree: createPaseoWorktreeForMcpTest({
- paseoHome,
- broadcasts,
- createdWorkspaceIds,
- setupContinuations,
- startedAgentSetupIds,
- }),
- logger,
- });
- const tool = registeredTool(server, "create_agent");
- await tool.handler({
- ...detachedWorktreeWorkspace(repoDir, {
- kind: "branch-off",
- worktreeSlug: "agent-worktree",
- branchName: "feature/agent-worktree",
- baseBranch: "main",
- }),
- title: "Worktree agent",
- provider: "codex/gpt-5.4",
- initialPrompt: "Do work",
- });
-
- expect(broadcasts).toHaveLength(1);
- expect(createdWorkspaceIds).toHaveLength(1);
- expect(broadcasts[0]).toBe(createdWorkspaceIds[0]);
- expect(setupContinuations).toEqual(["agent"]);
- expect(startedAgentSetupIds).toEqual(["agent-with-worktree"]);
- const agentCwd = z.string().parse(spies.agentManager.createAgent.mock.calls[0]?.[0].cwd);
- const branchName = execFileSync("git", ["branch", "--show-current"], {
- cwd: agentCwd,
- stdio: "pipe",
- })
- .toString()
- .trim();
- expect(branchName).toBe("feature/agent-worktree");
- // The agent is stamped with the freshly created worktree's workspaceId so
- // workspaceId-scoped archive can find and tear it down later.
- expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
- expect.objectContaining({
- cwd: expect.stringContaining("agent-worktree"),
- }),
- undefined,
- { workspaceId: createdWorkspaceIds[0] },
- );
- } finally {
- await removeTempDir(tempDir);
- }
- });
-
- it("creates a create_agent branch-off worktree without invoking the legacy metadata branch rename", async () => {
- const { agentManager, agentStorage, spies } = createTestDeps();
- const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-agent-worktree-name-context-"));
- const repoDir = join(tempDir, "repo");
- const paseoHome = join(tempDir, ".paseo");
- const broadcasts: string[] = [];
- const workspaceGitService = {
- getSnapshot: vi.fn(async () => {
- throw new Error("agent metadata branch rename should not run");
- }),
- };
-
- try {
- execFileSync("git", ["init", repoDir], { stdio: "pipe" });
- execFileSync("git", ["config", "user.email", "test@example.com"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["config", "commit.gpgsign", "false"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- await writeFile(join(repoDir, "README.md"), "hello\n");
- execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["commit", "-m", "init"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["branch", "-M", "main"], { cwd: repoDir, stdio: "pipe" });
-
- spies.agentManager.createAgent.mockImplementation(async (config: { cwd: string }) => ({
- id: "agent-auto-named-worktree",
- cwd: config.cwd,
- lifecycle: "idle",
- currentModeId: null,
- availableModes: [],
- config: { title: "Worktree agent" },
- }));
-
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- paseoHome,
- createPaseoWorktree: createPaseoWorktreeForMcpTest({ paseoHome, broadcasts }),
- workspaceGitService: workspaceGitService as unknown as Pick<
- WorkspaceGitService,
- "getSnapshot" | "listWorktrees"
- >,
- logger,
- });
- const tool = registeredTool(server, "create_agent");
- await tool.handler({
- ...detachedWorktreeWorkspace(repoDir, {
- kind: "branch-off",
- baseBranch: "main",
- }),
- title: "Worktree agent",
- provider: "codex/gpt-5.4",
- initialPrompt: "Fix workspace creation naming",
- });
-
- const agentCwd = z.string().parse(spies.agentManager.createAgent.mock.calls[0]?.[0].cwd);
- const initialBranch = execFileSync("git", ["branch", "--show-current"], {
- cwd: agentCwd,
- stdio: "pipe",
- })
- .toString()
- .trim();
- expect(initialBranch).not.toBe("");
- expect(initialBranch).not.toBe("main");
- await waitForUnexpectedWorkspaceNamingSideEffects();
- expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled();
- expect(broadcasts).toHaveLength(1);
- } finally {
- await removeTempDir(tempDir);
- }
- });
-
- it("auto-titles and renames an agent-created branch-off worktree from the initial prompt", async () => {
- const { agentManager, agentStorage, spies } = createTestDeps();
- const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-agent-worktree-auto-title-"));
- const repoDir = join(tempDir, "repo");
- const paseoHome = join(tempDir, ".paseo");
- const broadcasts: string[] = [];
- const createdWorkspaceIds: string[] = [];
- const workspaceRecords = new Map();
-
- try {
- execFileSync("git", ["init", repoDir], { stdio: "pipe" });
- execFileSync("git", ["config", "user.email", "test@example.com"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["config", "commit.gpgsign", "false"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- await writeFile(join(repoDir, "README.md"), "hello\n");
- execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["commit", "-m", "init"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["branch", "-M", "main"], { cwd: repoDir, stdio: "pipe" });
-
- spies.agentManager.createAgent.mockImplementation(async (config: { cwd: string }) => ({
- id: "agent-auto-titled-worktree",
- cwd: config.cwd,
- lifecycle: "idle",
- currentModeId: null,
- availableModes: [],
- config: { title: "Agent title" },
- }));
-
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- paseoHome,
- createPaseoWorktree: createPaseoWorktreeForMcpTest({
- paseoHome,
- broadcasts,
- createdWorkspaceIds,
- workspaceRecords,
- generateWorkspaceName: async () => ({
- title: "Workspace Auto Title Flow",
- branch: "workspace-auto-title-flow",
- }),
- }),
- logger,
- });
- const tool = registeredTool(server, "create_agent");
- await tool.handler({
- ...detachedWorktreeWorkspace(repoDir, {
- kind: "branch-off",
- branchName: "feat/placeholder-auto-title",
- baseBranch: "main",
- }),
- title: "Agent title",
- provider: "codex/gpt-5.4",
- initialPrompt: "Build a workspace auto title flow",
- });
- const workspaceId = z.string().parse(createdWorkspaceIds[0]);
- await waitForWorkspaceTitle(workspaceRecords, workspaceId, "Workspace Auto Title Flow");
-
- const agentCwd = z.string().parse(spies.agentManager.createAgent.mock.calls[0]?.[0].cwd);
- const workspace = workspaceRecords.get(workspaceId);
- const branchName = execFileSync("git", ["branch", "--show-current"], {
- cwd: agentCwd,
- stdio: "pipe",
- })
- .toString()
- .trim();
- const metadata = readPaseoWorktreeMetadata(agentCwd);
-
- expect(metadata).toMatchObject({
- version: 2,
- firstAgentBranchAutoName: {
- status: "attempted",
- placeholderBranchName: "feat/placeholder-auto-title",
- },
- });
- expect(branchName).toBe("workspace-auto-title-flow");
- expect(workspace).toMatchObject({
- title: "Workspace Auto Title Flow",
- branch: "workspace-auto-title-flow",
- });
- } finally {
- await removeTempDir(tempDir);
- }
- });
-
- it("keeps a manual workspace title when agent-created worktree naming finishes later", async () => {
- const { agentManager, agentStorage, spies } = createTestDeps();
- const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-agent-worktree-manual-title-"));
- const repoDir = join(tempDir, "repo");
- const paseoHome = join(tempDir, ".paseo");
- const broadcasts: string[] = [];
- const createdWorkspaceIds: string[] = [];
- const workspaceRecords = new Map();
-
- try {
- execFileSync("git", ["init", repoDir], { stdio: "pipe" });
- execFileSync("git", ["config", "user.email", "test@example.com"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["config", "commit.gpgsign", "false"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- await writeFile(join(repoDir, "README.md"), "hello\n");
- execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["commit", "-m", "init"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["branch", "-M", "main"], { cwd: repoDir, stdio: "pipe" });
-
- spies.agentManager.createAgent.mockImplementation(async (config: { cwd: string }) => ({
- id: "agent-manual-title-worktree",
- cwd: config.cwd,
- lifecycle: "idle",
- currentModeId: null,
- availableModes: [],
- config: { title: "Agent title" },
- }));
-
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- paseoHome,
- createPaseoWorktree: createPaseoWorktreeForMcpTest({
- paseoHome,
- broadcasts,
- createdWorkspaceIds,
- workspaceRecords,
- generateWorkspaceName: async () => ({
- title: "Generated Manual Race Title",
- branch: "generated-manual-race-title",
- }),
- }),
- workspaceRegistry: {
- get: async (workspaceId) => workspaceRecords.get(workspaceId) ?? null,
- upsert: async (record) => {
- workspaceRecords.set(record.workspaceId, record);
- },
- },
- emitWorkspaceUpdatesForWorkspaceIds: async (workspaceIds) => {
- broadcasts.push(...workspaceIds);
- },
- logger,
- });
- const createAgentTool = registeredTool(server, "create_agent");
- await createAgentTool.handler({
- ...detachedWorktreeWorkspace(repoDir, {
- kind: "branch-off",
- branchName: "feat/manual-title-placeholder",
- baseBranch: "main",
- }),
- title: "Agent title",
- provider: "codex/gpt-5.4",
- initialPrompt: "Keep the manually renamed workspace title",
- });
- const renameWorkspaceTool = registeredTool(server, "rename_workspace");
- await renameWorkspaceTool.handler({
- workspaceId: createdWorkspaceIds[0],
- title: "Manual Workspace Title",
- });
- const workspaceId = z.string().parse(createdWorkspaceIds[0]);
- await waitForWorkspaceBranch(workspaceRecords, workspaceId, "generated-manual-race-title");
-
- const agentCwd = z.string().parse(spies.agentManager.createAgent.mock.calls[0]?.[0].cwd);
- const workspace = workspaceRecords.get(workspaceId);
- const branchName = execFileSync("git", ["branch", "--show-current"], {
- cwd: agentCwd,
- stdio: "pipe",
- })
- .toString()
- .trim();
-
- expect(branchName).toBe("generated-manual-race-title");
- expect(workspace).toMatchObject({
- title: "Manual Workspace Title",
- branch: "generated-manual-race-title",
- });
- } finally {
- await removeTempDir(tempDir);
- }
- });
-
- it("uses create_agent title for the agent while still auto-titling the worktree workspace", async () => {
- const { agentManager, agentStorage, spies } = createTestDeps();
- const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-agent-title-workspace-title-"));
- const repoDir = join(tempDir, "repo");
- const paseoHome = join(tempDir, ".paseo");
- const broadcasts: string[] = [];
- const createdWorkspaceIds: string[] = [];
- const workspaceRecords = new Map();
-
- try {
- execFileSync("git", ["init", repoDir], { stdio: "pipe" });
- execFileSync("git", ["config", "user.email", "test@example.com"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["config", "commit.gpgsign", "false"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- await writeFile(join(repoDir, "README.md"), "hello\n");
- execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["commit", "-m", "init"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["branch", "-M", "main"], { cwd: repoDir, stdio: "pipe" });
-
- spies.agentManager.createAgent.mockImplementation(async (config: { cwd: string }) => ({
- id: "agent-explicit-title-worktree",
- cwd: config.cwd,
- lifecycle: "idle",
- currentModeId: null,
- availableModes: [],
- config: { title: "Explicit Agent Title" },
- }));
-
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- paseoHome,
- createPaseoWorktree: createPaseoWorktreeForMcpTest({
- paseoHome,
- broadcasts,
- createdWorkspaceIds,
- workspaceRecords,
- generateWorkspaceName: async () => ({
- title: "Generated Workspace Title",
- branch: "generated-workspace-title",
- }),
- }),
- logger,
- });
- const tool = registeredTool(server, "create_agent");
- await tool.handler({
- ...detachedWorktreeWorkspace(repoDir, {
- kind: "branch-off",
- branchName: "feat/agent-title-placeholder",
- baseBranch: "main",
- }),
- title: "Explicit Agent Title",
- provider: "codex/gpt-5.4",
- initialPrompt: "Generate the workspace title anyway",
- });
- const workspaceId = z.string().parse(createdWorkspaceIds[0]);
- await waitForWorkspaceTitle(workspaceRecords, workspaceId, "Generated Workspace Title");
-
- const workspace = workspaceRecords.get(workspaceId);
- expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
- expect.objectContaining({
- title: "Explicit Agent Title",
- }),
- undefined,
- { workspaceId },
- );
- expect(workspace).toMatchObject({
- title: "Generated Workspace Title",
- branch: "generated-workspace-title",
- });
- } finally {
- await removeTempDir(tempDir);
- }
- });
-
it("auto-titles an agent-created directory workspace from the initial prompt", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-agent-directory-auto-title-"));
@@ -2630,221 +2131,12 @@ describe("create_agent MCP tool", () => {
}
});
- it("auto-titles without renaming a create_agent checkout worktree from the initial prompt", async () => {
- const { agentManager, agentStorage, spies } = createTestDeps();
- const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-agent-checkout-name-context-"));
- const repoDir = join(tempDir, "repo");
- const paseoHome = join(tempDir, ".paseo");
- const broadcasts: string[] = [];
- const createdWorkspaceIds: string[] = [];
- const workspaceRecords = new Map();
- let generateCalls = 0;
- const workspaceGitService = {
- getSnapshot: vi.fn(async () => {
- throw new Error("agent metadata branch rename should not run");
- }),
- };
-
- try {
- execFileSync("git", ["init", repoDir], { stdio: "pipe" });
- execFileSync("git", ["config", "user.email", "test@example.com"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["config", "commit.gpgsign", "false"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- await writeFile(join(repoDir, "README.md"), "hello\n");
- execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["commit", "-m", "init"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["branch", "-M", "main"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["checkout", "-b", "existing-feature"], {
- cwd: repoDir,
- stdio: "pipe",
- });
- await writeFile(join(repoDir, "feature.txt"), "feature\n");
- execFileSync("git", ["add", "feature.txt"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["commit", "-m", "feature"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["checkout", "main"], { cwd: repoDir, stdio: "pipe" });
-
- spies.agentManager.createAgent.mockImplementation(async (config: { cwd: string }) => ({
- id: "agent-checkout-worktree",
- cwd: config.cwd,
- lifecycle: "idle",
- currentModeId: null,
- availableModes: [],
- config: { title: "Checkout agent" },
- }));
-
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- paseoHome,
- createPaseoWorktree: createPaseoWorktreeForMcpTest({
- paseoHome,
- broadcasts,
- createdWorkspaceIds,
- workspaceRecords,
- generateWorkspaceName: async () => {
- generateCalls += 1;
- return {
- title: "Generated Checkout Workspace Title",
- branch: "generated-checkout-workspace-title",
- };
- },
- }),
- workspaceGitService: workspaceGitService as unknown as Pick<
- WorkspaceGitService,
- "getSnapshot" | "listWorktrees"
- >,
- logger,
- });
- const tool = registeredTool(server, "create_agent");
- await tool.handler({
- ...detachedWorktreeWorkspace(repoDir, {
- kind: "checkout-branch",
- branch: "existing-feature",
- }),
- title: "Checkout agent",
- provider: "codex/gpt-5.4",
- initialPrompt: "Rename this checkout from the prompt",
- });
-
- const agentCwd = z.string().parse(spies.agentManager.createAgent.mock.calls[0]?.[0].cwd);
- const workspaceId = z.string().parse(createdWorkspaceIds[0]);
- await waitForWorkspaceTitle(
- workspaceRecords,
- workspaceId,
- "Generated Checkout Workspace Title",
- );
- expect(
- execFileSync("git", ["branch", "--show-current"], { cwd: agentCwd, stdio: "pipe" })
- .toString()
- .trim(),
- ).toBe("existing-feature");
- expect(workspaceRecords.get(workspaceId)).toMatchObject({
- title: "Generated Checkout Workspace Title",
- branch: "existing-feature",
- });
- expect(readPaseoWorktreeMetadata(agentCwd)).toMatchObject({
- version: 1,
- baseRefName: "main",
- });
- expect(generateCalls).toBe(1);
- expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled();
- expect(broadcasts).toEqual([workspaceId, workspaceId]);
- } finally {
- await removeTempDir(tempDir);
- }
- });
-
- it("passes create_agent GitHub PR worktrees through workspace creation without metadata branch rename", async () => {
- const { agentManager, agentStorage, spies } = createTestDeps();
- const startedAgentSetupIds: string[] = [];
- const createPaseoWorktree = vi.fn(
- async (
- input: CreatePaseoWorktreeInput,
- options?: Parameters[1],
- ) => ({
- worktree: {
- branchName: "pr-123",
- worktreePath: "/tmp/worktrees/pr-123",
- },
- intent: {
- kind: "checkout-github-pr" as const,
- githubPrNumber: input.githubPrNumber ?? 123,
- headRef: "pr-123",
- baseRefName: "main",
- },
- workspace: {
- workspaceId: "ws-pr-123",
- projectId: REPO_CWD,
- cwd: "/tmp/worktrees/pr-123",
- kind: "worktree" as const,
- displayName: "pr-123",
- createdAt: "2026-04-30T00:00:00.000Z",
- updatedAt: "2026-04-30T00:00:00.000Z",
- archivedAt: null,
- },
- repoRoot: REPO_CWD,
- created: true,
- ...(options?.setupContinuation?.kind === "agent"
- ? {
- setupContinuation: {
- kind: "agent" as const,
- startAfterAgentCreate: ({ agentId }: { agentId: string }) => {
- startedAgentSetupIds.push(agentId);
- },
- },
- }
- : {}),
- }),
- );
- const workspaceGitService = {
- getSnapshot: vi.fn(async () => {
- throw new Error("agent metadata branch rename should not run");
- }),
- };
- spies.agentManager.createAgent.mockImplementation(async (config: { cwd: string }) => ({
- id: "agent-pr-worktree",
- cwd: config.cwd,
- lifecycle: "idle",
- currentModeId: null,
- availableModes: [],
- config: { title: "PR agent" },
- }));
-
- const server = await createAgentMcpServer({
- agentManager,
- agentStorage,
- providerSnapshotManager: createOpenCodeManager().manager,
- createPaseoWorktree,
- workspaceGitService: workspaceGitService as unknown as Pick<
- WorkspaceGitService,
- "getSnapshot" | "listWorktrees"
- >,
- logger,
- });
- const tool = registeredTool(server, "create_agent");
- await tool.handler({
- ...detachedWorktreeWorkspace(REPO_CWD, {
- kind: "checkout-pr",
- githubPrNumber: 123,
- }),
- title: "PR agent",
- provider: "codex/gpt-5.4",
- initialPrompt: "Rename this PR branch from prompt",
- });
-
- expect(createPaseoWorktree).toHaveBeenCalledWith(
- expect.objectContaining({
- githubPrNumber: 123,
- firstAgentContext: { prompt: "Rename this PR branch from prompt" },
- }),
- expect.objectContaining({
- setupContinuation: expect.objectContaining({ kind: "agent" }),
- }),
- );
- expect(startedAgentSetupIds).toEqual(["agent-pr-worktree"]);
- expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
- expect.objectContaining({ cwd: "/tmp/worktrees/pr-123" }),
- undefined,
- { workspaceId: "ws-pr-123" },
- );
- await waitForUnexpectedWorkspaceNamingSideEffects();
- expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled();
- });
-
it("creates a worktree-isolated workspace", async () => {
const { agentManager, agentStorage } = createTestDeps();
const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-create-worktree-"));
const repoDir = join(tempDir, "repo");
const paseoHome = join(tempDir, ".paseo");
const broadcasts: string[] = [];
- const setupContinuations: Array<"workspace" | "agent" | undefined> = [];
try {
execFileSync("git", ["init", repoDir], { stdio: "pipe" });
@@ -2875,7 +2167,6 @@ describe("create_agent MCP tool", () => {
createPaseoWorktree: createPaseoWorktreeForMcpTest({
paseoHome,
broadcasts,
- setupContinuations,
}),
workspaceGitService: workspaceGitService as unknown as Pick<
WorkspaceGitService,
@@ -2896,7 +2187,6 @@ describe("create_agent MCP tool", () => {
expect(response.structuredContent.cwd).toContain("tool-worktree");
expect(response.structuredContent.workspaceId).toBe(broadcasts[0]);
expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled();
- expect(setupContinuations).toEqual([undefined]);
expect(broadcasts).toHaveLength(1);
expect(broadcasts[0]).toMatch(/^wks_[0-9a-f]{16}$/);
} finally {
@@ -7378,3 +6668,40 @@ describe("agent snapshot MCP serialization", () => {
expect(content).not.toContain("first answer");
});
});
+
+it("rejects hidden agent-worktree placement and directs callers to workspace creation", async () => {
+ const { agentManager, agentStorage } = createTestDeps();
+ const server = await createAgentMcpServer({
+ agentManager,
+ agentStorage,
+ providerSnapshotManager: createOpenCodeManager().manager,
+ logger: createTestLogger(),
+ });
+ const tool = registeredTool(server, "create_agent");
+ await expect(
+ tool.handler({
+ provider: "codex/gpt-5.4",
+ title: "Worktree agent",
+ initialPrompt: "test",
+ relationship: { kind: "detached" },
+ workspace: {
+ kind: "create",
+ source: {
+ kind: "worktree",
+ cwd: process.cwd(),
+ target: { kind: "branch-off", worktreeSlug: "legacy" },
+ },
+ },
+ }),
+ ).rejects.toThrow("create_workspace");
+ await expect(
+ tool.handler({
+ provider: "codex/gpt-5.4",
+ title: "Worktree agent",
+ initialPrompt: "test",
+ cwd: process.cwd(),
+ worktreeName: "legacy",
+ baseBranch: "main",
+ }),
+ ).rejects.toThrow("create_workspace");
+});
diff --git a/packages/server/src/server/agent/timeline-append.ts b/packages/server/src/server/agent/timeline-append.ts
deleted file mode 100644
index e97fce65c6..0000000000
--- a/packages/server/src/server/agent/timeline-append.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { AgentManager } from "./agent-manager.js";
-import type { AgentTimelineItem } from "./agent-sdk-types.js";
-
-export interface AppendTimelineItemIfAgentKnownOptions {
- agentManager: AgentManager;
- agentId: string;
- item: AgentTimelineItem;
-}
-
-export async function appendTimelineItemIfAgentKnown(
- options: AppendTimelineItemIfAgentKnownOptions,
-): Promise {
- try {
- await options.agentManager.appendTimelineItem(options.agentId, options.item);
- return true;
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- if (message.includes("Unknown agent")) {
- return false;
- }
- throw error;
- }
-}
-
-export async function emitLiveTimelineItemIfAgentKnown(
- options: AppendTimelineItemIfAgentKnownOptions,
-): Promise {
- try {
- await options.agentManager.emitLiveTimelineItem(options.agentId, options.item);
- return true;
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- if (message.includes("Unknown agent")) {
- return false;
- }
- throw error;
- }
-}
diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts
index 30f28426b1..a5117b36b1 100644
--- a/packages/server/src/server/agent/tools/paseo-tools.ts
+++ b/packages/server/src/server/agent/tools/paseo-tools.ts
@@ -33,7 +33,7 @@ import {
requireActiveWorkspaceForArchive,
type ArchiveDependencies,
} from "../../workspace-archive-service.js";
-import { createAgentCommand, type CreateAgentFromMcpInput } from "../create-agent/create.js";
+import { createAgentCommand } from "../create-agent/create.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "../../voice-types.js";
import type { FirstAgentContext } from "../../messages.js";
import { everyMsToFiveFieldCron } from "@getpaseo/protocol/schedule/cadence";
@@ -1030,43 +1030,6 @@ function createToolCatalog(
.strict()
.describe("Create a root agent that does not appear in this agent's subagent track."),
]);
- const AgentCreateWorktreeTargetInputSchema = z.discriminatedUnion("kind", [
- z
- .object({
- kind: z.literal("branch-off"),
- worktreeSlug: z
- .string()
- .min(1)
- .optional()
- .describe("Optional worktree slug/path label. Omit to let Paseo generate one."),
- branchName: z
- .string()
- .min(1)
- .optional()
- .describe("Optional git branch name. Defaults to the worktree slug."),
- baseBranch: z
- .string()
- .min(1)
- .optional()
- .describe("Optional base branch. Defaults to the repository default branch."),
- })
- .strict()
- .describe("Create a new branch in a new Paseo worktree."),
- z
- .object({
- kind: z.literal("checkout-branch"),
- branch: z.string().min(1).describe("Existing branch to check out."),
- })
- .strict()
- .describe("Check out an existing branch in a new Paseo worktree."),
- z
- .object({
- kind: z.literal("checkout-pr"),
- githubPrNumber: z.number().int().positive().describe("GitHub pull request number."),
- })
- .strict()
- .describe("Check out a GitHub pull request in a new Paseo worktree."),
- ]);
const AgentWorkspaceInputSchema = z.discriminatedUnion("kind", [
z
.object({
@@ -1099,16 +1062,6 @@ function createToolCatalog(
.describe("Optional directory path. Defaults to the caller's cwd."),
})
.strict(),
- z
- .object({
- kind: z.literal("worktree"),
- cwd: z
- .string()
- .optional()
- .describe("Optional source repository. Defaults to the caller's cwd."),
- target: AgentCreateWorktreeTargetInputSchema,
- })
- .strict(),
]),
})
.strict()
@@ -1195,32 +1148,6 @@ function createToolCatalog(
.record(z.string(), z.unknown())
.optional()
.describe("Legacy feature values. Prefer settings.features."),
- worktreeName: z
- .string()
- .min(1)
- .optional()
- .describe("Legacy worktree slug. Prefer workspace.source.target.worktreeSlug."),
- branchName: z
- .string()
- .min(1)
- .optional()
- .describe("Legacy branch name. Prefer workspace.source.target.branchName."),
- baseBranch: z
- .string()
- .min(1)
- .optional()
- .describe("Legacy base branch. Prefer workspace.source.target.baseBranch."),
- refName: z
- .string()
- .min(1)
- .optional()
- .describe("Legacy branch/ref to check out. Prefer workspace.source.target.branch."),
- githubPrNumber: z
- .number()
- .int()
- .positive()
- .optional()
- .describe("Legacy GitHub PR number. Prefer workspace.source.target.githubPrNumber."),
};
const createAgentInputSchema = z
.object(scope === "agent" ? agentToAgentInputSchema : canonicalTopLevelInputSchema)
@@ -1560,7 +1487,7 @@ function createToolCatalog(
},
async (args: unknown) => {
const resolvedArgs = await resolveCreateAgentToolArgs(args);
- const { parsedArgs, worktree } = resolvedArgs;
+ const { parsedArgs } = resolvedArgs;
let notifyOnFinish: boolean;
if (resolvedArgs.kind === "agent-scoped") {
notifyOnFinish = parsedArgs.notifyOnFinish;
@@ -1600,7 +1527,6 @@ function createToolCatalog(
detached: resolvedArgs.detached,
callerAgentId,
callerContext,
- worktree,
},
);
@@ -1634,7 +1560,6 @@ function createToolCatalog(
detached: boolean;
cwd: string | undefined;
workspaceId: string | undefined;
- worktree: CreateAgentFromMcpInput["worktree"];
}
| {
kind: "top-level";
@@ -1642,16 +1567,38 @@ function createToolCatalog(
detached: boolean;
cwd: string | undefined;
workspaceId: string | undefined;
- worktree: CreateAgentFromMcpInput["worktree"];
};
async function resolveCreateAgentToolArgs(args: unknown): Promise {
+ const placement = args as {
+ worktreeName?: unknown;
+ branchName?: unknown;
+ baseBranch?: unknown;
+ refName?: unknown;
+ githubPrNumber?: unknown;
+ workspace?: { source?: { kind?: unknown } };
+ };
+ if (
+ placement &&
+ (placement.workspace?.source?.kind === "worktree" ||
+ [
+ placement.worktreeName,
+ placement.branchName,
+ placement.baseBranch,
+ placement.refName,
+ placement.githubPrNumber,
+ ].some((value) => value !== undefined))
+ ) {
+ throw new PaseoToolInputError(
+ "Agent worktree placement was removed. Use create_workspace first, then create_agent with workspaceId.",
+ );
+ }
if (callerAgentId) {
if (hasLegacyCreateAgentPlacement(args)) {
// COMPAT(nestedCreateAgentPlacement): accept the old relationship/workspace shape without
// advertising it to models. Added in v0.2.0; remove after 2027-01-17.
const parsed = parseToolArguments(legacyAgentToAgentCreateAgentArgsSchema, args);
- const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace, {
+ const { cwd, workspaceId } = await resolveCreateAgentWorkspace(parsed.workspace, {
prompt: parsed.initialPrompt,
});
return {
@@ -1660,7 +1607,6 @@ function createToolCatalog(
detached: parsed.relationship.kind === "detached",
cwd,
workspaceId,
- worktree,
};
}
const parsed = parseToolArguments(agentToAgentCreateAgentArgsSchema, args);
@@ -1673,7 +1619,6 @@ function createToolCatalog(
detached: false,
cwd,
workspaceId,
- worktree: undefined,
};
}
if (hasLegacyCreateAgentPlacement(args)) {
@@ -1689,17 +1634,15 @@ function createToolCatalog(
if (!parsedArgs.workspace) {
throw new PaseoToolInputError("Legacy create_agent placement could not be resolved");
}
- const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(
- parsedArgs.workspace,
- { prompt: parsedArgs.initialPrompt },
- );
+ const { cwd, workspaceId } = await resolveCreateAgentWorkspace(parsedArgs.workspace, {
+ prompt: parsedArgs.initialPrompt,
+ });
return {
kind: "top-level",
parsedArgs,
detached: true,
cwd,
workspaceId,
- worktree,
};
}
const parsedArgs = parseToolArguments(canonicalTopLevelCreateAgentArgsSchema, args);
@@ -1713,7 +1656,6 @@ function createToolCatalog(
detached: false,
cwd,
workspaceId,
- worktree: undefined,
};
}
@@ -1770,18 +1712,7 @@ function createToolCatalog(
function normalizeTopLevelCreateAgentArgs(
args: LegacyTopLevelCreateAgentArgs,
): LegacyTopLevelCreateAgentArgs {
- const {
- cwd,
- mode,
- thinking,
- features,
- worktreeName,
- branchName,
- baseBranch,
- refName,
- githubPrNumber,
- ...canonicalCandidate
- } = args;
+ const { cwd, mode, thinking, features, ...canonicalCandidate } = args;
const settings = {
...canonicalCandidate.settings,
...(mode ? { modeId: mode } : {}),
@@ -1804,29 +1735,10 @@ function createToolCatalog(
throw new PaseoToolInputError("cwd is required for legacy top-level create_agent calls");
}
- const legacyWorktreeTarget = resolveLegacyCreateAgentWorktreeTarget({
- worktreeName,
- branchName,
- baseBranch,
- refName,
- githubPrNumber,
- });
- const workspace = legacyWorktreeTarget
- ? {
- kind: "create" as const,
- source: {
- kind: "worktree" as const,
- cwd,
- target: legacyWorktreeTarget,
- },
- }
- : {
- kind: "create" as const,
- source: {
- kind: "directory" as const,
- path: cwd,
- },
- };
+ const workspace = {
+ kind: "create" as const,
+ source: { kind: "directory" as const, path: cwd },
+ };
return parseToolArguments(legacyTopLevelCreateAgentArgsSchema, {
...canonicalCandidate,
@@ -1836,39 +1748,6 @@ function createToolCatalog(
});
}
- function resolveLegacyCreateAgentWorktreeTarget(input: {
- worktreeName?: string;
- branchName?: string;
- baseBranch?: string;
- refName?: string;
- githubPrNumber?: number;
- }): z.infer | null {
- if (input.githubPrNumber !== undefined) {
- return {
- kind: "checkout-pr",
- githubPrNumber: input.githubPrNumber,
- };
- }
-
- if (input.refName) {
- return {
- kind: "checkout-branch",
- branch: input.refName,
- };
- }
-
- if (input.worktreeName || input.branchName || input.baseBranch) {
- return {
- kind: "branch-off",
- worktreeSlug: input.worktreeName,
- branchName: input.branchName,
- baseBranch: input.baseBranch,
- };
- }
-
- return null;
- }
-
async function resolveCreateAgentWorkspace(
workspace:
| LegacyAgentToAgentCreateAgentArgs["workspace"]
@@ -1877,7 +1756,6 @@ function createToolCatalog(
): Promise<{
cwd: string | undefined;
workspaceId: string | undefined;
- worktree: CreateAgentFromMcpInput["worktree"];
}> {
if (workspace.kind === "current") {
if (!callerAgentId) {
@@ -1890,7 +1768,6 @@ function createToolCatalog(
return {
cwd: workspace.cwd,
workspaceId: callerAgent.workspaceId,
- worktree: undefined,
};
}
@@ -1916,7 +1793,6 @@ function createToolCatalog(
return {
cwd,
workspaceId: workspace.workspaceId,
- worktree: undefined,
};
}
@@ -1930,42 +1806,12 @@ function createToolCatalog(
workspaceId: await options.ensureWorkspaceForCreate(cwd, firstAgentContext, {
callerWorkspaceId: resolveCallerAgent()?.workspaceId,
}),
- worktree: undefined,
};
}
- const cwd = resolveScopedCwd(workspace.source.cwd, { required: true });
- return {
- cwd,
- workspaceId: undefined,
- worktree: resolveCreateAgentWorktree(workspace.source.target),
- };
- }
-
- function resolveCreateAgentWorktree(
- target: z.infer,
- ): NonNullable {
- switch (target.kind) {
- case "branch-off":
- return {
- action: "branch-off",
- worktreeName: target.worktreeSlug,
- branchName: target.branchName,
- baseBranch: target.baseBranch,
- };
- case "checkout-branch":
- return {
- action: "checkout",
- refName: target.branch,
- };
- case "checkout-pr":
- return {
- action: "checkout",
- githubPrNumber: target.githubPrNumber,
- };
- default:
- throw new Error("unreachable");
- }
+ throw new PaseoToolInputError(
+ "Use create_workspace first, then create_agent with workspaceId.",
+ );
}
const PROMPTED_AGENT_NOTIFICATION_GUIDANCE =
diff --git a/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts b/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts
index cc0bf08250..f050237620 100644
--- a/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts
+++ b/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts
@@ -200,7 +200,7 @@ async function createPaseoOwnedWorktree(
baseBranch: "main",
branchName: worktreeSlug,
},
- runSetup: false,
+
paseoHome,
});
}
diff --git a/packages/server/src/server/auto-archive-on-merge/behavior.test.ts b/packages/server/src/server/auto-archive-on-merge/behavior.test.ts
index c0e20e36f2..218ae4aee3 100644
--- a/packages/server/src/server/auto-archive-on-merge/behavior.test.ts
+++ b/packages/server/src/server/auto-archive-on-merge/behavior.test.ts
@@ -48,7 +48,7 @@ async function createWorkspaceJourney() {
cwd: repoDir,
worktreeSlug: "workspace",
source: { kind: "branch-off", baseBranch: "main", branchName: "workspace" },
- runSetup: false,
+
paseoHome,
});
const workspaceId = "workspace-under-test";
diff --git a/packages/server/src/server/creation/creation.e2e.test.ts b/packages/server/src/server/creation/creation.e2e.test.ts
index f4d35bd3f3..48cecaa4ac 100644
--- a/packages/server/src/server/creation/creation.e2e.test.ts
+++ b/packages/server/src/server/creation/creation.e2e.test.ts
@@ -4,7 +4,7 @@ import { BUILTIN_PROVIDER_IDS } from "@getpaseo/protocol/provider-manifest";
import { WebSocket } from "ws";
import { SessionInboundMessageSchema, WSOutboundMessageSchema } from "@getpaseo/protocol/messages";
import { randomUUID } from "node:crypto";
-import { mkdtemp, rm } from "node:fs/promises";
+import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test, vi } from "vitest";
@@ -395,3 +395,181 @@ test.each(["agent", "workspace"] as const)(
},
60000,
);
+
+test("combined worktree and agent creation owns setup on the workspace and replays one identity", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "workspace-owned-setup-"));
+ const daemon = await createCreationDaemon();
+ const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
+ try {
+ await initializeSetupRepository(directory);
+ await client.connect();
+ const input = {
+ idempotencyKey: "combined-workspace-setup",
+ source: { kind: "worktree" as const, cwd: directory, worktreeSlug: "combined-setup" },
+ agent: { config: { provider: "codex", cwd: directory } },
+ };
+ const first = await client.createWorkspace(input);
+ const replay = await client.createWorkspace(input);
+ expect(first.error).toBeNull();
+ expect(replay.error).toBeNull();
+ expect(replay.workspace?.id).toBe(first.workspace?.id);
+ expect(replay.agent?.id).toBe(first.agent?.id);
+ if (!first.workspace || !first.agent) throw new Error("Expected combined creation resources");
+ const observation = client.observeWorkspaceSetup(first.workspace.id);
+ await observation.ready;
+ await expect
+ .poll(async () => (await client.fetchWorkspaceSetupStatus(first.workspace!.id)).snapshot, {
+ timeout: 10000,
+ })
+ .toMatchObject({
+ status: "completed",
+ detail: {
+ log: "",
+ commands: [
+ expect.objectContaining({
+ log: expect.stringContaining("workspace setup output\n"),
+ status: "completed",
+ }),
+ ],
+ },
+ });
+ await observation.release();
+ expect(await readFile(join(first.workspace.workspaceDirectory!, "setup-runs"), "utf8")).toBe(
+ "once\n",
+ );
+ const timeline = await client.fetchAgentTimeline(first.agent.id);
+ expect(
+ timeline.entries.map((entry) => entry.item).filter((item) => item.type === "tool_call"),
+ ).toEqual([]);
+ expect(first.agent.workspaceId).toBe(first.workspace.id);
+ const conflict = await client.createWorkspace({
+ ...input,
+ source: { ...input.source, worktreeSlug: "different-intent" },
+ });
+ expect(conflict.error).toBe("workspace_request_key_conflict");
+ } finally {
+ await client.close();
+ await daemon.close();
+ await rm(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
+ }
+}, 60000);
+
+test.each(["create_agent_request", "agent.create.request"] as const)(
+ "%s rejects agent worktree placement at the wire boundary",
+ async (type) => {
+ const directory = await mkdtemp(join(tmpdir(), "agent-worktree-rejected-"));
+ const daemon = await createCreationDaemon();
+ const peer = await connectCreationPeer(daemon.port);
+ try {
+ for (const placement of [{ git: { createWorktree: true } }, { worktreeName: "legacy" }]) {
+ const response = await peer.request({
+ type,
+ requestId: randomUUID(),
+ config: { provider: "codex", cwd: directory },
+ ...placement,
+ });
+ if (response.type !== "agent.create.response" && response.type !== "status")
+ throw new Error("Expected agent creation response");
+ const error = response.payload.error;
+ expect(error).toContain("createWorkspace");
+ }
+ } finally {
+ peer.close();
+ await daemon.close();
+ await rm(directory, { recursive: true, force: true });
+ }
+ },
+);
+
+async function initializeSetupRepository(directory: string): Promise {
+ execFileSync("git", ["init", "-b", "main", directory], { stdio: "pipe" });
+ await writeFile(
+ join(directory, "paseo.json"),
+ JSON.stringify({
+ worktree: {
+ setup: [
+ "node -e \"require('fs').appendFileSync('setup-runs', 'once\\n'); console.log('workspace setup output')\"",
+ ],
+ },
+ }),
+ );
+ execFileSync("git", ["add", "paseo.json"], { cwd: directory });
+ execFileSync(
+ "git",
+ [
+ "-c",
+ "user.name=Test",
+ "-c",
+ "user.email=test@example.com",
+ "-c",
+ "commit.gpgsign=false",
+ "commit",
+ "-m",
+ "setup fixture",
+ ],
+ { cwd: directory, stdio: "pipe" },
+ );
+}
+
+test("Hub-shaped keyed agent worktree creation uses workspace setup and preserves resource identity", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "keyed-agent-workspace-setup-"));
+ const daemon = await createCreationDaemon();
+ const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
+ const peer = await connectCreationPeer(daemon.port);
+ try {
+ await initializeSetupRepository(directory);
+ await client.connect();
+ const input = {
+ type: "create_agent_request" as const,
+ idempotencyKey: "keyed-agent-worktree",
+ config: { provider: "codex", cwd: directory },
+ worktree: { mode: "branch-off" as const, newBranch: "keyed-agent-setup", base: "main" },
+ };
+ const first = await peer.request({ ...input, requestId: "first" });
+ const replay = await peer.request({ ...input, requestId: "replay" });
+ if (
+ first.type !== "status" ||
+ first.payload.status !== "agent_created" ||
+ replay.type !== "status" ||
+ replay.payload.status !== "agent_created"
+ )
+ throw new Error("Expected agent creation responses");
+ expect(replay.payload.agent.id).toBe(first.payload.agent.id);
+ expect(replay.payload.agent.workspaceId).toBe(first.payload.agent.workspaceId);
+ const agent = first.payload.agent;
+ if (!agent.workspaceId) throw new Error("Expected agent workspace ownership");
+ await expect
+ .poll(async () => (await client.fetchWorkspaceSetupStatus(agent.workspaceId!)).snapshot, {
+ timeout: 10000,
+ })
+ .toMatchObject({
+ status: "completed",
+ detail: {
+ log: "",
+ commands: [
+ expect.objectContaining({ log: expect.stringContaining("workspace setup output\n") }),
+ ],
+ },
+ });
+ expect(await readFile(join(agent.cwd, "setup-runs"), "utf8")).toBe("once\n");
+ expect(
+ (await client.fetchAgentTimeline(agent.id)).entries
+ .map((entry) => entry.item)
+ .filter((item) => item.type === "tool_call"),
+ ).toEqual([]);
+ const conflict = await peer.request({
+ ...input,
+ requestId: "conflict",
+ worktree: { ...input.worktree, newBranch: "different-intent" },
+ });
+ expect(conflict).toMatchObject({
+ type: "status",
+ payload: { status: "agent_create_failed", error: "agent_request_key_conflict" },
+ });
+ } finally {
+ peer.close();
+ await client.close();
+ await daemon.close();
+ await rm(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
+ }
+}, 60000);
diff --git a/packages/server/src/server/daemon-e2e/auto-archive-unrelated-github-merge.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/auto-archive-unrelated-github-merge.real.e2e.test.ts
index 67d39b3134..76ed83b3b2 100644
--- a/packages/server/src/server/daemon-e2e/auto-archive-unrelated-github-merge.real.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/auto-archive-unrelated-github-merge.real.e2e.test.ts
@@ -106,14 +106,14 @@ githubTest(
cwd: repository,
worktreeSlug: "merged-feature",
source: { kind: "branch-off", baseBranch: "main", branchName: "merged-feature" },
- runSetup: false,
+
paseoHome: context.daemon.paseoHome,
});
const unrelated = await createWorktree({
cwd: repository,
worktreeSlug: "never-pushed-unrelated",
source: { kind: "branch-off", baseBranch: "main", branchName: "never-pushed-unrelated" },
- runSetup: false,
+
paseoHome: context.daemon.paseoHome,
});
const mergedWorkspace = await context.client.createWorkspace({
diff --git a/packages/server/src/server/daemon-e2e/checkout-pr-merge.e2e.test.ts b/packages/server/src/server/daemon-e2e/checkout-pr-merge.e2e.test.ts
index d686783835..d7f09182f9 100644
--- a/packages/server/src/server/daemon-e2e/checkout-pr-merge.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/checkout-pr-merge.e2e.test.ts
@@ -215,7 +215,7 @@ describe("daemon checkout PR merge loop", () => {
baseBranch: "main",
branchName: "merge-pr-squash",
},
- runSetup: true,
+
paseoHome: ctx.daemon.paseoHome,
});
worktreePath = worktree.worktreePath;
diff --git a/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts b/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts
index 9fdc90635d..a9becb6473 100644
--- a/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts
@@ -20,7 +20,7 @@ interface LegacyCreateWorktreeTestOptions {
cwd: string;
baseBranch: string;
worktreeSlug: string;
- runSetup?: boolean;
+
paseoHome?: string;
}
@@ -39,7 +39,7 @@ function createLegacyWorktreeForTest(
baseBranch: options.baseBranch,
branchName: options.branchName,
},
- runSetup: options.runSetup ?? true,
+
paseoHome: options.paseoHome,
});
}
diff --git a/packages/server/src/server/daemon-e2e/git-operations.e2e.test.ts b/packages/server/src/server/daemon-e2e/git-operations.e2e.test.ts
index be225f171d..2c49f7f79f 100644
--- a/packages/server/src/server/daemon-e2e/git-operations.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/git-operations.e2e.test.ts
@@ -12,74 +12,14 @@ import { execFileSync } from "node:child_process";
import { tmpdir } from "os";
import path from "path";
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
-import { createMessageCollector, type MessageCollector } from "../test-utils/message-collector.js";
import { withTimeout } from "../../utils/promise-timeout.js";
import { deriveWorktreeProjectHash } from "../../utils/worktree.js";
import { isPlatform } from "../../test-utils/platform.js";
-import type { AgentTimelineItem } from "../agent/agent-sdk-types.js";
-import type { SessionOutboundMessage } from "../messages.js";
function tmpCwd(): string {
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
}
-function findTimelineToolCall(
- messages: SessionOutboundMessage[],
- agentId: string,
- predicate: (item: AgentTimelineItem) => boolean,
-): AgentTimelineItem | null {
- for (let i = messages.length - 1; i >= 0; i -= 1) {
- const msg = messages[i];
- if (msg?.type !== "agent_stream") {
- continue;
- }
- if (msg.payload.agentId !== agentId) {
- continue;
- }
- const event = msg.payload.event as { type?: string; item?: AgentTimelineItem };
- if (event?.type !== "timeline") {
- continue;
- }
- const item = event.item as AgentTimelineItem;
- if (item?.type === "tool_call" && predicate(item)) {
- return item;
- }
- }
- return null;
-}
-
-async function waitForTimelineToolCall(
- messages: SessionOutboundMessage[],
- agentId: string,
- predicate: (item: AgentTimelineItem) => boolean,
- timeoutMs = 10000,
-): Promise> {
- const startTime = Date.now();
- while (Date.now() - startTime < timeoutMs) {
- const existing = findTimelineToolCall(messages, agentId, predicate);
- if (existing && existing.type === "tool_call") {
- return existing;
- }
- await new Promise((resolve) => setTimeout(resolve, 50));
- }
- const recentToolCalls: Array<{ name: string; status?: string; callId?: string }> = [];
- for (let i = messages.length - 1; i >= 0 && recentToolCalls.length < 10; i -= 1) {
- const msg = messages[i];
- if (msg?.type !== "agent_stream") continue;
- if (msg.payload.agentId !== agentId) continue;
- const event = msg.payload.event as { type?: string; item?: AgentTimelineItem };
- if (event?.type !== "timeline") continue;
- const item = event.item as AgentTimelineItem;
- if (item?.type !== "tool_call") continue;
- recentToolCalls.push({ name: item.name, status: item.status, callId: item.callId });
- }
- throw new Error(
- `Timed out waiting for timeline tool_call (${agentId}). Recent tool_calls: ${JSON.stringify(
- recentToolCalls,
- )}`,
- );
-}
-
async function waitForPathExists(options: {
targetPath: string;
timeoutMs: number;
@@ -121,43 +61,17 @@ async function withShell(shell: string, run: () => Promise): Promise {
}
}
-interface WorktreeTerminalBootstrapEntry {
- name: string | null;
- command: string;
- status: "started" | "failed";
- terminalId: string | null;
- error: string | null;
-}
-
-function getWorktreeTerminalBootstrapEntries(
- item: Extract,
-): WorktreeTerminalBootstrapEntry[] | null {
- const detail = item.detail;
- if (!detail || detail.type !== "unknown" || !detail.output) {
- return null;
- }
- const output = detail.output as Record;
- const terminals = output.terminals;
- if (!Array.isArray(terminals)) {
- return null;
- }
- return terminals as WorktreeTerminalBootstrapEntry[];
-}
-
// Use gpt-5.4-mini with low thinking preset for faster test execution
const CODEX_TEST_MODEL = "gpt-5.4-mini";
const CODEX_TEST_THINKING_OPTION_ID = "low";
let ctx: DaemonTestContext;
-let collector: MessageCollector;
beforeEach(async () => {
ctx = await createDaemonTestContext();
- collector = createMessageCollector(ctx.client);
});
afterEach(async () => {
- collector.unsubscribe();
await ctx.cleanup();
}, 60000);
@@ -423,7 +337,7 @@ test("returns isGit false for non-git directory", async () => {
rmSync(cwd, { recursive: true, force: true });
}, 60000); // 1 minute timeout
-test("runs paseo.json setup asynchronously and reports status via timeline tool_call", async () => {
+test("runs paseo.json setup asynchronously and reports workspace status", async () => {
const repoRoot = tmpCwd();
const { execSync } = await import("child_process");
@@ -461,13 +375,7 @@ test("runs paseo.json setup asynchronously and reports status via timeline tool_
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
cwd: repoRoot,
title: "Async Worktree Setup Test",
- git: {
- createWorktree: true,
- createNewBranch: true,
- baseBranch: "main",
- newBranchName: "async-setup-test",
- worktreeSlug: "async-setup-test",
- },
+ worktree: { mode: "branch-off", newBranch: "async-setup-test", base: "main" },
}),
timeoutMs: 2500,
label: "createAgent should not block on setup",
@@ -478,19 +386,11 @@ test("runs paseo.json setup asynchronously and reports status via timeline tool_
writeFileSync(path.join(agent.cwd, "allow-setup"), "ok\n");
- const completed = await waitForTimelineToolCall(
- collector.messages,
- agent.id,
- (item) => item.name === "paseo_worktree_setup" && item.status === "completed",
- 20000,
- );
-
- expect(completed.callId).toBeTruthy();
- expect(completed.detail.type).toBe("worktree_setup");
- if (completed.detail.type === "worktree_setup") {
- expect(completed.detail.commands.length).toBeGreaterThan(0);
- expect(completed.detail.log.length).toBeGreaterThan(0);
- }
+ const completed = await waitForWorkspaceSetup(agent.workspaceId!, "completed");
+ expect(completed.detail.log).toBe("");
+ expect(completed.detail.commands).toEqual([
+ expect.objectContaining({ status: "completed", exitCode: 0 }),
+ ]);
expect(existsSync(path.join(agent.cwd, "setup-done.txt"))).toBe(true);
await ctx.client.deleteAgent(agent.id);
@@ -549,13 +449,7 @@ test("bootstraps configured worktree terminals after setup succeeds", async () =
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
cwd: repoRoot,
title: "Async Worktree Setup + Terminals Test",
- git: {
- createWorktree: true,
- createNewBranch: true,
- baseBranch: "main",
- newBranchName: "async-setup-terminals-test",
- worktreeSlug: "async-setup-terminals-test",
- },
+ worktree: { mode: "branch-off", newBranch: "async-setup-terminals-test", base: "main" },
}),
timeoutMs: 2500,
label: "createAgent should not block on setup",
@@ -568,25 +462,12 @@ test("bootstraps configured worktree terminals after setup succeeds", async () =
writeFileSync(path.join(agent.cwd, "allow-setup"), "ok\n");
- await waitForTimelineToolCall(
- collector.messages,
- agent.id,
- (item) => item.name === "paseo_worktree_setup" && item.status === "completed",
- 20000,
- );
- const terminalsBootstrapToolCall = await waitForTimelineToolCall(
- collector.messages,
- agent.id,
- (item) => item.name === "paseo_worktree_terminals" && item.status === "completed",
- 30000,
- );
- const bootstrappedTerminals = getWorktreeTerminalBootstrapEntries(terminalsBootstrapToolCall);
- expect(bootstrappedTerminals).toBeTruthy();
- expect(bootstrappedTerminals?.length ?? 0).toBeGreaterThanOrEqual(2);
- const failedBootstraps =
- bootstrappedTerminals?.filter((terminal) => terminal.status === "failed") ?? [];
- expect(failedBootstraps).toEqual([]);
-
+ await waitForWorkspaceSetup(agent.workspaceId!, "completed");
+ await expect
+ .poll(async () => (await ctx.client.listTerminals(agent.cwd)).terminals.length, {
+ timeout: 30000,
+ })
+ .toBe(2);
const list = await ctx.client.listTerminals(agent.cwd);
expect(list.error).toBeUndefined();
expect(list.terminals.some((terminal) => terminal.name === "Dev Server")).toBe(true);
@@ -639,7 +520,7 @@ test("bootstraps configured worktree terminals after setup succeeds", async () =
});
}, 60000);
-test("reports failures via timeline tool_call without deleting the created worktree", async () => {
+test("reports workspace setup failures without deleting the created worktree", async () => {
const repoRoot = tmpCwd();
const { execSync } = await import("child_process");
@@ -687,13 +568,7 @@ test("reports failures via timeline tool_call without deleting the created workt
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
cwd: repoRoot,
title: "Async Worktree Setup Failure Test",
- git: {
- createWorktree: true,
- createNewBranch: true,
- baseBranch: "main",
- newBranchName: "async-setup-failure-test",
- worktreeSlug: "async-setup-failure-test",
- },
+ worktree: { mode: "branch-off", newBranch: "async-setup-failure-test", base: "main" },
}),
timeoutMs: 2500,
label: "createAgent should not block on failing setup",
@@ -702,23 +577,7 @@ test("reports failures via timeline tool_call without deleting the created workt
expect(agent.cwd).toContain(path.join(".paseo", "worktrees"));
expect(existsSync(agent.cwd)).toBe(true);
- const started = await waitForTimelineToolCall(
- collector.messages,
- agent.id,
- (item) => item.name === "paseo_worktree_setup" && item.status === "running",
- 10000,
- );
-
- const failed = await waitForTimelineToolCall(
- collector.messages,
- agent.id,
- (item) =>
- item.name === "paseo_worktree_setup" &&
- item.callId === started.callId &&
- item.status === "failed",
- 20000,
- );
-
+ const failed = await waitForWorkspaceSetup(agent.workspaceId!, "failed");
expect(existsSync(path.join(agent.cwd, "setup-start.txt"))).toBe(true);
expect(existsSync(path.join(agent.cwd, "should-not-run.txt"))).toBe(false);
@@ -726,7 +585,8 @@ test("reports failures via timeline tool_call without deleting the created workt
if (failed.detail.type === "worktree_setup") {
expect(Array.isArray(failed.detail.commands)).toBe(true);
expect(failed.detail.commands[0]?.exitCode).toBe(7);
- expect(failed.detail.log).toContain("Exit 7");
+ expect(failed.detail.log).toBe("");
+ expect(failed.detail.commands[0]?.log).toContain("boom");
}
await ctx.client.deleteAgent(agent.id);
@@ -756,13 +616,7 @@ test("creates agent in ~/.paseo/worktrees/{hash} when worktree is requested", as
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
cwd,
title: "Worktree Agent Test",
- git: {
- createWorktree: true,
- createNewBranch: true,
- newBranchName: "worktree-test",
- worktreeSlug: "worktree-test",
- baseBranch: "main",
- },
+ worktree: { mode: "branch-off", newBranch: "worktree-test", base: "main" },
});
expect(agent.id).toBeTruthy();
@@ -776,7 +630,7 @@ test("creates agent in ~/.paseo/worktrees/{hash} when worktree is requested", as
rmSync(cwd, { recursive: true, force: true });
}, 60000);
-test("archiving a worktree shuts down its terminals but leaves the worktree on disk", async () => {
+test("archiving a worktree shuts down terminals and removes its final workspace backing", async () => {
const repoRoot = tmpCwd();
const { execSync } = await import("child_process");
@@ -823,13 +677,7 @@ test("archiving a worktree shuts down its terminals but leaves the worktree on d
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
cwd: repoRoot,
title: "Worktree Archive Cleanup Test",
- git: {
- createWorktree: true,
- createNewBranch: true,
- baseBranch: "main",
- newBranchName: "archive-cleanup-test",
- worktreeSlug: "archive-cleanup-test",
- },
+ worktree: { mode: "branch-off", newBranch: "archive-cleanup-test", base: "main" },
}),
timeoutMs: 2500,
label: "createAgent should not block on setup",
@@ -858,14 +706,23 @@ test("archiving a worktree shuts down its terminals but leaves the worktree on d
expect(archive.success).toBe(true);
expect(archive.removedAgents).toContain(agent.id);
- // Archiving tears down the workspace's terminals but never deletes the
- // worktree from disk and never runs teardown commands — on-disk removal is a
- // separate, explicit step.
- expect(existsSync(agent.cwd)).toBe(true);
- expect(existsSync(teardownMarkerPath)).toBe(false);
+ // The final workspace reference owns teardown and backing-worktree removal.
+ expect(existsSync(agent.cwd)).toBe(false);
+ expect(readFileSync(teardownMarkerPath, "utf8").trim()).toBe(agent.cwd);
const afterArchiveDirectories = ctx.daemon.daemon.terminalManager.listDirectories();
expect(afterArchiveDirectories).not.toContain(agent.cwd);
rmSync(repoRoot, { recursive: true, force: true });
}, 60000);
+
+async function waitForWorkspaceSetup(workspaceId: string, status: "completed" | "failed") {
+ await expect
+ .poll(async () => (await ctx.client.fetchWorkspaceSetupStatus(workspaceId)).snapshot?.status, {
+ timeout: 20000,
+ })
+ .toBe(status);
+ const snapshot = (await ctx.client.fetchWorkspaceSetupStatus(workspaceId)).snapshot;
+ if (!snapshot) throw new Error("Expected workspace setup snapshot");
+ return snapshot;
+}
diff --git a/packages/server/src/server/paseo-worktree-service.test.ts b/packages/server/src/server/paseo-worktree-service.test.ts
index a5120aa5ff..0e0ccf34f7 100644
--- a/packages/server/src/server/paseo-worktree-service.test.ts
+++ b/packages/server/src/server/paseo-worktree-service.test.ts
@@ -59,7 +59,7 @@ test("creates a worktree and registers it in the source workspace project withou
cwd: repoDir,
worktreeSlug: "feature-one",
title: "Feature One",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -114,7 +114,7 @@ test("refreshes a source project that became Git while creating a worktree", asy
{
cwd: repoDir,
worktreeSlug: "project-became-git",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -148,7 +148,7 @@ test("repairs a legacy source workspace whose project record is missing", async
{
cwd: repoDir,
worktreeSlug: "repaired-source",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -192,7 +192,7 @@ test("uses an equivalent source workspace path when creating a worktree", async
{
cwd: sourceDir,
worktreeSlug: "equivalent-source",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -221,7 +221,7 @@ test("creates a worktree workspace at the selected project subdirectory", async
cwd: sourceDir,
projectId: project.projectId,
worktreeSlug: "selected-subdirectory",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -249,7 +249,7 @@ test("seeds an uncommitted exact-project config into the mapped worktree directo
{
cwd: sourceDir,
worktreeSlug: "seed-nested-config",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
createDeps(),
@@ -277,7 +277,7 @@ test("does not overwrite a committed exact-project config with source checkout e
{
cwd: sourceDir,
worktreeSlug: "preserve-nested-config",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
createDeps(),
@@ -314,7 +314,7 @@ test("removes a new worktree when its ref does not contain the selected project
action: "checkout",
refName: "without-subproject",
worktreeSlug: "missing-subproject",
- runSetup: false,
+
paseoHome,
},
deps,
@@ -345,7 +345,7 @@ test("removes a new worktree when workspace persistence fails", async () => {
cwd: repoDir,
projectId: "missing-project",
worktreeSlug: "persistence-failure",
- runSetup: false,
+
paseoHome,
},
createDeps(),
@@ -373,7 +373,7 @@ test("maps a nested cwd from an existing Paseo worktree into the next worktree",
{
cwd: repoDir,
worktreeSlug: "source-worktree",
- runSetup: false,
+
paseoHome,
},
deps,
@@ -384,7 +384,7 @@ test("maps a nested cwd from an existing Paseo worktree into the next worktree",
{
cwd: sourceCwd,
worktreeSlug: "nested-worktree",
- runSetup: false,
+
paseoHome,
},
deps,
@@ -408,7 +408,7 @@ test("rejects source checkout planning before creating a worktree", async () =>
{
cwd: repoDir,
worktreeSlug: "must-not-create",
- runSetup: false,
+
paseoHome,
},
deps,
@@ -443,7 +443,7 @@ test("registers a new worktree in the existing root project after the main check
cwd: repoDir,
projectId: sourceProject.projectId,
worktreeSlug: "second-worktree",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -472,7 +472,7 @@ test("an explicit project FK remains unchanged when its worktree comes from anot
cwd: repoDir,
projectId: project.projectId,
worktreeSlug: "attached-worktree",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -502,7 +502,7 @@ test.skipIf(isPlatform("win32"))(
{
cwd: repoDir,
worktreeSlug: "reuse-me",
- runSetup: false,
+
paseoHome,
},
firstDeps,
@@ -518,7 +518,7 @@ test.skipIf(isPlatform("win32"))(
{
cwd: repoDir,
worktreeSlug: "reuse-me",
- runSetup: false,
+
paseoHome,
},
deps,
@@ -541,7 +541,7 @@ test("renames an eligible unnamed branch-off worktree once on first agent contex
{
cwd: repoDir,
worktreeSlug: "dazzling-yak",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -610,7 +610,7 @@ test("falls back to a numeric suffix when the desired branch name already exists
{
cwd: repoDir,
worktreeSlug: "dazzling-yak",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
createDeps(),
@@ -647,7 +647,7 @@ test("renames the branch even when the app supplies a random placeholder slug",
cwd: repoDir,
worktreeSlug: "dazzling-yak",
firstAgentContext: { prompt: "Investigate the failing login flow" },
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -695,7 +695,7 @@ test("renames the branch from a github_pr attachment when no prompt is supplied"
},
],
},
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -740,7 +740,7 @@ test("leaves the branch alone when generated branch text is invalid", async () =
cwd: repoDir,
worktreeSlug: "dazzling-yak",
firstAgentContext: { prompt: "Name this branch" },
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
createDeps(),
@@ -785,7 +785,7 @@ test("does not mark checkout branch worktrees as eligible for first-agent rename
cwd: repoDir,
action: "checkout",
refName: "dev",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
createDeps(),
@@ -825,7 +825,7 @@ test("does not mark GitHub PR checkout worktrees as eligible for first-agent ren
cwd: repoDir,
action: "checkout",
githubPrNumber: 123,
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
createDeps(),
@@ -862,7 +862,7 @@ test("does not mutate registries or broadcast when core worktree creation fails"
{
cwd: tempDir,
worktreeSlug: "not-git",
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
@@ -888,7 +888,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
- runSetup: false,
+
paseoHome,
});
expect(existsSync(created.worktreePath)).toBe(true);
@@ -903,7 +903,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
- runSetup: false,
+
paseoHome,
});
@@ -947,7 +947,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
- runSetup: false,
+
paseoHome,
});
expect(existsSync(created.worktreePath)).toBe(true);
@@ -970,7 +970,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
- runSetup: false,
+
paseoHome,
}),
).rejects.toThrow("missing but already registered worktree");
@@ -982,7 +982,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
- runSetup: false,
+
paseoHome,
});
@@ -1010,7 +1010,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "gone-branch",
source: { kind: "checkout-branch", branchName: "gone-branch" },
- runSetup: false,
+
paseoHome: path.join(tempDir, ".paseo"),
}),
).rejects.toMatchObject({ name: "UnknownBranchError" });
@@ -1029,7 +1029,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "busy-branch",
source: { kind: "checkout-branch", branchName: "busy-branch" },
- runSetup: false,
+
paseoHome,
});
expect(existsSync(first.worktreePath)).toBe(true);
@@ -1038,7 +1038,7 @@ test.skipIf(isPlatform("win32"))(
cwd: repoDir,
worktreeSlug: "busy-branch-again",
source: { kind: "checkout-branch", branchName: "busy-branch" },
- runSetup: false,
+
paseoHome,
});
diff --git a/packages/server/src/server/paseo-worktree-service.ts b/packages/server/src/server/paseo-worktree-service.ts
index 94e82d5e95..61e00723d0 100644
--- a/packages/server/src/server/paseo-worktree-service.ts
+++ b/packages/server/src/server/paseo-worktree-service.ts
@@ -135,7 +135,7 @@ async function createPaseoWorktreeWithPriority(
{
cwd: createdWorktree.repoRoot,
worktreePath: createdWorktree.worktree.worktreePath,
- ...(input.runSetup === false ? { teardownCwds: [] } : {}),
+ teardownCwds: [],
paseoHome: input.paseoHome,
worktreesBaseRoot: input.worktreesRoot,
},
diff --git a/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts b/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts
index fb23daa6f0..34d1483023 100644
--- a/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts
+++ b/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts
@@ -411,7 +411,7 @@ test("create_agent_request rejects legacy git options before creating a worktree
},
initialPrompt: "Say done.",
}),
- ).rejects.toThrow("worktree cannot be combined with git options");
+ ).rejects.toThrow("createWorkspace");
await expectActiveAgentListEmpty();
await expectWorktreeListEmpty(repoDir);
@@ -447,72 +447,6 @@ function storedAgentIds(): string[] {
.map((entry) => path.basename(entry, ".json"));
}
-test.each(["git", "worktreeName"] as const)(
- "legacy %s worktree creation resolves final workspace visibility without intermediate records",
- async (placement) => {
- const cwd = createGitRepo();
- const config = { ...getFullAccessConfig("codex"), cwd };
- const backgroundCaller = await ctx.client.createAgent({ config, background: true });
- const visibleCaller = await ctx.client.createAgent({ config });
- const cases = [
- { name: "review", background: true, expected: true },
- { name: "review-child", callerAgentId: backgroundCaller.id, expected: true },
- {
- name: "visible-child",
- callerAgentId: backgroundCaller.id,
- background: false,
- expected: false,
- },
- { name: "hidden-child", callerAgentId: visibleCaller.id, background: true, expected: true },
- { name: "human-default", expected: false },
- ];
- const orderedCases =
- placement === "worktreeName" ? [cases[1]!, cases[0]!, ...cases.slice(2)] : cases;
- for (const entry of orderedCases) {
- const before = await ctx.client.fetchWorkspaces({ filter: { includeBackground: true } });
- const child = await ctx.client.createAgent({
- config,
- ...(entry.callerAgentId ? { callerAgentId: entry.callerAgentId } : {}),
- ...(entry.background !== undefined ? { background: entry.background } : {}),
- ...(placement === "git"
- ? {
- git: {
- createWorktree: true,
- createNewBranch: true,
- newBranchName: entry.name,
- baseBranch: "main",
- },
- }
- : { worktreeName: entry.name }),
- });
- const inclusive = await ctx.client.fetchWorkspaces({ filter: { includeBackground: true } });
- const created = inclusive.entries.filter(
- (workspace) => !before.entries.some((existing) => existing.id === workspace.id),
- );
- expect(created).toHaveLength(1);
- expect(created[0]).toMatchObject({
- id: child.workspaceId,
- workspaceDirectory: child.cwd,
- background: entry.expected,
- workspaceKind: "worktree",
- });
- expect(child.cwd).not.toBe(cwd);
- expect(child.labels?.["paseo.parent-agent-id"]).toBe(entry.callerAgentId);
- const defaultAgents = await ctx.client.fetchAgents();
- const allAgents = await ctx.client.fetchAgents({ filter: { includeBackground: true } });
- expect(defaultAgents.entries.some((item) => item.agent.id === child.id)).toBe(
- !entry.expected,
- );
- expect(allAgents.entries.some((item) => item.agent.id === child.id)).toBe(true);
- const defaultWorkspaces = await ctx.client.fetchWorkspaces();
- expect(
- defaultWorkspaces.entries.some((workspace) => workspace.id === child.workspaceId),
- ).toBe(!entry.expected);
- }
- },
- 30_000,
-);
-
test("background workspaces keep durable agents addressable through restart and archive", async () => {
const cwd = createGitRepo();
const config = { ...getFullAccessConfig("codex"), cwd };
diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts
index e114d8b26e..5b85d12a2b 100644
--- a/packages/server/src/server/session.test.ts
+++ b/packages/server/src/server/session.test.ts
@@ -4694,7 +4694,6 @@ describe("session paseo worktree creation handling", () => {
await asSessionInternals(session).createPaseoWorktree({
cwd: "/tmp/repo",
worktreeSlug: "new-worktree",
- runSetup: false,
});
expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo", {
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index 87949594aa..f2e5e28bf2 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -29,7 +29,6 @@ import {
type SessionInboundMessage,
type SessionOutboundMessage,
type ScriptStatusUpdateMessage,
- type GitSetupOptions,
type StartWorkspaceScriptRequest,
type WorkspaceScriptListRequest,
type WorkspaceScriptStartRequest,
@@ -122,10 +121,6 @@ import {
updateAgentCommand,
} from "./agent/lifecycle-command.js";
import { buildStoredAgentPayload, toAgentPayload } from "./agent/agent-projections.js";
-import {
- appendTimelineItemIfAgentKnown,
- emitLiveTimelineItemIfAgentKnown,
-} from "./agent/timeline-append.js";
import { assertPluginTimelineDataSize } from "./agent/agent-timeline-content.js";
import { parsePluginClientId } from "./plugins/plugin-session-identity.js";
import { buildAgentForkContextAttachment } from "./agent/activity-curator.js";
@@ -249,10 +244,7 @@ import {
} from "./paseo-worktree-service.js";
import { WorkspaceAutoName } from "./workspace-auto-name.js";
import {
- buildAgentSessionConfig as buildWorktreeAgentSessionConfig,
- normalizeAgentCreationGitOptions,
createPaseoWorktreeWorkflow as createWorktreeWorkflow,
- type CreatePaseoWorktreeSetupContinuationInput,
type CreatePaseoWorktreeWorkflowResult,
handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest,
handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest,
@@ -4293,17 +4285,19 @@ export class Session {
}
}
- private validatePublicAgentWorkspaceIntent(
- request: CreateAgentRequestMessage,
- createsWorktree: boolean,
- ): void {
+ private validatePublicAgentWorkspaceIntent(request: CreateAgentRequestMessage): void {
+ if (request.git !== undefined || request.worktreeName !== undefined) {
+ throw new Error(
+ "Legacy git/worktreeName agent placement was removed. Use createWorkspace({ source, agent, idempotencyKey }) to create a workspace with an agent, or create the workspace first and pass workspaceId.",
+ );
+ }
if (request.internal)
throw new Error(
"Public internal agents were removed; create a background workspace instead.",
);
if (
request.background !== undefined &&
- (request.workspaceId || (request.callerAgentId && !createsWorktree))
+ (request.workspaceId || (request.callerAgentId && !request.worktree))
) {
throw new Error(
"background is workspace creation intent; configure the selected workspace at creation.",
@@ -4322,7 +4316,6 @@ export class Session {
initialPrompt,
clientMessageId,
outputSchema,
- git,
worktree,
autoArchive,
images,
@@ -4340,14 +4333,11 @@ export class Session {
let createdAgentId: string | null = null;
try {
const requestedCwd = resolve(config.cwd);
- const needsRequestedDirectory =
- Boolean(worktreeName || git || worktree) || (!msg.workspaceId && !msg.callerAgentId);
+ const needsRequestedDirectory = Boolean(worktree) || (!msg.workspaceId && !msg.callerAgentId);
if (needsRequestedDirectory && !(await this.filesystem.isDirectory(requestedCwd))) {
throw new Error(`Working directory does not exist or is not a directory: ${requestedCwd}`);
}
- // Resolve all supported worktree input forms before provisioning workspace ownership.
- const gitIntent = normalizeAgentCreationGitOptions(git, worktreeName, worktree);
- this.validatePublicAgentWorkspaceIntent(msg, gitIntent?.createWorktree === true);
+ this.validatePublicAgentWorkspaceIntent(msg);
const trimmedPrompt = initialPrompt?.trim();
const { provisionalTitle } = resolveCreateAgentTitles({
configTitle: config.title,
@@ -4361,9 +4351,8 @@ export class Session {
const workspacePromptTitle = resolveFirstAgentPromptTitle(firstAgentContext);
const createdWorktree = await this.createAgentLifecycleDispatch.createWorktreeForRequest({
cwd: config.cwd,
- target: gitIntent,
+ target: worktree,
firstAgentContext,
- setupContinuation: worktree ? undefined : this.agentWorktreeSetupContinuation(),
background: msg.background,
callerWorkspaceId: this.getCallerWorkspaceId(msg.callerAgentId),
});
@@ -4396,7 +4385,6 @@ export class Session {
agentId,
config: resolvedIntent.config,
workspaceId: resolvedIntent.intent.workspaceId,
- worktreeName,
initialPrompt,
source: msg.callerAgentId
? { kind: "agent-message", agentId: msg.callerAgentId }
@@ -4405,19 +4393,10 @@ export class Session {
outputSchema,
images,
attachments,
- git,
labels: resolvedIntent.intent.labels,
env,
provisionalTitle,
firstAgentContext,
- buildSessionConfig: async (sessionConfig, gitOptions, legacyWorktreeName, ctx) =>
- createdWorktree
- ? {
- sessionConfig,
- setupContinuation: createdWorktree.setupContinuation,
- createdWorkspaceId: createdWorktree.workspace.workspaceId,
- }
- : this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx),
},
);
createdAgentId = snapshot.id;
@@ -4887,58 +4866,6 @@ export class Session {
}
}
- private agentWorktreeSetupContinuation(): CreatePaseoWorktreeSetupContinuationInput {
- return {
- kind: "agent",
- terminalManager: this.terminalManager,
- appendTimelineItem: ({ agentId, item }) =>
- appendTimelineItemIfAgentKnown({
- agentManager: this.agentManager,
- agentId,
- item,
- }),
- emitLiveTimelineItem: ({ agentId, item }) =>
- emitLiveTimelineItemIfAgentKnown({
- agentManager: this.agentManager,
- agentId,
- item,
- }),
- logger: this.sessionLogger,
- };
- }
-
- private async buildAgentSessionConfig(
- config: AgentSessionConfig,
- gitOptions?: GitSetupOptions,
- legacyWorktreeName?: string,
- firstAgentContext?: FirstAgentContext,
- ): Promise<{
- sessionConfig: AgentSessionConfig;
- setupContinuation?: CreatePaseoWorktreeWorkflowResult["setupContinuation"];
- createdWorkspaceId?: string;
- }> {
- return buildWorktreeAgentSessionConfig(
- {
- paseoHome: this.paseoHome,
- worktreesRoot: this.worktreesRoot,
- sessionLogger: this.sessionLogger,
- workspaceGitService: this.workspaceGitService,
- createPaseoWorktree: (input, serviceOptions) =>
- this.createPaseoWorktreeWorkflow(input, {
- ...serviceOptions,
- setupContinuation: this.agentWorktreeSetupContinuation(),
- }),
- checkoutExistingBranch: (cwd, branch) =>
- this.gitMutation.checkoutExistingBranch(cwd, branch),
- createBranchFromBase: (params) => this.gitMutation.createBranchFromBase(params),
- },
- config,
- gitOptions,
- legacyWorktreeName,
- firstAgentContext,
- );
- }
-
private isPathWithinRoot(rootPath: string, candidatePath: string): boolean {
const resolvedRoot = resolve(rootPath);
const resolvedCandidate = resolve(candidatePath);
@@ -7400,7 +7327,6 @@ export class Session {
input: CreatePaseoWorktreeInput,
options?: {
resolveDefaultBranch?: (repoRoot: string) => Promise;
- setupContinuation?: CreatePaseoWorktreeSetupContinuationInput;
},
): Promise {
return createWorktreeWorkflow(
diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts
index fafcbc182a..89bc5a43e3 100644
--- a/packages/server/src/server/session.workspaces.test.ts
+++ b/packages/server/src/server/session.workspaces.test.ts
@@ -5704,7 +5704,7 @@ test("legacy refresh_agent_request restores a real deleted worktree", async () =
cwd: repoDir,
worktreeSlug: "keep",
source: { kind: "checkout-branch", branchName: branch },
- runSetup: false,
+
paseoHome,
worktreesRoot,
});
@@ -5952,7 +5952,7 @@ test("archive_workspace_request archives a worktree-kind workspace and removes t
baseBranch: "main",
branchName: "worktree-kind-archive",
},
- runSetup: false,
+
paseoHome,
});
diff --git a/packages/server/src/server/session/checkout/checkout-session.test.ts b/packages/server/src/server/session/checkout/checkout-session.test.ts
index bc65b01d87..aaa79e3da1 100644
--- a/packages/server/src/server/session/checkout/checkout-session.test.ts
+++ b/packages/server/src/server/session/checkout/checkout-session.test.ts
@@ -1746,7 +1746,6 @@ it("creates a PR from a restored exact base using the host metadata and a forge
paseoHome,
worktreeSlug: "restored-pr",
source: { kind: "branch-off", baseBranch: baseRef, branchName: "restored-pr" },
- runSetup: false,
});
writeFileSync(join(created.worktreePath, "feature.txt"), "feature\n");
git(created.worktreePath, "add", ".");
@@ -1757,7 +1756,6 @@ it("creates a PR from a restored exact base using the host metadata and a forge
paseoHome,
worktreeSlug: "restored-pr",
source: { kind: "restore", branchName: created.branchName, baseRef },
- runSetup: false,
});
const requests: Array[0]> = [];
const service: ForgeService = {
diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts
index 964f305eb8..f738a0b0d6 100644
--- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts
+++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts
@@ -153,7 +153,7 @@ describe("workspace recovery", () => {
cwd: repoDir,
worktreeSlug: "mixed-project",
source: { kind: "checkout-branch", branchName: branch },
- runSetup: false,
+
paseoHome,
worktreesRoot,
});
@@ -209,7 +209,7 @@ describe("workspace recovery", () => {
cwd: repoDir,
worktreeSlug: "without-subproject",
source: { kind: "checkout-branch", branchName: branch },
- runSetup: false,
+
paseoHome,
worktreesRoot,
});
@@ -303,7 +303,6 @@ async function createBaseRecoveryFixture(baseBranch: string | null) {
paseoHome,
worktreeSlug: "base-recovery",
source: { kind: "branch-off", branchName: "feature", baseBranch: "main" },
- runSetup: false,
});
writeFileSync(join(created.worktreePath, "feature.txt"), "feature\n");
execFileSync("git", ["add", "."], { cwd: created.worktreePath });
diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts
index 8b0e9a853a..1a715dd349 100644
--- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts
+++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts
@@ -184,7 +184,7 @@ export function createWorkspaceRecoveryService(deps: {
cwd: sourceRepoRoot,
worktreeSlug: basename(previousWorktreePath),
source,
- runSetup: false,
+
paseoHome: deps.paseoHome,
worktreesRoot: deps.worktreesRoot,
});
diff --git a/packages/server/src/server/wire-compat.test.ts b/packages/server/src/server/wire-compat.test.ts
index 22c8661544..a7b36f521d 100644
--- a/packages/server/src/server/wire-compat.test.ts
+++ b/packages/server/src/server/wire-compat.test.ts
@@ -550,7 +550,7 @@ describe("wire compatibility", () => {
refName: undefined,
action: undefined,
githubPrNumber: undefined,
- runSetup: false,
+
paseoHome: "/tmp/paseo-home",
});
});
diff --git a/packages/server/src/server/workspace-archive-record-scoped.e2e.test.ts b/packages/server/src/server/workspace-archive-record-scoped.e2e.test.ts
index 87cb758ac5..df229f9221 100644
--- a/packages/server/src/server/workspace-archive-record-scoped.e2e.test.ts
+++ b/packages/server/src/server/workspace-archive-record-scoped.e2e.test.ts
@@ -329,7 +329,7 @@ test("archiving an unregistered worktree in a bare repo clears the main checkout
cwd: mainCheckout,
source: { kind: "branch-off", baseBranch: "main", branchName: "cached-bare-worktree" },
worktreeSlug: "cached-bare-worktree",
- runSetup: false,
+
paseoHome: ctx.daemon.paseoHome,
});
expect(await activeWorkspaceIds()).toEqual(new Set());
diff --git a/packages/server/src/server/workspace-archive-service.test.ts b/packages/server/src/server/workspace-archive-service.test.ts
index d1cfb8ada7..a1a901c4d6 100644
--- a/packages/server/src/server/workspace-archive-service.test.ts
+++ b/packages/server/src/server/workspace-archive-service.test.ts
@@ -110,7 +110,7 @@ async function createPaseoOwnedWorktree(
baseBranch: "main",
branchName: worktreeSlug,
},
- runSetup: false,
+
paseoHome,
});
}
diff --git a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts
index 2d5c60a752..fc9efe1684 100644
--- a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts
+++ b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts
@@ -902,54 +902,26 @@ const timer = setInterval(() => {
}
}, 180000);
-test("legacy agent git creation retains setup output in its readable chat timeline", async () => {
+test("agent creation rejects legacy worktree intent without provisioning or executing setup", async () => {
const daemon = await createTestPaseoDaemon();
const { repoDir, tempRoot } = createGitRepoWithBranch();
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
try {
- writeFileSync(
- path.join(repoDir, "paseo.json"),
- JSON.stringify({ worktree: { setup: ["echo legacy-setup-output"] } }),
- );
- execFileSync("git", ["add", "paseo.json"], { cwd: repoDir });
- execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "setup fixture"], {
- cwd: repoDir,
- stdio: "pipe",
- });
await client.connect();
- const agent = await client.createAgent({
- provider: "codex",
- cwd: repoDir,
- git: {
- createWorktree: true,
- createNewBranch: true,
- baseBranch: "main",
- newBranchName: "legacy-setup",
- worktreeSlug: "legacy-setup",
- },
- });
- await expect
- .poll(
- async () => {
- const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 });
- return timeline.entries
- .map((entry) => entry.item)
- .find(
- (item) =>
- item.type === "tool_call" &&
- item.name === "paseo_worktree_setup" &&
- item.status === "completed",
- );
+ await expect(
+ client.createAgent({
+ provider: "codex",
+ cwd: repoDir,
+ git: {
+ createWorktree: true,
+ createNewBranch: true,
+ baseBranch: "main",
+ newBranchName: "legacy-setup",
+ worktreeSlug: "legacy-setup",
},
- { timeout: 10000 },
- )
- .toMatchObject({
- detail: {
- type: "worktree_setup",
- log: expect.stringContaining("legacy-setup-output"),
- commands: [expect.objectContaining({ log: "legacy-setup-output\n" })],
- },
- });
+ }),
+ ).rejects.toThrow("createWorkspace");
+ expect((await client.fetchWorkspaces()).entries).toEqual([]);
} finally {
await client.close();
await daemon.close();
diff --git a/packages/server/src/server/worktree-bootstrap.posix.test.ts b/packages/server/src/server/worktree-bootstrap.posix.test.ts
index 41de520d09..36ee6af24f 100644
--- a/packages/server/src/server/worktree-bootstrap.posix.test.ts
+++ b/packages/server/src/server/worktree-bootstrap.posix.test.ts
@@ -13,31 +13,13 @@ import {
} from "fs";
import { join } from "path";
import { tmpdir } from "os";
-import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
-import { runAsyncWorktreeBootstrap, spawnWorkspaceScript } from "./worktree-bootstrap.js";
+import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import { ScriptRouteStore } from "./script-proxy.js";
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import { isPlatform } from "../test-utils/platform.js";
-import {
- createWorktree as createWorktreePrimitive,
- type WorktreeConfig,
-} from "../utils/worktree.js";
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
import type { TerminalSession } from "../terminal/terminal.js";
-interface CreateAgentWorktreeTestOptions {
- cwd: string;
- branchName: string;
- baseBranch: string;
- worktreeSlug: string;
- paseoHome?: string;
-}
-
-interface CreateAgentWorktreeTestResult {
- worktree: WorktreeConfig;
- shouldBootstrap: boolean;
-}
-
async function cleanupTerminalManager(terminalManager: TerminalManager): Promise {
const terminalsByCwd = await Promise.all(
terminalManager.listDirectories().map((cwd) => terminalManager.getTerminals(cwd)),
@@ -54,28 +36,10 @@ function killTerminal(terminalManager: TerminalManager, terminal: TerminalSessio
});
}
-async function createBootstrapWorktreeForTest(
- options: CreateAgentWorktreeTestOptions,
-): Promise {
- const worktree = await createWorktreePrimitive({
- cwd: options.cwd,
- worktreeSlug: options.worktreeSlug,
- source: {
- kind: "branch-off",
- baseBranch: options.baseBranch,
- branchName: options.branchName,
- },
- runSetup: false,
- paseoHome: options.paseoHome,
- });
- return { worktree, shouldBootstrap: true };
-}
-
describe.skipIf(isPlatform("win32"))("worktree-bootstrap POSIX-only", () => {
- describe("runAsyncWorktreeBootstrap", () => {
+ describe("workspace scripts", () => {
let tempDir: string;
let repoDir: string;
- let paseoHome: string;
let realTerminalManagers: TerminalManager[];
async function waitForPathExists(targetPath: string, timeoutMs = 10000): Promise {
@@ -108,7 +72,6 @@ describe.skipIf(isPlatform("win32"))("worktree-bootstrap POSIX-only", () => {
realTerminalManagers = [];
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "worktree-bootstrap-test-")));
repoDir = join(tempDir, "repo");
- paseoHome = join(tempDir, "paseo-home");
mkdirSync(repoDir, { recursive: true });
execFileSync("git", ["init", "-b", "main"], { cwd: repoDir, stdio: "pipe" });
@@ -129,290 +92,6 @@ describe.skipIf(isPlatform("win32"))("worktree-bootstrap POSIX-only", () => {
await Promise.all(realTerminalManagers.map(cleanupTerminalManager));
rmSync(tempDir, { recursive: true, force: true });
});
- it("streams running setup updates live and persists only a final setup timeline row", async () => {
- writeFileSync(
- join(repoDir, "paseo.json"),
- JSON.stringify({
- worktree: {
- setup: ['echo "line-one"; echo "line-two" 1>&2', 'echo "line-three"'],
- },
- }),
- );
- execFileSync("git", ["add", "paseo.json"], { cwd: repoDir, stdio: "pipe" });
- execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add setup"], {
- cwd: repoDir,
- stdio: "pipe",
- });
-
- const worktreeBootstrap = await createBootstrapWorktreeForTest({
- cwd: repoDir,
- branchName: "feature-streaming-setup",
- baseBranch: "main",
- worktreeSlug: "feature-streaming-setup",
- paseoHome,
- });
-
- const persisted: AgentTimelineItem[] = [];
- const live: AgentTimelineItem[] = [];
-
- await runAsyncWorktreeBootstrap({
- agentId: "agent-test",
- workspaceId: "ws-agent-test",
- worktree: worktreeBootstrap.worktree,
- shouldBootstrap: worktreeBootstrap.shouldBootstrap,
- terminalManager: null,
- appendTimelineItem: async (item) => {
- persisted.push(item);
- return true;
- },
- emitLiveTimelineItem: async (item: AgentTimelineItem) => {
- live.push(item);
- return true;
- },
- });
-
- const liveSetupItems = live.filter(
- (item) =>
- item.type === "tool_call" &&
- item.name === "paseo_worktree_setup" &&
- item.status === "running",
- );
- expect(liveSetupItems.length).toBeGreaterThan(0);
-
- const persistedSetupItems = persisted.filter(
- (item) => item.type === "tool_call" && item.name === "paseo_worktree_setup",
- );
- expect(persistedSetupItems).toHaveLength(1);
- expect(persistedSetupItems[0]?.type).toBe("tool_call");
- if (persistedSetupItems[0]?.type === "tool_call") {
- expect(persistedSetupItems[0].status).toBe("completed");
- expect(persistedSetupItems[0].detail.type).toBe("worktree_setup");
-
- if (persistedSetupItems[0].detail.type === "worktree_setup") {
- expect(persistedSetupItems[0].detail.log).toContain(
- '==> [1/2] Running: echo "line-one"; echo "line-two" 1>&2',
- );
- expect(persistedSetupItems[0].detail.log).toContain("line-one");
- expect(persistedSetupItems[0].detail.log).toContain("line-two");
- expect(persistedSetupItems[0].detail.log).toContain(
- '==> [2/2] Running: echo "line-three"',
- );
- expect(persistedSetupItems[0].detail.log).toContain("line-three");
- expect(persistedSetupItems[0].detail.log).toMatch(/<== \[1\/2\] Exit 0 in \d+\.\d{2}s/);
- expect(persistedSetupItems[0].detail.log).toMatch(/<== \[2\/2\] Exit 0 in \d+\.\d{2}s/);
-
- expect(persistedSetupItems[0].detail.commands).toHaveLength(2);
- expect(persistedSetupItems[0].detail.commands[0]).toMatchObject({
- index: 1,
- command: 'echo "line-one"; echo "line-two" 1>&2',
- log: expect.stringContaining("line-one"),
- status: "completed",
- exitCode: 0,
- });
- expect(persistedSetupItems[0].detail.commands[0]?.log).toContain("line-two");
- expect(persistedSetupItems[0].detail.commands[1]).toMatchObject({
- index: 2,
- command: 'echo "line-three"',
- log: "line-three\n",
- status: "completed",
- exitCode: 0,
- });
- expect(typeof persistedSetupItems[0].detail.commands[0]?.durationMs === "number").toBe(
- true,
- );
- expect(typeof persistedSetupItems[0].detail.commands[1]?.durationMs === "number").toBe(
- true,
- );
- }
- }
-
- const liveCallIds = new Set(
- liveSetupItems
- .filter(
- (item): item is Extract =>
- item.type === "tool_call",
- )
- .map((item) => item.callId),
- );
- expect(liveCallIds.size).toBe(1);
- if (persistedSetupItems[0]?.type === "tool_call") {
- expect(liveCallIds.has(persistedSetupItems[0].callId)).toBe(true);
- }
- });
-
- it("keeps only the final carriage-return-updated content in command logs", async () => {
- writeFileSync(
- join(repoDir, "paseo.json"),
- JSON.stringify({
- worktree: {
- setup: [
- `node -e "process.stdout.write('fetch 1/3\\\\rfetch 2/3\\\\rfetch 3/3\\\\nready\\\\n')"`,
- ],
- },
- }),
- );
- execFileSync("git", ["add", "paseo.json"], { cwd: repoDir, stdio: "pipe" });
- execFileSync(
- "git",
- ["-c", "commit.gpgsign=false", "commit", "-m", "add carriage return setup"],
- {
- cwd: repoDir,
- stdio: "pipe",
- },
- );
-
- const worktreeBootstrap = await createBootstrapWorktreeForTest({
- cwd: repoDir,
- branchName: "feature-carriage-return",
- baseBranch: "main",
- worktreeSlug: "feature-carriage-return",
- paseoHome,
- });
-
- const persisted: AgentTimelineItem[] = [];
- await runAsyncWorktreeBootstrap({
- agentId: "agent-carriage-return",
- workspaceId: "ws-carriage-return",
- worktree: worktreeBootstrap.worktree,
- shouldBootstrap: worktreeBootstrap.shouldBootstrap,
- terminalManager: null,
- appendTimelineItem: async (item) => {
- persisted.push(item);
- return true;
- },
- emitLiveTimelineItem: async () => true,
- });
-
- const persistedSetupItem = persisted.find(
- (item): item is Extract =>
- item.type === "tool_call" && item.name === "paseo_worktree_setup",
- );
- expect(persistedSetupItem?.detail.type).toBe("worktree_setup");
- if (!persistedSetupItem || persistedSetupItem.detail.type !== "worktree_setup") {
- throw new Error("Expected worktree_setup tool detail");
- }
-
- expect(persistedSetupItem.detail.log).toContain("\nfetch 3/3\nready\n");
- expect(persistedSetupItem.detail.log).not.toContain("\nfetch 1/3\n");
- expect(persistedSetupItem.detail.log).not.toContain("\nfetch 2/3\n");
- expect(persistedSetupItem.detail.commands[0]?.log).toBe("fetch 3/3\nready\n");
- });
-
- it("shares the same worktree runtime port across setup and bootstrap terminals", async () => {
- writeFileSync(
- join(repoDir, "paseo.json"),
- JSON.stringify({
- worktree: {
- setup: ['echo "$PASEO_WORKTREE_PORT" > setup-port.txt'],
- terminals: [
- {
- name: "Port Terminal",
- command: "true",
- },
- ],
- },
- }),
- );
- execFileSync("git", ["add", "paseo.json"], { cwd: repoDir, stdio: "pipe" });
- execFileSync(
- "git",
- ["-c", "commit.gpgsign=false", "commit", "-m", "add port setup and terminals"],
- {
- cwd: repoDir,
- stdio: "pipe",
- },
- );
-
- const worktreeBootstrap = await createBootstrapWorktreeForTest({
- cwd: repoDir,
- branchName: "feature-shared-runtime-port",
- baseBranch: "main",
- worktreeSlug: "feature-shared-runtime-port",
- paseoHome,
- });
-
- const registeredEnvs: Array<{ cwd: string; env: Record }> = [];
- const createTerminalEnvs: Record[] = [];
- const createTerminalWorkspaceIds: Array = [];
- const persisted: AgentTimelineItem[] = [];
- await runAsyncWorktreeBootstrap({
- agentId: "agent-shared-runtime-port",
- workspaceId: "ws-shared-runtime-port",
- worktree: worktreeBootstrap.worktree,
- shouldBootstrap: worktreeBootstrap.shouldBootstrap,
- terminalManager: {
- async getTerminals() {
- return [];
- },
- async createTerminal(options) {
- createTerminalEnvs.push(options.env ?? {});
- createTerminalWorkspaceIds.push(options.workspaceId);
- return {
- id: "term-1",
- name: options.name ?? "Terminal",
- cwd: options.cwd,
- send: () => {},
- subscribe: () => () => {},
- onExit: () => () => {},
- onCommandFinished: () => () => {},
- onTitleChange: () => () => {},
- getSize: () => ({ rows: 1, cols: 1 }),
- getTitle: () => undefined,
- getExitInfo: () => null,
- getState: () => ({
- rows: 1,
- cols: 1,
- grid: [[{ char: "$" }]],
- scrollback: [],
- cursor: { row: 0, col: 0 },
- }),
- kill: () => {},
- killAndWait: async () => {},
- };
- },
- registerCwdEnv(options) {
- registeredEnvs.push({ cwd: options.cwd, env: options.env });
- },
- getTerminal() {
- return undefined;
- },
- killTerminal() {},
- async killTerminalAndWait() {},
- listDirectories() {
- return [];
- },
- killAll() {},
- subscribeTerminalsChanged() {
- return () => {};
- },
- },
- appendTimelineItem: async (item) => {
- persisted.push(item);
- return true;
- },
- emitLiveTimelineItem: async () => true,
- });
-
- const setupPortPath = join(worktreeBootstrap.worktree.worktreePath, "setup-port.txt");
- await waitForPathExists(setupPortPath);
-
- const setupPort = readFileSync(setupPortPath, "utf8").trim();
- expect(setupPort.length).toBeGreaterThan(0);
- expect(registeredEnvs).toHaveLength(1);
- expect(registeredEnvs[0]?.cwd).toBe(worktreeBootstrap.worktree.worktreePath);
- expect(registeredEnvs[0]?.env.PASEO_WORKTREE_PORT).toBe(setupPort);
- expect(createTerminalEnvs.length).toBeGreaterThan(0);
- expect(createTerminalEnvs[0]?.PASEO_WORKTREE_PORT).toBe(setupPort);
- expect(createTerminalWorkspaceIds).toEqual(["ws-shared-runtime-port"]);
-
- const terminalToolCall = persisted.find(
- (item): item is Extract =>
- item.type === "tool_call" &&
- item.name === "paseo_worktree_terminals" &&
- item.status === "completed",
- );
- expect(terminalToolCall?.status).toBe("completed");
- });
it("injects real peer service env into terminal-backed services", async () => {
writeFileSync(
diff --git a/packages/server/src/server/worktree-bootstrap.test.ts b/packages/server/src/server/worktree-bootstrap.test.ts
index 42f48dbe22..96da6e648b 100644
--- a/packages/server/src/server/worktree-bootstrap.test.ts
+++ b/packages/server/src/server/worktree-bootstrap.test.ts
@@ -3,11 +3,9 @@ import { execFileSync } from "child_process";
import { mkdtempSync, realpathSync, rmSync, writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
-
-import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
import {
buildWorktreeSetupDetail,
- runAsyncWorktreeBootstrap,
+ runWorktreeAutoTerminals,
spawnWorkspaceScript,
} from "./worktree-bootstrap.js";
import { ensureWorkspaceServicePortPlan } from "./workspace-service-port-registry.js";
@@ -17,6 +15,7 @@ import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js
import {
createWorktree as createWorktreePrimitive,
type WorktreeConfig,
+ resolveWorktreeRuntimeEnv,
} from "../utils/worktree.js";
import type { TerminalManager } from "../terminal/terminal-manager.js";
import type { TerminalSession } from "../terminal/terminal.js";
@@ -61,7 +60,7 @@ async function createBootstrapWorktreeForTest(
baseBranch: options.baseBranch,
branchName: options.branchName,
},
- runSetup: false,
+
paseoHome: options.paseoHome,
});
return { worktree, shouldBootstrap: true };
@@ -89,9 +88,30 @@ describe("workspace setup detail", () => {
});
expect(serialized.split("ready\\n")).toHaveLength(2);
});
+ it("retains bounded per-command output and truncation without an aggregate copy", () => {
+ const detail = buildWorktreeSetupDetail({
+ worktree: { worktreePath: "/workspace", branchName: "feature" },
+ results: [
+ {
+ command: "large-output",
+ cwd: "/workspace",
+ stdout: "prefix-" + "x".repeat(100000) + "-suffix",
+ stderr: "",
+ exitCode: 0,
+ durationMs: 10,
+ },
+ ],
+ });
+ expect(detail.log).toBe("");
+ expect(detail.truncated).toBe(true);
+ expect(detail.commands[0]?.log).toContain("prefix-");
+ expect(detail.commands[0]?.log).toContain("-suffix");
+ expect(detail.commands[0]?.log).toContain("...