Skip to content

Commit 09772a0

Browse files
committed
Change AGENTS.md to read from project root, not workspace
Design decision: Read AGENTS.md from projectPath (project root) instead of workspacePath (individual workspace/worktree directories). Rationale: - Ensures consistent instructions across all workspaces for a project - Simplifies mental model: one AGENTS.md per project, not per branch - Aligns with typical workflow where instructions are project-level context - Still allows workspace-specific overrides via AGENTS.local.md (gitignored) Changes: - buildSystemMessage() now reads AGENTS.md from metadata.projectPath - Environment context still uses workspacePath (where code executes) - Updated docs/instruction-files.md to reflect project-level instructions - Updated all tests to use projectDir instead of workspaceDir - Renamed test variables for clarity Breaking change: Branch-specific AGENTS.md files will no longer be read. Migration: Move workspace-specific instructions to project root AGENTS.md, or use AGENTS.local.md for personal/workspace-specific overrides.
1 parent 2dc1f47 commit 09772a0

File tree

3 files changed

+45
-38
lines changed

3 files changed

+45
-38
lines changed

docs/instruction-files.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
cmux layers instructions from two locations:
66

77
1. `~/.cmux/AGENTS.md` (+ optional `AGENTS.local.md`) — global defaults
8-
2. `<workspace>/AGENTS.md` (+ optional `AGENTS.local.md`) — workspace-specific context
8+
2. `<project>/AGENTS.md` (+ optional `AGENTS.local.md`) — project-specific context
99

1010
Priority within each location: `AGENTS.md``AGENT.md``CLAUDE.md` (first match wins). If the base file is found, cmux also appends `AGENTS.local.md` from the same directory when present.
1111

12+
**Note**: Instructions are read from the project root (where the main repository is located), not from individual workspace directories. This ensures consistent instructions across all workspaces for a project.
13+
1214
## Mode Prompts
1315

1416
> Use mode-specific sections to optimize context and customize the behavior specific modes.
@@ -19,7 +21,7 @@ cmux reads mode context from sections inside your instruction files. Add a headi
1921

2022
Rules:
2123

22-
- Workspace instructions are checked first, then global instructions
24+
- Project instructions are checked first, then global instructions
2325
- The first matching section wins (at most one section is used)
2426
- The section's content is everything until the next heading of the same or higher level
2527
- Missing sections are ignored (no error)

src/services/systemMessage.test.ts

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import type { Mock } from "bun:test";
88

99
describe("buildSystemMessage", () => {
1010
let tempDir: string;
11+
let projectDir: string;
1112
let workspaceDir: string;
1213
let globalDir: string;
1314
let mockHomedir: Mock<typeof os.homedir>;
1415

1516
beforeEach(async () => {
1617
// Create temp directory for test
1718
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "systemMessage-test-"));
19+
projectDir = path.join(tempDir, "project");
1820
workspaceDir = path.join(tempDir, "workspace");
1921
globalDir = path.join(tempDir, ".cmux");
22+
await fs.mkdir(projectDir, { recursive: true });
2023
await fs.mkdir(workspaceDir, { recursive: true });
2124
await fs.mkdir(globalDir, { recursive: true });
2225

@@ -33,9 +36,9 @@ describe("buildSystemMessage", () => {
3336
});
3437

3538
test("includes mode-specific section when mode is provided", async () => {
36-
// Write instruction file with mode section
39+
// Write instruction file with mode section to projectDir
3740
await fs.writeFile(
38-
path.join(workspaceDir, "AGENTS.md"),
41+
path.join(projectDir, "AGENTS.md"),
3942
`# General Instructions
4043
Always be helpful.
4144
@@ -49,7 +52,7 @@ Use diagrams where appropriate.
4952
id: "test-workspace",
5053
name: "test-workspace",
5154
projectName: "test-project",
52-
projectPath: tempDir,
55+
projectPath: projectDir,
5356
};
5457

5558
const systemMessage = await buildSystemMessage(metadata, workspaceDir, "plan");
@@ -65,9 +68,9 @@ Use diagrams where appropriate.
6568
});
6669

6770
test("excludes mode-specific section when mode is not provided", async () => {
68-
// Write instruction file with mode section
71+
// Write instruction file with mode section to projectDir
6972
await fs.writeFile(
70-
path.join(workspaceDir, "AGENTS.md"),
73+
path.join(projectDir, "AGENTS.md"),
7174
`# General Instructions
7275
Always be helpful.
7376
@@ -80,7 +83,7 @@ Focus on planning and design.
8083
id: "test-workspace",
8184
name: "test-workspace",
8285
projectName: "test-project",
83-
projectPath: tempDir,
86+
projectPath: projectDir,
8487
};
8588

8689
const systemMessage = await buildSystemMessage(metadata, workspaceDir);
@@ -94,7 +97,7 @@ Focus on planning and design.
9497
expect(systemMessage).toContain("Focus on planning and design");
9598
});
9699

97-
test("prefers workspace mode section over global mode section", async () => {
100+
test("prefers project mode section over global mode section", async () => {
98101
// Write global instruction file with mode section
99102
await fs.writeFile(
100103
path.join(globalDir, "AGENTS.md"),
@@ -105,33 +108,33 @@ Global plan instructions.
105108
`
106109
);
107110

108-
// Write workspace instruction file with mode section
111+
// Write project instruction file with mode section
109112
await fs.writeFile(
110-
path.join(workspaceDir, "AGENTS.md"),
111-
`# Workspace Instructions
113+
path.join(projectDir, "AGENTS.md"),
114+
`# Project Instructions
112115
113116
## Mode: Plan
114-
Workspace plan instructions (should win).
117+
Project plan instructions (should win).
115118
`
116119
);
117120

118121
const metadata: WorkspaceMetadata = {
119122
id: "test-workspace",
120123
name: "test-workspace",
121124
projectName: "test-project",
122-
projectPath: tempDir,
125+
projectPath: projectDir,
123126
};
124127

125128
const systemMessage = await buildSystemMessage(metadata, workspaceDir, "plan");
126129

127-
// Should include workspace mode section in the <plan> tag (workspace wins)
128-
expect(systemMessage).toMatch(/<plan>\s*Workspace plan instructions \(should win\)\./s);
130+
// Should include project mode section in the <plan> tag (project wins)
131+
expect(systemMessage).toMatch(/<plan>\s*Project plan instructions \(should win\)\./s);
129132
// Global instructions are still present in <custom-instructions> section (that's correct)
130-
// But the mode-specific <plan> section should only have workspace content
133+
// But the mode-specific <plan> section should only have project content
131134
expect(systemMessage).not.toMatch(/<plan>[^<]*Global plan instructions/s);
132135
});
133136

134-
test("falls back to global mode section when workspace has none", async () => {
137+
test("falls back to global mode section when project has none", async () => {
135138
// Write global instruction file with mode section
136139
await fs.writeFile(
137140
path.join(globalDir, "AGENTS.md"),
@@ -142,19 +145,19 @@ Global plan instructions.
142145
`
143146
);
144147

145-
// Write workspace instruction file WITHOUT mode section
148+
// Write project instruction file WITHOUT mode section
146149
await fs.writeFile(
147-
path.join(workspaceDir, "AGENTS.md"),
148-
`# Workspace Instructions
149-
Just general workspace stuff.
150+
path.join(projectDir, "AGENTS.md"),
151+
`# Project Instructions
152+
Just general project stuff.
150153
`
151154
);
152155

153156
const metadata: WorkspaceMetadata = {
154157
id: "test-workspace",
155158
name: "test-workspace",
156159
projectName: "test-project",
157-
projectPath: tempDir,
160+
projectPath: projectDir,
158161
};
159162

160163
const systemMessage = await buildSystemMessage(metadata, workspaceDir, "plan");
@@ -165,7 +168,7 @@ Just general workspace stuff.
165168

166169
test("handles mode with special characters by sanitizing tag name", async () => {
167170
await fs.writeFile(
168-
path.join(workspaceDir, "AGENTS.md"),
171+
path.join(projectDir, "AGENTS.md"),
169172
`## Mode: My-Special_Mode!
170173
Special mode instructions.
171174
`
@@ -175,7 +178,7 @@ Special mode instructions.
175178
id: "test-workspace",
176179
name: "test-workspace",
177180
projectName: "test-project",
178-
projectPath: tempDir,
181+
projectPath: projectDir,
179182
};
180183

181184
const systemMessage = await buildSystemMessage(metadata, workspaceDir, "My-Special_Mode!");

src/services/systemMessage.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ function getSystemDirectory(): string {
5555
*
5656
* Instruction sources are layered in this order:
5757
* 1. Global instructions: ~/.cmux/AGENTS.md (+ AGENTS.local.md)
58-
* 2. Workspace instructions: <workspace>/AGENTS.md (+ AGENTS.local.md)
58+
* 2. Project instructions: <projectPath>/AGENTS.md (+ AGENTS.local.md)
5959
* 3. Mode-specific context (if mode provided): Extract a section titled "Mode: <mode>"
6060
* (case-insensitive) from the instruction file. We search at most one section in
61-
* precedence order: workspace instructions first, then global instructions.
61+
* precedence order: project instructions first, then global instructions.
6262
*
6363
* Each instruction file location is searched for in priority order:
6464
* - AGENTS.md
@@ -68,8 +68,8 @@ function getSystemDirectory(): string {
6868
* If a base instruction file is found, its corresponding .local.md variant is also
6969
* checked and appended when building the instruction set (useful for personal preferences not committed to git).
7070
*
71-
* @param metadata - Workspace metadata
72-
* @param workspacePath - Absolute path to the workspace worktree directory
71+
* @param metadata - Workspace metadata (contains projectPath for reading AGENTS.md)
72+
* @param workspacePath - Absolute path to the workspace directory (for environment context)
7373
* @param mode - Optional mode name (e.g., "plan", "exec") - looks for {MODE}.md files if provided
7474
* @param additionalSystemInstructions - Optional additional system instructions to append at the end
7575
* @returns System message string with all instruction sources combined
@@ -90,21 +90,22 @@ export async function buildSystemMessage(
9090
}
9191

9292
const systemDir = getSystemDirectory();
93-
const workspaceDir = workspacePath;
93+
const projectDir = metadata.projectPath;
9494

95-
// Gather instruction sets from both global and workspace directories
96-
// Global instructions apply first, then workspace-specific ones
97-
const instructionDirectories = [systemDir, workspaceDir];
95+
// Gather instruction sets from both global and project directories
96+
// Global instructions apply first, then project-specific ones
97+
// Note: We read from projectPath (the main repo) not workspacePath (the worktree)
98+
const instructionDirectories = [systemDir, projectDir];
9899
const instructionSegments = await gatherInstructionSets(instructionDirectories);
99100
const customInstructions = instructionSegments.join("\n\n");
100101

101-
// Look for a "Mode: <mode>" section inside instruction sets, preferring workspace over global
102+
// Look for a "Mode: <mode>" section inside instruction sets, preferring project over global
102103
// This behavior is documented in docs/instruction-files.md - keep both in sync when changing.
103104
let modeContent: string | null = null;
104105
if (mode) {
105-
const workspaceInstructions = await readInstructionSet(workspaceDir);
106-
if (workspaceInstructions) {
107-
modeContent = extractModeSection(workspaceInstructions, mode);
106+
const projectInstructions = await readInstructionSet(projectDir);
107+
if (projectInstructions) {
108+
modeContent = extractModeSection(projectInstructions, mode);
108109
}
109110
if (!modeContent) {
110111
const globalInstructions = await readInstructionSet(systemDir);
@@ -115,7 +116,8 @@ export async function buildSystemMessage(
115116
}
116117

117118
// Build the final system message
118-
const environmentContext = buildEnvironmentContext(workspaceDir);
119+
// Use workspacePath for environment context (where code actually executes)
120+
const environmentContext = buildEnvironmentContext(workspacePath);
119121
const trimmedPrelude = PRELUDE.trim();
120122
let systemMessage = `${trimmedPrelude}\n\n${environmentContext}`;
121123

0 commit comments

Comments
 (0)