Skip to content

Commit 8bd8f5d

Browse files
feat: add support for AGENT.md alongside AGENTS.md (#6913)
Co-authored-by: Roo Code <[email protected]>
1 parent 7b0f489 commit 8bd8f5d

File tree

4 files changed

+118
-26
lines changed

4 files changed

+118
-26
lines changed

src/core/prompts/sections/__tests__/custom-instructions.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,86 @@ describe("addCustomInstructions", () => {
775775
expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENTS.md"), "utf-8")
776776
})
777777

778+
it("should load AGENT.md (singular) when AGENTS.md is not found", async () => {
779+
// Simulate no .roo/rules-test-mode directory
780+
statMock.mockRejectedValueOnce({ code: "ENOENT" })
781+
782+
// Mock lstat to indicate AGENTS.md doesn't exist but AGENT.md does
783+
lstatMock.mockImplementation((filePath: PathLike) => {
784+
const pathStr = filePath.toString()
785+
if (pathStr.endsWith("AGENTS.md")) {
786+
return Promise.reject({ code: "ENOENT" })
787+
}
788+
if (pathStr.endsWith("AGENT.md")) {
789+
return Promise.resolve({
790+
isSymbolicLink: vi.fn().mockReturnValue(false),
791+
})
792+
}
793+
return Promise.reject({ code: "ENOENT" })
794+
})
795+
796+
readFileMock.mockImplementation((filePath: PathLike) => {
797+
const pathStr = filePath.toString()
798+
if (pathStr.endsWith("AGENT.md")) {
799+
return Promise.resolve("Agent rules from AGENT.md file (singular)")
800+
}
801+
return Promise.reject({ code: "ENOENT" })
802+
})
803+
804+
const result = await addCustomInstructions(
805+
"mode instructions",
806+
"global instructions",
807+
"/fake/path",
808+
"test-mode",
809+
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
810+
)
811+
812+
expect(result).toContain("# Agent Rules Standard (AGENT.md):")
813+
expect(result).toContain("Agent rules from AGENT.md file (singular)")
814+
expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENT.md"), "utf-8")
815+
})
816+
817+
it("should prefer AGENTS.md over AGENT.md when both exist", async () => {
818+
// Simulate no .roo/rules-test-mode directory
819+
statMock.mockRejectedValueOnce({ code: "ENOENT" })
820+
821+
// Mock lstat to indicate both files exist
822+
lstatMock.mockImplementation((filePath: PathLike) => {
823+
const pathStr = filePath.toString()
824+
if (pathStr.endsWith("AGENTS.md") || pathStr.endsWith("AGENT.md")) {
825+
return Promise.resolve({
826+
isSymbolicLink: vi.fn().mockReturnValue(false),
827+
})
828+
}
829+
return Promise.reject({ code: "ENOENT" })
830+
})
831+
832+
readFileMock.mockImplementation((filePath: PathLike) => {
833+
const pathStr = filePath.toString()
834+
if (pathStr.endsWith("AGENTS.md")) {
835+
return Promise.resolve("Agent rules from AGENTS.md file (plural)")
836+
}
837+
if (pathStr.endsWith("AGENT.md")) {
838+
return Promise.resolve("Agent rules from AGENT.md file (singular)")
839+
}
840+
return Promise.reject({ code: "ENOENT" })
841+
})
842+
843+
const result = await addCustomInstructions(
844+
"mode instructions",
845+
"global instructions",
846+
"/fake/path",
847+
"test-mode",
848+
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
849+
)
850+
851+
// Should contain AGENTS.md content (preferred) and not AGENT.md
852+
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
853+
expect(result).toContain("Agent rules from AGENTS.md file (plural)")
854+
expect(result).not.toContain("Agent rules from AGENT.md file (singular)")
855+
expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENTS.md"), "utf-8")
856+
})
857+
778858
it("should return empty string when no instructions provided", async () => {
779859
// Simulate no .roo/rules directory
780860
statMock.mockRejectedValueOnce({ code: "ENOENT" })

src/core/prompts/sections/custom-instructions.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -217,40 +217,46 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
217217
}
218218

219219
/**
220-
* Load AGENTS.md file from the project root if it exists
220+
* Load AGENTS.md or AGENT.md file from the project root if it exists
221+
* Checks for both AGENTS.md (standard) and AGENT.md (alternative) for compatibility
221222
*/
222223
async function loadAgentRulesFile(cwd: string): Promise<string> {
223-
try {
224-
const agentsPath = path.join(cwd, "AGENTS.md")
225-
let resolvedPath = agentsPath
224+
// Try both filenames - AGENTS.md (standard) first, then AGENT.md (alternative)
225+
const filenames = ["AGENTS.md", "AGENT.md"]
226226

227-
// Check if AGENTS.md exists and handle symlinks
227+
for (const filename of filenames) {
228228
try {
229-
const stats = await fs.lstat(agentsPath)
230-
if (stats.isSymbolicLink()) {
231-
// Create a temporary fileInfo array to use with resolveSymLink
232-
const fileInfo: Array<{ originalPath: string; resolvedPath: string }> = []
233-
234-
// Use the existing resolveSymLink function to handle symlink resolution
235-
await resolveSymLink(agentsPath, fileInfo, 0)
236-
237-
// Extract the resolved path from fileInfo
238-
if (fileInfo.length > 0) {
239-
resolvedPath = fileInfo[0].resolvedPath
229+
const agentPath = path.join(cwd, filename)
230+
let resolvedPath = agentPath
231+
232+
// Check if file exists and handle symlinks
233+
try {
234+
const stats = await fs.lstat(agentPath)
235+
if (stats.isSymbolicLink()) {
236+
// Create a temporary fileInfo array to use with resolveSymLink
237+
const fileInfo: Array<{ originalPath: string; resolvedPath: string }> = []
238+
239+
// Use the existing resolveSymLink function to handle symlink resolution
240+
await resolveSymLink(agentPath, fileInfo, 0)
241+
242+
// Extract the resolved path from fileInfo
243+
if (fileInfo.length > 0) {
244+
resolvedPath = fileInfo[0].resolvedPath
245+
}
240246
}
247+
} catch (err) {
248+
// If lstat fails (file doesn't exist), try next filename
249+
continue
241250
}
242-
} catch (err) {
243-
// If lstat fails (file doesn't exist), return empty
244-
return ""
245-
}
246251

247-
// Read the content from the resolved path
248-
const content = await safeReadFile(resolvedPath)
249-
if (content) {
250-
return `# Agent Rules Standard (AGENTS.md):\n${content}`
252+
// Read the content from the resolved path
253+
const content = await safeReadFile(resolvedPath)
254+
if (content) {
255+
return `# Agent Rules Standard (${filename}):\n${content}`
256+
}
257+
} catch (err) {
258+
// Silently ignore errors - agent rules files are optional
251259
}
252-
} catch (err) {
253-
// Silently ignore errors - AGENTS.md is optional
254260
}
255261
return ""
256262
}

src/core/protect/RooProtectedController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class RooProtectedController {
2121
".vscode/**",
2222
".rooprotected", // For future use
2323
"AGENTS.md",
24+
"AGENT.md", // Alternative singular form for compatibility
2425
]
2526

2627
constructor(cwd: string) {

src/core/protect/__tests__/RooProtectedController.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ describe("RooProtectedController", () => {
4848
expect(controller.isWriteProtected("AGENTS.md")).toBe(true)
4949
})
5050

51+
it("should protect AGENT.md file", () => {
52+
expect(controller.isWriteProtected("AGENT.md")).toBe(true)
53+
})
54+
5155
it("should not protect other files starting with .roo", () => {
5256
expect(controller.isWriteProtected(".roosettings")).toBe(false)
5357
expect(controller.isWriteProtected(".rooconfig")).toBe(false)
@@ -147,6 +151,7 @@ describe("RooProtectedController", () => {
147151
".vscode/**",
148152
".rooprotected",
149153
"AGENTS.md",
154+
"AGENT.md",
150155
])
151156
})
152157
})

0 commit comments

Comments
 (0)