mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
feat: add support for AGENTS.local.md personal override files (#11183)
Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
This commit is contained in:
parent
7f6272afb9
commit
1da2b1c457
3 changed files with 197 additions and 29 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,6 +18,7 @@ bin/
|
|||
|
||||
# Local prompts and rules
|
||||
/local-prompts
|
||||
AGENTS.local.md
|
||||
|
||||
# Test environment
|
||||
.test_env
|
||||
|
|
|
|||
|
|
@ -1587,4 +1587,136 @@ describe("Rules directory reading", () => {
|
|||
const result = await loadRuleFiles("/fake/path")
|
||||
expect(result).toBe("\n# Rules from .roorules:\nfallback content\n")
|
||||
})
|
||||
|
||||
it("should load AGENTS.local.md alongside AGENTS.md for personal overrides", async () => {
|
||||
// Simulate no .roo/rules-test-mode directory
|
||||
statMock.mockRejectedValueOnce({ code: "ENOENT" })
|
||||
|
||||
// Mock lstat to indicate both AGENTS.md and AGENTS.local.md exist (not symlinks)
|
||||
lstatMock.mockImplementation((filePath: PathLike) => {
|
||||
const pathStr = filePath.toString()
|
||||
if (pathStr.endsWith("AGENTS.md") || pathStr.endsWith("AGENTS.local.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.local.md")) {
|
||||
return Promise.resolve("Local overrides from AGENTS.local.md")
|
||||
}
|
||||
if (pathStr.endsWith("AGENTS.md")) {
|
||||
return Promise.resolve("Base rules from AGENTS.md")
|
||||
}
|
||||
return Promise.reject({ code: "ENOENT" })
|
||||
})
|
||||
|
||||
const result = await addCustomInstructions(
|
||||
"mode instructions",
|
||||
"global instructions",
|
||||
"/fake/path",
|
||||
"test-mode",
|
||||
{
|
||||
settings: {
|
||||
todoListEnabled: true,
|
||||
useAgentRules: true,
|
||||
newTaskRequireTodos: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Should contain both AGENTS.md and AGENTS.local.md content
|
||||
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
|
||||
expect(result).toContain("Base rules from AGENTS.md")
|
||||
expect(result).toContain("# Agent Rules Local (AGENTS.local.md):")
|
||||
expect(result).toContain("Local overrides from AGENTS.local.md")
|
||||
})
|
||||
|
||||
it("should load AGENTS.local.md even when base AGENTS.md does not exist", async () => {
|
||||
// Simulate no .roo/rules-test-mode directory
|
||||
statMock.mockRejectedValueOnce({ code: "ENOENT" })
|
||||
|
||||
// Mock lstat to indicate only AGENTS.local.md exists (no base file)
|
||||
lstatMock.mockImplementation((filePath: PathLike) => {
|
||||
const pathStr = filePath.toString()
|
||||
if (pathStr.endsWith("AGENTS.local.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.local.md")) {
|
||||
return Promise.resolve("Local overrides without base file")
|
||||
}
|
||||
return Promise.reject({ code: "ENOENT" })
|
||||
})
|
||||
|
||||
const result = await addCustomInstructions(
|
||||
"mode instructions",
|
||||
"global instructions",
|
||||
"/fake/path",
|
||||
"test-mode",
|
||||
{
|
||||
settings: {
|
||||
todoListEnabled: true,
|
||||
useAgentRules: true,
|
||||
newTaskRequireTodos: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Should contain AGENTS.local.md content even without base AGENTS.md
|
||||
expect(result).toContain("# Agent Rules Local (AGENTS.local.md):")
|
||||
expect(result).toContain("Local overrides without base file")
|
||||
})
|
||||
|
||||
it("should load AGENTS.md without .local.md when local file does not exist", async () => {
|
||||
// Simulate no .roo/rules-test-mode directory
|
||||
statMock.mockRejectedValueOnce({ code: "ENOENT" })
|
||||
|
||||
// Mock lstat to indicate only AGENTS.md exists (no local override)
|
||||
lstatMock.mockImplementation((filePath: PathLike) => {
|
||||
const pathStr = filePath.toString()
|
||||
if (pathStr.endsWith("AGENTS.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("Base rules from AGENTS.md only")
|
||||
}
|
||||
return Promise.reject({ code: "ENOENT" })
|
||||
})
|
||||
|
||||
const result = await addCustomInstructions(
|
||||
"mode instructions",
|
||||
"global instructions",
|
||||
"/fake/path",
|
||||
"test-mode",
|
||||
{
|
||||
settings: {
|
||||
todoListEnabled: true,
|
||||
useAgentRules: true,
|
||||
newTaskRequireTodos: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Should contain only AGENTS.md content
|
||||
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
|
||||
expect(result).toContain("Base rules from AGENTS.md only")
|
||||
expect(result).not.toContain("AGENTS.local.md")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -238,9 +238,48 @@ export async function loadRuleFiles(cwd: string, enableSubfolderRules: boolean =
|
|||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content from an agent rules file (AGENTS.md, AGENT.md, etc.)
|
||||
* Handles symlink resolution.
|
||||
*
|
||||
* @param filePath - Full path to the agent rules file
|
||||
* @returns File content or empty string if file doesn't exist
|
||||
*/
|
||||
async function readAgentRulesFile(filePath: string): Promise<string> {
|
||||
let resolvedPath = filePath
|
||||
|
||||
// Check if file exists and handle symlinks
|
||||
try {
|
||||
const stats = await fs.lstat(filePath)
|
||||
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(filePath, 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), return empty
|
||||
return ""
|
||||
}
|
||||
|
||||
// Read the content from the resolved path
|
||||
return safeReadFile(resolvedPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load AGENTS.md or AGENT.md file from a specific directory
|
||||
* Checks for both AGENTS.md (standard) and AGENT.md (alternative) for compatibility
|
||||
* Also loads AGENTS.local.md for personal overrides (not checked in to version control)
|
||||
* AGENTS.local.md can be loaded even if AGENTS.md doesn't exist
|
||||
*
|
||||
* @param directory - Directory to check for AGENTS.md
|
||||
* @param showPath - Whether to include the directory path in the header
|
||||
|
|
@ -253,50 +292,46 @@ async function loadAgentRulesFileFromDirectory(
|
|||
): Promise<string> {
|
||||
// Try both filenames - AGENTS.md (standard) first, then AGENT.md (alternative)
|
||||
const filenames = ["AGENTS.md", "AGENT.md"]
|
||||
const results: string[] = []
|
||||
const displayPath = cwd ? path.relative(cwd, directory) : directory
|
||||
|
||||
for (const filename of filenames) {
|
||||
try {
|
||||
const agentPath = path.join(directory, filename)
|
||||
let resolvedPath = agentPath
|
||||
const content = await readAgentRulesFile(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
|
||||
}
|
||||
|
||||
// Read the content from the resolved path
|
||||
const content = await safeReadFile(resolvedPath)
|
||||
if (content) {
|
||||
// Compute relative path for display if cwd is provided
|
||||
const displayPath = cwd ? path.relative(cwd, directory) : directory
|
||||
const header = showPath
|
||||
? `# Agent Rules Standard (${filename}) from ${displayPath}:`
|
||||
: `# Agent Rules Standard (${filename}):`
|
||||
return `${header}\n${content}`
|
||||
results.push(`${header}\n${content}`)
|
||||
|
||||
// Found a standard file, don't check alternative
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently ignore errors - agent rules files are optional
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
// Always try to load AGENTS.local.md for personal overrides (even if AGENTS.md doesn't exist)
|
||||
try {
|
||||
const localFilename = "AGENTS.local.md"
|
||||
const localPath = path.join(directory, localFilename)
|
||||
const localContent = await readAgentRulesFile(localPath)
|
||||
|
||||
if (localContent) {
|
||||
const localHeader = showPath
|
||||
? `# Agent Rules Local (${localFilename}) from ${displayPath}:`
|
||||
: `# Agent Rules Local (${localFilename}):`
|
||||
results.push(`${localHeader}\n${localContent}`)
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently ignore errors - local agent rules file is optional
|
||||
}
|
||||
|
||||
return results.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue