Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/core/prompts/sections/__tests__/custom-instructions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand Down
58 changes: 32 additions & 26 deletions src/core/prompts/sections/custom-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> = []

// 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 ""
}
Expand Down
1 change: 1 addition & 0 deletions src/core/protect/RooProtectedController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class RooProtectedController {
".vscode/**",
".rooprotected", // For future use
"AGENTS.md",
"AGENT.md", // Alternative singular form for compatibility
]

constructor(cwd: string) {
Expand Down
5 changes: 5 additions & 0 deletions src/core/protect/__tests__/RooProtectedController.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -147,6 +151,7 @@ describe("RooProtectedController", () => {
".vscode/**",
".rooprotected",
"AGENTS.md",
"AGENT.md",
])
})
})
Expand Down
Loading