diff --git a/packages/core/src/orchestrator/orchestrator-agent.test.ts b/packages/core/src/orchestrator/orchestrator-agent.test.ts index ab8165ca7e..ed9fc9ef4a 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.test.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.test.ts @@ -902,10 +902,12 @@ describe('discoverAllWorkflows — remote sync', () => { mockSendQuery.mockClear(); mockGetCodebaseEnvVars.mockReset(); mockLoadConfig.mockReset(); + mockListCodebases.mockReset(); // Reset mocks between tests in this suite and restore safe defaults mockGetOrCreateConversation.mockImplementation(() => Promise.resolve(null)); mockGetCodebase.mockImplementation(() => Promise.resolve(null)); mockGetCodebaseEnvVars.mockImplementation(() => Promise.resolve({})); + mockListCodebases.mockImplementation(() => Promise.resolve([])); mockLoadConfig.mockImplementation(() => Promise.resolve({ assistants: { claude: {}, codex: {} }, @@ -1043,6 +1045,61 @@ describe('discoverAllWorkflows — remote sync', () => { const requestOptions = mockSendQuery.mock.calls[0][3] as Record; expect(requestOptions.env).toEqual({ FILE_SECRET: 'file-value' }); }); + + describe('provider cwd resolution', () => { + function getCwdPassedToProvider(): string { + expect(mockSendQuery).toHaveBeenCalled(); + return mockSendQuery.mock.calls[0][1] as string; + } + + test('passes codebase.default_cwd to provider when conversation is codebase-scoped', async () => { + const codebase = makeCodebaseForSync(); + const conversation = makeConversation({ codebase_id: 'codebase-1', cwd: null }); + mockGetOrCreateConversation.mockReturnValueOnce(Promise.resolve(conversation)); + mockGetCodebase.mockReturnValueOnce(Promise.resolve(codebase)); + mockListCodebases.mockReturnValueOnce(Promise.resolve([codebase])); + + await handleMessage(makePlatform(), 'conv-1', 'What files are in src/?'); + + expect(getCwdPassedToProvider()).toBe('/repos/test-repo'); + }); + + test('prefers conversation.cwd over codebase.default_cwd when set (worktree)', async () => { + const codebase = makeCodebaseForSync(); + const conversation = makeConversation({ + codebase_id: 'codebase-1', + cwd: '/home/test/.archon/workspaces/owner/repo/worktrees/feature-branch', + }); + mockGetOrCreateConversation.mockReturnValueOnce(Promise.resolve(conversation)); + mockGetCodebase.mockReturnValueOnce(Promise.resolve(codebase)); + mockListCodebases.mockReturnValueOnce(Promise.resolve([codebase])); + + await handleMessage(makePlatform(), 'conv-1', 'What files are in src/?'); + + expect(getCwdPassedToProvider()).toBe( + '/home/test/.archon/workspaces/owner/repo/worktrees/feature-branch' + ); + }); + + test('falls back to getArchonWorkspacesPath when conversation has no codebase', async () => { + const conversation = makeConversation({ codebase_id: null, cwd: null }); + mockGetOrCreateConversation.mockReturnValueOnce(Promise.resolve(conversation)); + + await handleMessage(makePlatform(), 'conv-1', 'Hello'); + + expect(getCwdPassedToProvider()).toBe('/home/test/.archon/workspaces'); + }); + + test('falls back to getArchonWorkspacesPath when codebase_id is set but codebase not in list', async () => { + const conversation = makeConversation({ codebase_id: 'codebase-1', cwd: null }); + mockGetOrCreateConversation.mockReturnValueOnce(Promise.resolve(conversation)); + mockListCodebases.mockReturnValueOnce(Promise.resolve([])); + + await handleMessage(makePlatform(), 'conv-1', 'Hello'); + + expect(getCwdPassedToProvider()).toBe('/home/test/.archon/workspaces'); + }); + }); }); // ─── Workflow dispatch routing — interactive flag ───────────────────────────── diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index ba24331b69..9f1b6dd558 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -809,7 +809,22 @@ export async function handleMessage( attachedFiles, workflowContext ); - const cwd = getArchonWorkspacesPath(); + // For codebase-scoped chat, use the worktree path (conversation.cwd) if set, + // otherwise the codebase's default working directory. + // Non-scoped chat falls back to the Archon workspaces root. + let cwd = getArchonWorkspacesPath(); + if (conversation.codebase_id) { + const attachedCodebase = codebases.find(c => c.id === conversation.codebase_id); + if (attachedCodebase) { + cwd = conversation.cwd ?? attachedCodebase.default_cwd; + } else { + // Intentional fallback: codebase may have been deleted; run with workspaces root. + getLog().warn( + { codebaseId: conversation.codebase_id, conversationId }, + 'orchestrator.codebase_not_found_cwd_fallback' + ); + } + } // 4. Update activity and get/create session await db.touchConversation(conversation.id); diff --git a/packages/core/src/services/cleanup-service.test.ts b/packages/core/src/services/cleanup-service.test.ts index 308a13c80d..000ef8757f 100644 --- a/packages/core/src/services/cleanup-service.test.ts +++ b/packages/core/src/services/cleanup-service.test.ts @@ -678,7 +678,7 @@ describe('runScheduledCleanup', () => { expect(report.removed).toHaveLength(0); }); - test('continues processing after error on one environment', async () => { + test('processes all environments in batch (both paths missing)', async () => { mockListAllActiveWithCodebase.mockResolvedValueOnce([ { id: 'env-error',