From 018a96ac07efe44bcc7ceaed496c3403e2398567 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:38:02 +0000 Subject: [PATCH] Fix AGENTS.md forwarding: read from projectPath not workspacePath System message incorrectly read AGENTS.md from workspacePath (the worktree directory) instead of projectPath (the project root). This broke AGENTS.md forwarding for all workspaces. Root cause: Recent runtime changes treated workspace and project paths identically, but instruction files should come from the project root while the environment context uses the workspace path. Changes: - buildSystemMessage() now reads AGENTS.md from metadata.projectPath - Environment context still uses workspacePath (where code executes) - Updated all tests to reflect project vs workspace distinction - Renamed test variables for clarity (workspace -> project) Fixes instruction file loading for both LocalRuntime (worktrees) and SSHRuntime (clones). --- src/services/systemMessage.test.ts | 49 ++++++++++++++++-------------- src/services/systemMessage.ts | 28 +++++++++-------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/services/systemMessage.test.ts b/src/services/systemMessage.test.ts index 40e50589cc..c5854fc5b2 100644 --- a/src/services/systemMessage.test.ts +++ b/src/services/systemMessage.test.ts @@ -8,6 +8,7 @@ import type { Mock } from "bun:test"; describe("buildSystemMessage", () => { let tempDir: string; + let projectDir: string; let workspaceDir: string; let globalDir: string; let mockHomedir: Mock; @@ -15,8 +16,10 @@ describe("buildSystemMessage", () => { beforeEach(async () => { // Create temp directory for test tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "systemMessage-test-")); + projectDir = path.join(tempDir, "project"); workspaceDir = path.join(tempDir, "workspace"); globalDir = path.join(tempDir, ".cmux"); + await fs.mkdir(projectDir, { recursive: true }); await fs.mkdir(workspaceDir, { recursive: true }); await fs.mkdir(globalDir, { recursive: true }); @@ -33,9 +36,9 @@ describe("buildSystemMessage", () => { }); test("includes mode-specific section when mode is provided", async () => { - // Write instruction file with mode section + // Write instruction file with mode section to projectDir await fs.writeFile( - path.join(workspaceDir, "AGENTS.md"), + path.join(projectDir, "AGENTS.md"), `# General Instructions Always be helpful. @@ -49,7 +52,7 @@ Use diagrams where appropriate. id: "test-workspace", name: "test-workspace", projectName: "test-project", - projectPath: tempDir, + projectPath: projectDir, }; const systemMessage = await buildSystemMessage(metadata, workspaceDir, "plan"); @@ -65,9 +68,9 @@ Use diagrams where appropriate. }); test("excludes mode-specific section when mode is not provided", async () => { - // Write instruction file with mode section + // Write instruction file with mode section to projectDir await fs.writeFile( - path.join(workspaceDir, "AGENTS.md"), + path.join(projectDir, "AGENTS.md"), `# General Instructions Always be helpful. @@ -80,7 +83,7 @@ Focus on planning and design. id: "test-workspace", name: "test-workspace", projectName: "test-project", - projectPath: tempDir, + projectPath: projectDir, }; const systemMessage = await buildSystemMessage(metadata, workspaceDir); @@ -94,7 +97,7 @@ Focus on planning and design. expect(systemMessage).toContain("Focus on planning and design"); }); - test("prefers workspace mode section over global mode section", async () => { + test("prefers project mode section over global mode section", async () => { // Write global instruction file with mode section await fs.writeFile( path.join(globalDir, "AGENTS.md"), @@ -105,13 +108,13 @@ Global plan instructions. ` ); - // Write workspace instruction file with mode section + // Write project instruction file with mode section await fs.writeFile( - path.join(workspaceDir, "AGENTS.md"), - `# Workspace Instructions + path.join(projectDir, "AGENTS.md"), + `# Project Instructions ## Mode: Plan -Workspace plan instructions (should win). +Project plan instructions (should win). ` ); @@ -119,19 +122,19 @@ Workspace plan instructions (should win). id: "test-workspace", name: "test-workspace", projectName: "test-project", - projectPath: tempDir, + projectPath: projectDir, }; const systemMessage = await buildSystemMessage(metadata, workspaceDir, "plan"); - // Should include workspace mode section in the tag (workspace wins) - expect(systemMessage).toMatch(/\s*Workspace plan instructions \(should win\)\./s); + // Should include project mode section in the tag (project wins) + expect(systemMessage).toMatch(/\s*Project plan instructions \(should win\)\./s); // Global instructions are still present in section (that's correct) - // But the mode-specific section should only have workspace content + // But the mode-specific section should only have project content expect(systemMessage).not.toMatch(/[^<]*Global plan instructions/s); }); - test("falls back to global mode section when workspace has none", async () => { + test("falls back to global mode section when project has none", async () => { // Write global instruction file with mode section await fs.writeFile( path.join(globalDir, "AGENTS.md"), @@ -142,11 +145,11 @@ Global plan instructions. ` ); - // Write workspace instruction file WITHOUT mode section + // Write project instruction file WITHOUT mode section await fs.writeFile( - path.join(workspaceDir, "AGENTS.md"), - `# Workspace Instructions -Just general workspace stuff. + path.join(projectDir, "AGENTS.md"), + `# Project Instructions +Just general project stuff. ` ); @@ -154,7 +157,7 @@ Just general workspace stuff. id: "test-workspace", name: "test-workspace", projectName: "test-project", - projectPath: tempDir, + projectPath: projectDir, }; const systemMessage = await buildSystemMessage(metadata, workspaceDir, "plan"); @@ -165,7 +168,7 @@ Just general workspace stuff. test("handles mode with special characters by sanitizing tag name", async () => { await fs.writeFile( - path.join(workspaceDir, "AGENTS.md"), + path.join(projectDir, "AGENTS.md"), `## Mode: My-Special_Mode! Special mode instructions. ` @@ -175,7 +178,7 @@ Special mode instructions. id: "test-workspace", name: "test-workspace", projectName: "test-project", - projectPath: tempDir, + projectPath: projectDir, }; const systemMessage = await buildSystemMessage(metadata, workspaceDir, "My-Special_Mode!"); diff --git a/src/services/systemMessage.ts b/src/services/systemMessage.ts index a1cff1c9bf..5cc494fe15 100644 --- a/src/services/systemMessage.ts +++ b/src/services/systemMessage.ts @@ -55,10 +55,10 @@ function getSystemDirectory(): string { * * Instruction sources are layered in this order: * 1. Global instructions: ~/.cmux/AGENTS.md (+ AGENTS.local.md) - * 2. Workspace instructions: /AGENTS.md (+ AGENTS.local.md) + * 2. Project instructions: /AGENTS.md (+ AGENTS.local.md) * 3. Mode-specific context (if mode provided): Extract a section titled "Mode: " * (case-insensitive) from the instruction file. We search at most one section in - * precedence order: workspace instructions first, then global instructions. + * precedence order: project instructions first, then global instructions. * * Each instruction file location is searched for in priority order: * - AGENTS.md @@ -68,8 +68,8 @@ function getSystemDirectory(): string { * If a base instruction file is found, its corresponding .local.md variant is also * checked and appended when building the instruction set (useful for personal preferences not committed to git). * - * @param metadata - Workspace metadata - * @param workspacePath - Absolute path to the workspace worktree directory + * @param metadata - Workspace metadata (contains projectPath for reading AGENTS.md) + * @param workspacePath - Absolute path to the workspace directory (for environment context) * @param mode - Optional mode name (e.g., "plan", "exec") - looks for {MODE}.md files if provided * @param additionalSystemInstructions - Optional additional system instructions to append at the end * @returns System message string with all instruction sources combined @@ -90,21 +90,22 @@ export async function buildSystemMessage( } const systemDir = getSystemDirectory(); - const workspaceDir = workspacePath; + const projectDir = metadata.projectPath; - // Gather instruction sets from both global and workspace directories - // Global instructions apply first, then workspace-specific ones - const instructionDirectories = [systemDir, workspaceDir]; + // Gather instruction sets from both global and project directories + // Global instructions apply first, then project-specific ones + // Note: We read from projectPath (the main repo) not workspacePath (the worktree) + const instructionDirectories = [systemDir, projectDir]; const instructionSegments = await gatherInstructionSets(instructionDirectories); const customInstructions = instructionSegments.join("\n\n"); - // Look for a "Mode: " section inside instruction sets, preferring workspace over global + // Look for a "Mode: " section inside instruction sets, preferring project over global // This behavior is documented in docs/instruction-files.md - keep both in sync when changing. let modeContent: string | null = null; if (mode) { - const workspaceInstructions = await readInstructionSet(workspaceDir); - if (workspaceInstructions) { - modeContent = extractModeSection(workspaceInstructions, mode); + const projectInstructions = await readInstructionSet(projectDir); + if (projectInstructions) { + modeContent = extractModeSection(projectInstructions, mode); } if (!modeContent) { const globalInstructions = await readInstructionSet(systemDir); @@ -115,7 +116,8 @@ export async function buildSystemMessage( } // Build the final system message - const environmentContext = buildEnvironmentContext(workspaceDir); + // Use workspacePath for environment context (where code actually executes) + const environmentContext = buildEnvironmentContext(workspacePath); const trimmedPrelude = PRELUDE.trim(); let systemMessage = `${trimmedPrelude}\n\n${environmentContext}`;