fix: skip standalone worktree when errand provides cwd

When spawn() receives an explicit cwd (errands), the manager was still
creating a standalone worktree at agent-workdirs/<alias>/ 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.
This commit is contained in:
Lukas May
2026-03-06 22:39:56 +01:00
parent 79a0bd0a74
commit e7c95af1ca

View File

@@ -233,9 +233,13 @@ export class MultiProviderAgentManager implements AgentManager {
log.debug('no accounts available, spawning without account'); 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; 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'); log.debug({ alias, initiativeId, baseBranch, branchName }, 'creating initiative-based worktrees');
agentCwd = await this.processManager.createProjectWorktrees(alias, initiativeId, baseBranch, branchName); agentCwd = await this.processManager.createProjectWorktrees(alias, initiativeId, baseBranch, branchName);
@@ -318,15 +322,13 @@ export class MultiProviderAgentManager implements AgentManager {
// 4. Build spawn command // 4. Build spawn command
const { command, args, env: providerEnv } = this.processManager.buildSpawnCommand(provider, prompt); const { command, args, env: providerEnv } = this.processManager.buildSpawnCommand(provider, prompt);
const finalCwd = cwd ?? agentCwd;
log.info({ log.info({
agentId, agentId,
alias, alias,
command, command,
args: args.join(' '), args: args.join(' '),
finalCwd, agentCwd,
customCwdProvided: !!cwd,
providerEnv: Object.keys(providerEnv) providerEnv: Object.keys(providerEnv)
}, 'spawn command built'); }, 'spawn command built');
@@ -342,7 +344,7 @@ export class MultiProviderAgentManager implements AgentManager {
// 6. Spawn detached subprocess // 6. Spawn detached subprocess
const { pid, outputFilePath, tailer } = await this.processManager.spawnDetached( 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)), (event) => this.outputHandler.handleStreamEvent(agentId, event, this.activeAgents.get(agentId)),
this.createLogChunkCallback(agentId, alias, 1), 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 // Register agent and start polling BEFORE non-critical I/O so that a
// diagnostic-write failure can never orphan a running process. // 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); this.activeAgents.set(agentId, activeEntry);
// Emit spawned event // Emit spawned event
@@ -376,19 +378,19 @@ export class MultiProviderAgentManager implements AgentManager {
// Write spawn diagnostic file (non-fatal — .cw/ may not exist yet for // Write spawn diagnostic file (non-fatal — .cw/ may not exist yet for
// agents spawned without inputContext, e.g. conflict-resolution agents) // agents spawned without inputContext, e.g. conflict-resolution agents)
try { try {
const diagnosticDir = join(finalCwd, '.cw'); const diagnosticDir = join(agentCwd, '.cw');
await mkdir(diagnosticDir, { recursive: true }); await mkdir(diagnosticDir, { recursive: true });
const diagnostic = { const diagnostic = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
agentId, agentId,
alias, alias,
intendedCwd: finalCwd, intendedCwd: agentCwd,
worktreeId: agent.worktreeId, worktreeId: agent.worktreeId,
provider: providerName, provider: providerName,
command, command,
args, args,
env: processEnv, env: processEnv,
cwdExistsAtSpawn: existsSync(finalCwd), cwdExistsAtSpawn: existsSync(agentCwd),
initiativeId: initiativeId || null, initiativeId: initiativeId || null,
customCwdProvided: !!cwd, customCwdProvided: !!cwd,
accountId: accountId || null, accountId: accountId || null,