feat: add support for AGENT.md alongside AGENTS.md (#6913)

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
roomote[bot] 2025-08-12 21:20:59 -04:00 committed by GitHub
parent 7b0f489ea2
commit 8bd8f5dc59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 116 additions and 24 deletions

View file

@ -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" })

View file

@ -217,40 +217,46 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
}
/**
* 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<string> {
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 }> = []
const agentPath = path.join(cwd, filename)
let resolvedPath = agentPath
// Use the existing resolveSymLink function to handle symlink resolution
await resolveSymLink(agentsPath, fileInfo, 0)
// 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 }> = []
// Extract the resolved path from fileInfo
if (fileInfo.length > 0) {
resolvedPath = fileInfo[0].resolvedPath
// 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
}
// Read the content from the resolved path
const content = await safeReadFile(resolvedPath)
if (content) {
return `# Agent Rules Standard (${filename}):\n${content}`
}
} catch (err) {
// If lstat fails (file doesn't exist), return empty
return ""
// Silently ignore errors - agent rules files are optional
}
// Read the content from the resolved path
const content = await safeReadFile(resolvedPath)
if (content) {
return `# Agent Rules Standard (AGENTS.md):\n${content}`
}
} catch (err) {
// Silently ignore errors - AGENTS.md is optional
}
return ""
}

View file

@ -21,6 +21,7 @@ export class RooProtectedController {
".vscode/**",
".rooprotected", // For future use
"AGENTS.md",
"AGENT.md", // Alternative singular form for compatibility
]
constructor(cwd: string) {

View file

@ -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",
])
})
})