From ff14ee9ce5722a5bc5d5338f9cdff9c0ba2df2b8 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Wed, 15 Apr 2026 10:24:45 +0300 Subject: [PATCH 1/3] fix(orchestrator): use codebase cwd for direct chat provider execution (#1179) Direct chat with a codebase-scoped conversation was always passing the Archon workspaces root as cwd to the provider, ignoring the attached project. Now resolves cwd from conversation.cwd (worktree) or codebase.default_cwd, matching workflow execution behavior. Changes: - Resolve cwd from attached codebase in orchestrator-agent.ts - Add 3 tests covering codebase-scoped, worktree, and fallback paths Fixes #1179 --- .../orchestrator/orchestrator-agent.test.ts | 47 +++++++++++++++++++ .../src/orchestrator/orchestrator-agent.ts | 11 ++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.test.ts b/packages/core/src/orchestrator/orchestrator-agent.test.ts index ab8165ca7e..4d2163a0e7 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.test.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.test.ts @@ -1043,6 +1043,53 @@ 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', () => { + 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])); + + const platform = makePlatform(); + await handleMessage(platform, 'conv-1', 'What files are in src/?'); + + expect(mockSendQuery).toHaveBeenCalled(); + const calledCwd = mockSendQuery.mock.calls[0][1] as string; + expect(calledCwd).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])); + + const platform = makePlatform(); + await handleMessage(platform, 'conv-1', 'What files are in src/?'); + + expect(mockSendQuery).toHaveBeenCalled(); + const calledCwd = mockSendQuery.mock.calls[0][1] as string; + expect(calledCwd).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)); + + const platform = makePlatform(); + await handleMessage(platform, 'conv-1', 'Hello'); + + expect(mockSendQuery).toHaveBeenCalled(); + const calledCwd = mockSendQuery.mock.calls[0][1] as string; + expect(calledCwd).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..6911b04d6f 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -809,7 +809,16 @@ 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; + } + } // 4. Update activity and get/create session await db.touchConversation(conversation.id); From ab4fce405e5d97e9a7a9c1f35aec1ee6c51ddee1 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Wed, 15 Apr 2026 10:39:10 +0300 Subject: [PATCH 2/3] fix(orchestrator): warn when codebase_id set but codebase not found in cwd resolution - Add warn log when codebase lookup fails in the cwd fallback path, matching the pattern used for codebase_env_vars_load_failed one block below - Add test covering the codebase_id-set-but-not-found fallback branch - Reset mockListCodebases in beforeEach to prevent potential mock leaks - Rename misleading cleanup-service test that no longer injects errors --- .../src/orchestrator/orchestrator-agent.test.ts | 15 +++++++++++++++ .../core/src/orchestrator/orchestrator-agent.ts | 6 ++++++ .../core/src/services/cleanup-service.test.ts | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.test.ts b/packages/core/src/orchestrator/orchestrator-agent.test.ts index 4d2163a0e7..1f730a2f2f 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: {} }, @@ -1089,6 +1091,19 @@ describe('discoverAllWorkflows — remote sync', () => { const calledCwd = mockSendQuery.mock.calls[0][1] as string; expect(calledCwd).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([])); + + const platform = makePlatform(); + await handleMessage(platform, 'conv-1', 'Hello'); + + expect(mockSendQuery).toHaveBeenCalled(); + const calledCwd = mockSendQuery.mock.calls[0][1] as string; + expect(calledCwd).toBe('/home/test/.archon/workspaces'); + }); }); }); diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 6911b04d6f..9f1b6dd558 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -817,6 +817,12 @@ export async function handleMessage( 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' + ); } } 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', From d2b3ba3dc33261f2be904fe51ee7bb7151d8e40f Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Wed, 15 Apr 2026 10:41:35 +0300 Subject: [PATCH 3/3] simplify: extract cwd assertion helper in provider cwd resolution tests --- .../orchestrator/orchestrator-agent.test.ts | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.test.ts b/packages/core/src/orchestrator/orchestrator-agent.test.ts index 1f730a2f2f..ed9fc9ef4a 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.test.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.test.ts @@ -1047,6 +1047,11 @@ describe('discoverAllWorkflows — remote sync', () => { }); 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 }); @@ -1054,12 +1059,9 @@ describe('discoverAllWorkflows — remote sync', () => { mockGetCodebase.mockReturnValueOnce(Promise.resolve(codebase)); mockListCodebases.mockReturnValueOnce(Promise.resolve([codebase])); - const platform = makePlatform(); - await handleMessage(platform, 'conv-1', 'What files are in src/?'); + await handleMessage(makePlatform(), 'conv-1', 'What files are in src/?'); - expect(mockSendQuery).toHaveBeenCalled(); - const calledCwd = mockSendQuery.mock.calls[0][1] as string; - expect(calledCwd).toBe('/repos/test-repo'); + expect(getCwdPassedToProvider()).toBe('/repos/test-repo'); }); test('prefers conversation.cwd over codebase.default_cwd when set (worktree)', async () => { @@ -1072,24 +1074,20 @@ describe('discoverAllWorkflows — remote sync', () => { mockGetCodebase.mockReturnValueOnce(Promise.resolve(codebase)); mockListCodebases.mockReturnValueOnce(Promise.resolve([codebase])); - const platform = makePlatform(); - await handleMessage(platform, 'conv-1', 'What files are in src/?'); + await handleMessage(makePlatform(), 'conv-1', 'What files are in src/?'); - expect(mockSendQuery).toHaveBeenCalled(); - const calledCwd = mockSendQuery.mock.calls[0][1] as string; - expect(calledCwd).toBe('/home/test/.archon/workspaces/owner/repo/worktrees/feature-branch'); + 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)); - const platform = makePlatform(); - await handleMessage(platform, 'conv-1', 'Hello'); + await handleMessage(makePlatform(), 'conv-1', 'Hello'); - expect(mockSendQuery).toHaveBeenCalled(); - const calledCwd = mockSendQuery.mock.calls[0][1] as string; - expect(calledCwd).toBe('/home/test/.archon/workspaces'); + expect(getCwdPassedToProvider()).toBe('/home/test/.archon/workspaces'); }); test('falls back to getArchonWorkspacesPath when codebase_id is set but codebase not in list', async () => { @@ -1097,12 +1095,9 @@ describe('discoverAllWorkflows — remote sync', () => { mockGetOrCreateConversation.mockReturnValueOnce(Promise.resolve(conversation)); mockListCodebases.mockReturnValueOnce(Promise.resolve([])); - const platform = makePlatform(); - await handleMessage(platform, 'conv-1', 'Hello'); + await handleMessage(makePlatform(), 'conv-1', 'Hello'); - expect(mockSendQuery).toHaveBeenCalled(); - const calledCwd = mockSendQuery.mock.calls[0][1] as string; - expect(calledCwd).toBe('/home/test/.archon/workspaces'); + expect(getCwdPassedToProvider()).toBe('/home/test/.archon/workspaces'); }); }); });