From e7c95af1ca29c4c2c4e6c631f6844dc7d9f0f831 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Fri, 6 Mar 2026 22:39:56 +0100 Subject: [PATCH] fix: skip standalone worktree when errand provides cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When spawn() receives an explicit cwd (errands), the manager was still creating a standalone worktree at agent-workdirs// and injecting its path into the workspace layout prompt. The agent then edited files in the wrong directory — on a different branch than the errand's. Now when cwd is provided, we skip worktree creation entirely and use the caller's cwd for workspace layout, .cw/output/, and all paths. --- apps/server/agent/manager.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/server/agent/manager.ts b/apps/server/agent/manager.ts index 3e022bc..551f7eb 100644 --- a/apps/server/agent/manager.ts +++ b/apps/server/agent/manager.ts @@ -233,9 +233,13 @@ export class MultiProviderAgentManager implements AgentManager { log.debug('no accounts available, spawning without account'); } - // 2. Create isolated worktrees + // 2. Create isolated worktrees (skip when caller provides explicit cwd, e.g. errands) let agentCwd: string; - if (initiativeId) { + if (cwd) { + // Caller manages the worktree (errands). Use their cwd directly. + agentCwd = cwd; + log.info({ alias, agentCwd }, 'using caller-provided cwd, skipping worktree creation'); + } else if (initiativeId) { log.debug({ alias, initiativeId, baseBranch, branchName }, 'creating initiative-based worktrees'); agentCwd = await this.processManager.createProjectWorktrees(alias, initiativeId, baseBranch, branchName); @@ -318,15 +322,13 @@ export class MultiProviderAgentManager implements AgentManager { // 4. Build spawn command const { command, args, env: providerEnv } = this.processManager.buildSpawnCommand(provider, prompt); - const finalCwd = cwd ?? agentCwd; log.info({ agentId, alias, command, args: args.join(' '), - finalCwd, - customCwdProvided: !!cwd, + agentCwd, providerEnv: Object.keys(providerEnv) }, 'spawn command built'); @@ -342,7 +344,7 @@ export class MultiProviderAgentManager implements AgentManager { // 6. Spawn detached subprocess const { pid, outputFilePath, tailer } = await this.processManager.spawnDetached( - agentId, alias, command, args, cwd ?? agentCwd, processEnv, providerName, prompt, + agentId, alias, command, args, agentCwd, processEnv, providerName, prompt, (event) => this.outputHandler.handleStreamEvent(agentId, event, this.activeAgents.get(agentId)), this.createLogChunkCallback(agentId, alias, 1), ); @@ -351,7 +353,7 @@ export class MultiProviderAgentManager implements AgentManager { // Register agent and start polling BEFORE non-critical I/O so that a // diagnostic-write failure can never orphan a running process. - const activeEntry: ActiveAgent = { agentId, pid, tailer, outputFilePath, agentCwd: finalCwd }; + const activeEntry: ActiveAgent = { agentId, pid, tailer, outputFilePath, agentCwd }; this.activeAgents.set(agentId, activeEntry); // Emit spawned event @@ -376,19 +378,19 @@ export class MultiProviderAgentManager implements AgentManager { // Write spawn diagnostic file (non-fatal — .cw/ may not exist yet for // agents spawned without inputContext, e.g. conflict-resolution agents) try { - const diagnosticDir = join(finalCwd, '.cw'); + const diagnosticDir = join(agentCwd, '.cw'); await mkdir(diagnosticDir, { recursive: true }); const diagnostic = { timestamp: new Date().toISOString(), agentId, alias, - intendedCwd: finalCwd, + intendedCwd: agentCwd, worktreeId: agent.worktreeId, provider: providerName, command, args, env: processEnv, - cwdExistsAtSpawn: existsSync(finalCwd), + cwdExistsAtSpawn: existsSync(agentCwd), initiativeId: initiativeId || null, customCwdProvided: !!cwd, accountId: accountId || null,