diff --git a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts index d015748315..be9c25bcaa 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts +++ b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts @@ -775,6 +775,86 @@ describe("addCustomInstructions", () => { expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENTS.md"), "utf-8") }) + it("should load AGENT.md (singular) when AGENTS.md is not found", async () => { + // Simulate no .roo/rules-test-mode directory + statMock.mockRejectedValueOnce({ code: "ENOENT" }) + + // Mock lstat to indicate AGENTS.md doesn't exist but AGENT.md does + lstatMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + if (pathStr.endsWith("AGENTS.md")) { + return Promise.reject({ code: "ENOENT" }) + } + if (pathStr.endsWith("AGENT.md")) { + return Promise.resolve({ + isSymbolicLink: vi.fn().mockReturnValue(false), + }) + } + return Promise.reject({ code: "ENOENT" }) + }) + + readFileMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + if (pathStr.endsWith("AGENT.md")) { + return Promise.resolve("Agent rules from AGENT.md file (singular)") + } + return Promise.reject({ code: "ENOENT" }) + }) + + const result = await addCustomInstructions( + "mode instructions", + "global instructions", + "/fake/path", + "test-mode", + { settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } }, + ) + + expect(result).toContain("# Agent Rules Standard (AGENT.md):") + expect(result).toContain("Agent rules from AGENT.md file (singular)") + expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENT.md"), "utf-8") + }) + + it("should prefer AGENTS.md over AGENT.md when both exist", async () => { + // Simulate no .roo/rules-test-mode directory + statMock.mockRejectedValueOnce({ code: "ENOENT" }) + + // Mock lstat to indicate both files exist + lstatMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + if (pathStr.endsWith("AGENTS.md") || pathStr.endsWith("AGENT.md")) { + return Promise.resolve({ + isSymbolicLink: vi.fn().mockReturnValue(false), + }) + } + return Promise.reject({ code: "ENOENT" }) + }) + + readFileMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + if (pathStr.endsWith("AGENTS.md")) { + return Promise.resolve("Agent rules from AGENTS.md file (plural)") + } + if (pathStr.endsWith("AGENT.md")) { + return Promise.resolve("Agent rules from AGENT.md file (singular)") + } + return Promise.reject({ code: "ENOENT" }) + }) + + const result = await addCustomInstructions( + "mode instructions", + "global instructions", + "/fake/path", + "test-mode", + { settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } }, + ) + + // Should contain AGENTS.md content (preferred) and not AGENT.md + expect(result).toContain("# Agent Rules Standard (AGENTS.md):") + expect(result).toContain("Agent rules from AGENTS.md file (plural)") + expect(result).not.toContain("Agent rules from AGENT.md file (singular)") + expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENTS.md"), "utf-8") + }) + it("should return empty string when no instructions provided", async () => { // Simulate no .roo/rules directory statMock.mockRejectedValueOnce({ code: "ENOENT" }) diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 22fb6122ec..2d70e45419 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -217,40 +217,46 @@ export async function loadRuleFiles(cwd: string): Promise { } /** - * Load AGENTS.md file from the project root if it exists + * Load AGENTS.md or AGENT.md file from the project root if it exists + * Checks for both AGENTS.md (standard) and AGENT.md (alternative) for compatibility */ async function loadAgentRulesFile(cwd: string): Promise { - try { - const agentsPath = path.join(cwd, "AGENTS.md") - let resolvedPath = agentsPath + // Try both filenames - AGENTS.md (standard) first, then AGENT.md (alternative) + const filenames = ["AGENTS.md", "AGENT.md"] - // Check if AGENTS.md exists and handle symlinks + for (const filename of filenames) { try { - const stats = await fs.lstat(agentsPath) - if (stats.isSymbolicLink()) { - // Create a temporary fileInfo array to use with resolveSymLink - const fileInfo: Array<{ originalPath: string; resolvedPath: string }> = [] - - // Use the existing resolveSymLink function to handle symlink resolution - await resolveSymLink(agentsPath, fileInfo, 0) - - // Extract the resolved path from fileInfo - if (fileInfo.length > 0) { - resolvedPath = fileInfo[0].resolvedPath + const agentPath = path.join(cwd, filename) + let resolvedPath = agentPath + + // Check if file exists and handle symlinks + try { + const stats = await fs.lstat(agentPath) + if (stats.isSymbolicLink()) { + // Create a temporary fileInfo array to use with resolveSymLink + const fileInfo: Array<{ originalPath: string; resolvedPath: string }> = [] + + // Use the existing resolveSymLink function to handle symlink resolution + await resolveSymLink(agentPath, fileInfo, 0) + + // Extract the resolved path from fileInfo + if (fileInfo.length > 0) { + resolvedPath = fileInfo[0].resolvedPath + } } + } catch (err) { + // If lstat fails (file doesn't exist), try next filename + continue } - } catch (err) { - // If lstat fails (file doesn't exist), return empty - return "" - } - // Read the content from the resolved path - const content = await safeReadFile(resolvedPath) - if (content) { - return `# Agent Rules Standard (AGENTS.md):\n${content}` + // Read the content from the resolved path + const content = await safeReadFile(resolvedPath) + if (content) { + return `# Agent Rules Standard (${filename}):\n${content}` + } + } catch (err) { + // Silently ignore errors - agent rules files are optional } - } catch (err) { - // Silently ignore errors - AGENTS.md is optional } return "" } diff --git a/src/core/protect/RooProtectedController.ts b/src/core/protect/RooProtectedController.ts index e6d57b22b5..f4d80c4d64 100644 --- a/src/core/protect/RooProtectedController.ts +++ b/src/core/protect/RooProtectedController.ts @@ -21,6 +21,7 @@ export class RooProtectedController { ".vscode/**", ".rooprotected", // For future use "AGENTS.md", + "AGENT.md", // Alternative singular form for compatibility ] constructor(cwd: string) { diff --git a/src/core/protect/__tests__/RooProtectedController.spec.ts b/src/core/protect/__tests__/RooProtectedController.spec.ts index 0ee6135e81..f19cf17548 100644 --- a/src/core/protect/__tests__/RooProtectedController.spec.ts +++ b/src/core/protect/__tests__/RooProtectedController.spec.ts @@ -48,6 +48,10 @@ describe("RooProtectedController", () => { expect(controller.isWriteProtected("AGENTS.md")).toBe(true) }) + it("should protect AGENT.md file", () => { + expect(controller.isWriteProtected("AGENT.md")).toBe(true) + }) + it("should not protect other files starting with .roo", () => { expect(controller.isWriteProtected(".roosettings")).toBe(false) expect(controller.isWriteProtected(".rooconfig")).toBe(false) @@ -147,6 +151,7 @@ describe("RooProtectedController", () => { ".vscode/**", ".rooprotected", "AGENTS.md", + "AGENT.md", ]) }) })