diff --git a/src/agent/cleanup-manager.ts b/src/agent/cleanup-manager.ts index c37433c..cb8b1ff 100644 --- a/src/agent/cleanup-manager.ts +++ b/src/agent/cleanup-manager.ts @@ -43,6 +43,19 @@ export class CleanupManager { return join(this.workspaceRoot, 'agent-workdirs', alias); } + /** + * Resolve the actual working directory for an agent, probing for the + * workspace/ subdirectory used by standalone agents. + */ + private resolveAgentCwd(worktreeId: string): string { + const base = this.getAgentWorkdir(worktreeId); + const workspaceSub = join(base, 'workspace'); + if (!existsSync(join(base, '.cw', 'output')) && existsSync(join(workspaceSub, '.cw'))) { + return workspaceSub; + } + return base; + } + /** * Remove git worktrees for an agent. * Handles both initiative-linked (multi-project) and standalone agents. @@ -341,6 +354,7 @@ export class CleanupManager { pid: number; tailer: FileTailer; outputFilePath: string; + agentCwd?: string; }>, onStreamEvent: (agentId: string, event: StreamEvent) => void, onAgentOutput: (agentId: string, rawOutput: string, provider: NonNullable>) => Promise, @@ -375,17 +389,22 @@ export class CleanupManager { const pid = agent.pid!; + // Resolve actual agent cwd — standalone agents run in workspace/ subdir + const resolvedCwd = this.resolveAgentCwd(agent.worktreeId); + activeAgents.set(agent.id, { agentId: agent.id, pid, tailer, outputFilePath: agent.outputFilePath, + agentCwd: resolvedCwd, }); pollForCompletion(agent.id, pid); } else if (agent.outputFilePath) { // CRITICAL FIX: Check for signal.json completion FIRST before parsing raw output - const agentWorkdir = this.getAgentWorkdir(agent.worktreeId); + // Resolve actual agent cwd — standalone agents run in workspace/ subdir + const agentWorkdir = this.resolveAgentCwd(agent.worktreeId); const hasValidSignal = this.signalManager ? await this.signalManager.readSignal(agentWorkdir) : null; if (hasValidSignal) { diff --git a/src/agent/manager.ts b/src/agent/manager.ts index 802914e..3043f9d 100644 --- a/src/agent/manager.ts +++ b/src/agent/manager.ts @@ -39,7 +39,7 @@ import { writeInputFiles } from './file-io.js'; import { buildWorkspaceLayout, buildInterAgentCommunication } from './prompts/index.js'; import { getProvider } from './providers/registry.js'; import { createModuleLogger } from '../logger/index.js'; -import { join, dirname } from 'node:path'; +import { join } from 'node:path'; import { unlink, readFile } from 'node:fs/promises'; import { existsSync, writeFileSync } from 'node:fs'; import type { AccountCredentialManager } from './credentials/types.js'; @@ -340,7 +340,7 @@ export class MultiProviderAgentManager implements AgentManager { 'utf-8' ); - const activeEntry: ActiveAgent = { agentId, pid, tailer, outputFilePath }; + const activeEntry: ActiveAgent = { agentId, pid, tailer, outputFilePath, agentCwd: finalCwd }; this.activeAgents.set(agentId, activeEntry); log.info({ agentId, alias, pid, diagnosticWritten: true }, 'detached subprocess started with diagnostic'); @@ -820,7 +820,7 @@ export class MultiProviderAgentManager implements AgentManager { // Check if the agent has output that indicates successful completion if (agent.outputFilePath) { - const hasCompletion = await this.checkAgentCompletionResult(agent.outputFilePath); + const hasCompletion = await this.checkAgentCompletionResult(agent.worktreeId); if (hasCompletion) { log.info({ agentId: processId, @@ -872,14 +872,21 @@ export class MultiProviderAgentManager implements AgentManager { /** * Check if agent completed successfully by reading signal.json file. + * Probes the workspace/ subdirectory for standalone agents. */ - private async checkAgentCompletionResult(outputFilePath: string): Promise { + private async checkAgentCompletionResult(worktreeId: string): Promise { try { - const agentDir = dirname(outputFilePath); - const signalPath = join(agentDir, '.cw/output/signal.json'); + // Resolve actual agent workdir — standalone agents have .cw inside workspace/ subdir + let agentWorkdir = this.processManager.getAgentWorkdir(worktreeId); + const workspaceSub = join(agentWorkdir, 'workspace'); + if (!existsSync(join(agentWorkdir, '.cw', 'output')) && existsSync(join(workspaceSub, '.cw'))) { + agentWorkdir = workspaceSub; + } + + const signalPath = join(agentWorkdir, '.cw/output/signal.json'); if (!existsSync(signalPath)) { - log.debug({ outputFilePath, signalPath }, 'no signal.json found - agent not completed'); + log.debug({ worktreeId, signalPath }, 'no signal.json found - agent not completed'); return false; } @@ -890,15 +897,15 @@ export class MultiProviderAgentManager implements AgentManager { const completed = signal.status === 'done' || signal.status === 'questions' || signal.status === 'error'; if (completed) { - log.debug({ outputFilePath, signal }, 'agent completion detected via signal.json'); + log.debug({ worktreeId, signal }, 'agent completion detected via signal.json'); } else { - log.debug({ outputFilePath, signal }, 'signal.json found but status indicates incomplete'); + log.debug({ worktreeId, signal }, 'signal.json found but status indicates incomplete'); } return completed; } catch (err) { - log.warn({ outputFilePath, err: err instanceof Error ? err.message : String(err) }, 'failed to read or parse signal.json'); + log.warn({ worktreeId, err: err instanceof Error ? err.message : String(err) }, 'failed to read or parse signal.json'); return false; } } diff --git a/src/agent/output-handler.ts b/src/agent/output-handler.ts index 9f49aa7..d99b81e 100644 --- a/src/agent/output-handler.ts +++ b/src/agent/output-handler.ts @@ -52,6 +52,8 @@ export interface ActiveAgent { pid: number; tailer: import('./file-tailer.js').FileTailer; outputFilePath: string; + /** Actual working directory the agent process runs in (may differ from getAgentWorkdir for standalone agents) */ + agentCwd?: string; result?: AgentResult; pendingQuestions?: PendingQuestions; streamResultText?: string; @@ -226,8 +228,10 @@ export class OutputHandler { log.debug({ agentId }, 'detached agent completed'); - // Verify agent worked in correct location by checking for output files - const agentWorkdir = getAgentWorkdir(agent.worktreeId); + // Resolve actual agent working directory — standalone agents run in a + // "workspace/" subdirectory inside getAgentWorkdir, so prefer agentCwd + // recorded at spawn time when available. + const agentWorkdir = active?.agentCwd ?? getAgentWorkdir(agent.worktreeId); const outputDir = join(agentWorkdir, '.cw', 'output'); const expectedPwdFile = join(agentWorkdir, '.cw', 'expected-pwd.txt'); const diagnosticFile = join(agentWorkdir, '.cw', 'spawn-diagnostic.json'); @@ -267,7 +271,6 @@ export class OutputHandler { const outputFilePath = active?.outputFilePath ?? ''; if (outputFilePath) { // First, check for robust signal.json completion before attempting incremental reading - const agentWorkdir = getAgentWorkdir(agent.worktreeId); // FIX: Use worktreeId, not agentId! log.debug({ agentId, worktreeId: agent.worktreeId, agentWorkdir }, 'checking signal completion'); const hasSignalCompletion = await this.readSignalCompletion(agentWorkdir); @@ -311,9 +314,8 @@ export class OutputHandler { } // Check for signal.json file first, then fall back to stream text - const completionWorkdir = getAgentWorkdir(agent.worktreeId); - if (await this.readSignalCompletion(completionWorkdir)) { - const signalPath = join(completionWorkdir, '.cw/output/signal.json'); + if (await this.readSignalCompletion(agentWorkdir)) { + const signalPath = join(agentWorkdir, '.cw/output/signal.json'); const signalContent = await readFile(signalPath, 'utf-8'); log.debug({ agentId, signalPath }, 'using signal.json content for completion'); await this.processSignalAndFiles(agentId, signalContent, agent.mode as AgentMode, getAgentWorkdir, active?.streamSessionId);