Skip to content

Commit 3977bbc

Browse files
roomote[bot]ellipsis-dev[bot]roomotedaniel-lxsmrubens
authored
feat: add support for Agent Rules standard via AGENTS.md (#5966) (#5969)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Roo Code <[email protected]> Co-authored-by: Daniel Riccio <[email protected]> Co-authored-by: Matt Rubens <[email protected]>
1 parent d48be23 commit 3977bbc

29 files changed

+240
-27
lines changed

src/core/prompts/__tests__/system-prompt.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,9 @@ describe("SYSTEM_PROMPT", () => {
577577

578578
it("should exclude update_todo_list tool when todoListEnabled is false", async () => {
579579
const settings = {
580+
maxConcurrentFileReads: 5,
580581
todoListEnabled: false,
582+
useAgentRules: true,
581583
}
582584

583585
const prompt = await SYSTEM_PROMPT(
@@ -607,7 +609,9 @@ describe("SYSTEM_PROMPT", () => {
607609

608610
it("should include update_todo_list tool when todoListEnabled is true", async () => {
609611
const settings = {
612+
maxConcurrentFileReads: 5,
610613
todoListEnabled: true,
614+
useAgentRules: true,
611615
}
612616

613617
const prompt = await SYSTEM_PROMPT(
@@ -636,7 +640,9 @@ describe("SYSTEM_PROMPT", () => {
636640

637641
it("should include update_todo_list tool when todoListEnabled is undefined", async () => {
638642
const settings = {
639-
// todoListEnabled not set
643+
maxConcurrentFileReads: 5,
644+
todoListEnabled: true,
645+
useAgentRules: true,
640646
}
641647

642648
const prompt = await SYSTEM_PROMPT(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ describe("custom-instructions global .roo support", () => {
193193
mockReadFile
194194
.mockResolvedValueOnce("global mode rule content")
195195
.mockResolvedValueOnce("project mode rule content")
196+
.mockResolvedValueOnce("") // AGENTS.md file (empty)
196197
.mockResolvedValueOnce("") // .roorules legacy file (empty)
197198
.mockResolvedValueOnce("") // .clinerules legacy file (empty)
198199

@@ -218,6 +219,7 @@ describe("custom-instructions global .roo support", () => {
218219
// Mock legacy mode file reading
219220
mockReadFile
220221
.mockResolvedValueOnce("legacy mode rule content") // .roorules-code
222+
.mockResolvedValueOnce("") // AGENTS.md file (empty)
221223
.mockResolvedValueOnce("") // generic .roorules (empty)
222224
.mockResolvedValueOnce("") // generic .clinerules (empty)
223225

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

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,129 @@ describe("addCustomInstructions", () => {
505505
expect(result).toContain("Rules from .roorules-test-mode:\nmode specific rules")
506506
})
507507

508+
it("should load AGENTS.md when settings.useAgentRules is true", async () => {
509+
// Simulate no .roo/rules-test-mode directory
510+
statMock.mockRejectedValueOnce({ code: "ENOENT" })
511+
512+
readFileMock.mockImplementation((filePath: PathLike) => {
513+
const pathStr = filePath.toString()
514+
if (pathStr.endsWith("AGENTS.md")) {
515+
return Promise.resolve("Agent rules from AGENTS.md file")
516+
}
517+
return Promise.reject({ code: "ENOENT" })
518+
})
519+
520+
const result = await addCustomInstructions(
521+
"mode instructions",
522+
"global instructions",
523+
"/fake/path",
524+
"test-mode",
525+
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
526+
)
527+
528+
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
529+
expect(result).toContain("Agent rules from AGENTS.md file")
530+
expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENTS.md"), "utf-8")
531+
})
532+
533+
it("should not load AGENTS.md when settings.useAgentRules is false", async () => {
534+
// Simulate no .roo/rules-test-mode directory
535+
statMock.mockRejectedValueOnce({ code: "ENOENT" })
536+
537+
readFileMock.mockImplementation((filePath: PathLike) => {
538+
const pathStr = filePath.toString()
539+
if (pathStr.endsWith("AGENTS.md")) {
540+
return Promise.resolve("Agent rules from AGENTS.md file")
541+
}
542+
return Promise.reject({ code: "ENOENT" })
543+
})
544+
545+
const result = await addCustomInstructions(
546+
"mode instructions",
547+
"global instructions",
548+
"/fake/path",
549+
"test-mode",
550+
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: false } },
551+
)
552+
553+
expect(result).not.toContain("# Agent Rules Standard (AGENTS.md):")
554+
expect(result).not.toContain("Agent rules from AGENTS.md file")
555+
})
556+
557+
it("should load AGENTS.md by default when settings.useAgentRules is undefined", async () => {
558+
// Simulate no .roo/rules-test-mode directory
559+
statMock.mockRejectedValueOnce({ code: "ENOENT" })
560+
561+
readFileMock.mockImplementation((filePath: PathLike) => {
562+
const pathStr = filePath.toString()
563+
if (pathStr.endsWith("AGENTS.md")) {
564+
return Promise.resolve("Agent rules from AGENTS.md file")
565+
}
566+
return Promise.reject({ code: "ENOENT" })
567+
})
568+
569+
const result = await addCustomInstructions(
570+
"mode instructions",
571+
"global instructions",
572+
"/fake/path",
573+
"test-mode",
574+
{}, // No settings.useAgentRules specified
575+
)
576+
577+
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
578+
expect(result).toContain("Agent rules from AGENTS.md file")
579+
expect(readFileMock).toHaveBeenCalledWith(expect.stringContaining("AGENTS.md"), "utf-8")
580+
})
581+
582+
it("should handle missing AGENTS.md gracefully", async () => {
583+
// Simulate no .roo/rules-test-mode directory
584+
statMock.mockRejectedValueOnce({ code: "ENOENT" })
585+
586+
readFileMock.mockRejectedValue({ code: "ENOENT" })
587+
588+
const result = await addCustomInstructions(
589+
"mode instructions",
590+
"global instructions",
591+
"/fake/path",
592+
"test-mode",
593+
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
594+
)
595+
596+
expect(result).toContain("Global Instructions:\nglobal instructions")
597+
expect(result).toContain("Mode-specific Instructions:\nmode instructions")
598+
expect(result).not.toContain("# Agent Rules Standard (AGENTS.md):")
599+
})
600+
601+
it("should include AGENTS.md content along with other rules", async () => {
602+
// Simulate no .roo/rules-test-mode directory
603+
statMock.mockRejectedValueOnce({ code: "ENOENT" })
604+
605+
readFileMock.mockImplementation((filePath: PathLike) => {
606+
const pathStr = filePath.toString()
607+
if (pathStr.endsWith("AGENTS.md")) {
608+
return Promise.resolve("Agent rules content")
609+
}
610+
if (pathStr.endsWith(".roorules")) {
611+
return Promise.resolve("Roo rules content")
612+
}
613+
return Promise.reject({ code: "ENOENT" })
614+
})
615+
616+
const result = await addCustomInstructions(
617+
"mode instructions",
618+
"global instructions",
619+
"/fake/path",
620+
"test-mode",
621+
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
622+
)
623+
624+
// Should contain both AGENTS.md and .roorules content
625+
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
626+
expect(result).toContain("Agent rules content")
627+
expect(result).toContain("# Rules from .roorules:")
628+
expect(result).toContain("Roo rules content")
629+
})
630+
508631
it("should return empty string when no instructions provided", async () => {
509632
// Simulate no .roo/rules directory
510633
statMock.mockRejectedValueOnce({ code: "ENOENT" })

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Dirent } from "fs"
55

66
import { isLanguage } from "@roo-code/types"
77

8+
import type { SystemPromptSettings } from "../types"
9+
810
import { LANGUAGES } from "../../../shared/language"
911
import { getRooDirectoriesForCwd, getGlobalRooDirectory } from "../../../services/roo-config"
1012

@@ -214,12 +216,32 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
214216
return ""
215217
}
216218

219+
/**
220+
* Load AGENTS.md file from the project root if it exists
221+
*/
222+
async function loadAgentRulesFile(cwd: string): Promise<string> {
223+
try {
224+
const agentsPath = path.join(cwd, "AGENTS.md")
225+
const content = await safeReadFile(agentsPath)
226+
if (content) {
227+
return `# Agent Rules Standard (AGENTS.md):\n${content}`
228+
}
229+
} catch (err) {
230+
// Silently ignore errors - AGENTS.md is optional
231+
}
232+
return ""
233+
}
234+
217235
export async function addCustomInstructions(
218236
modeCustomInstructions: string,
219237
globalCustomInstructions: string,
220238
cwd: string,
221239
mode: string,
222-
options: { language?: string; rooIgnoreInstructions?: string; settings?: Record<string, any> } = {},
240+
options: {
241+
language?: string
242+
rooIgnoreInstructions?: string
243+
settings?: SystemPromptSettings
244+
} = {},
223245
): Promise<string> {
224246
const sections = []
225247

@@ -297,6 +319,14 @@ export async function addCustomInstructions(
297319
rules.push(options.rooIgnoreInstructions)
298320
}
299321

322+
// Add AGENTS.md content if enabled (default: true)
323+
if (options.settings?.useAgentRules !== false) {
324+
const agentRulesContent = await loadAgentRulesFile(cwd)
325+
if (agentRulesContent && agentRulesContent.trim()) {
326+
rules.push(agentRulesContent.trim())
327+
}
328+
}
329+
300330
// Add generic rules
301331
const genericRuleContent = await loadRuleFiles(cwd)
302332
if (genericRuleContent && genericRuleContent.trim()) {

src/core/prompts/system.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as os from "os"
33

44
import type { ModeConfig, PromptComponent, CustomModePrompts, TodoItem } from "@roo-code/types"
55

6+
import type { SystemPromptSettings } from "./types"
7+
68
import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes"
79
import { DiffStrategy } from "../../shared/tools"
810
import { formatLanguage } from "../../shared/language"
@@ -57,7 +59,7 @@ async function generatePrompt(
5759
language?: string,
5860
rooIgnoreInstructions?: string,
5961
partialReadsEnabled?: boolean,
60-
settings?: Record<string, any>,
62+
settings?: SystemPromptSettings,
6163
todoList?: TodoItem[],
6264
): Promise<string> {
6365
if (!context) {
@@ -119,7 +121,11 @@ ${getSystemInfoSection(cwd)}
119121
120122
${getObjectiveSection(codeIndexManager, experiments)}
121123
122-
${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", cwd, mode, { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions, settings })}`
124+
${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", cwd, mode, {
125+
language: language ?? formatLanguage(vscode.env.language),
126+
rooIgnoreInstructions,
127+
settings,
128+
})}`
123129

124130
return basePrompt
125131
}
@@ -141,7 +147,7 @@ export const SYSTEM_PROMPT = async (
141147
language?: string,
142148
rooIgnoreInstructions?: string,
143149
partialReadsEnabled?: boolean,
144-
settings?: Record<string, any>,
150+
settings?: SystemPromptSettings,
145151
todoList?: TodoItem[],
146152
): Promise<string> => {
147153
if (!context) {
@@ -177,7 +183,11 @@ export const SYSTEM_PROMPT = async (
177183
globalCustomInstructions || "",
178184
cwd,
179185
mode,
180-
{ language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions, settings },
186+
{
187+
language: language ?? formatLanguage(vscode.env.language),
188+
rooIgnoreInstructions,
189+
settings,
190+
},
181191
)
182192

183193
// For file-based prompts, don't include the tool sections

src/core/prompts/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Settings passed to system prompt generation functions
3+
*/
4+
export interface SystemPromptSettings {
5+
maxConcurrentFileReads: number
6+
todoListEnabled: boolean
7+
useAgentRules: boolean
8+
}

src/core/protect/RooProtectedController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class RooProtectedController {
2020
".roo/**",
2121
".vscode/**",
2222
".rooprotected", // For future use
23+
"AGENTS.md",
2324
]
2425

2526
constructor(cwd: string) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ describe("RooProtectedController", () => {
4444
expect(controller.isWriteProtected(".vscode/tasks.json")).toBe(true)
4545
})
4646

47+
it("should protect AGENTS.md file", () => {
48+
expect(controller.isWriteProtected("AGENTS.md")).toBe(true)
49+
})
50+
4751
it("should not protect other files starting with .roo", () => {
4852
expect(controller.isWriteProtected(".roosettings")).toBe(false)
4953
expect(controller.isWriteProtected(".rooconfig")).toBe(false)
@@ -142,6 +146,7 @@ describe("RooProtectedController", () => {
142146
".roo/**",
143147
".vscode/**",
144148
".rooprotected",
149+
"AGENTS.md",
145150
])
146151
})
147152
})

src/core/task/Task.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as path from "path"
2+
import * as vscode from "vscode"
23
import os from "os"
34
import crypto from "crypto"
45
import EventEmitter from "events"
@@ -1660,8 +1661,9 @@ export class Task extends EventEmitter<ClineEvents> {
16601661
rooIgnoreInstructions,
16611662
maxReadFileLine !== -1,
16621663
{
1663-
maxConcurrentFileReads,
1664-
todoListEnabled: apiConfiguration?.todoListEnabled,
1664+
maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
1665+
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
1666+
useAgentRules: vscode.workspace.getConfiguration("roo-cline").get<boolean>("useAgentRules") ?? true,
16651667
},
16661668
)
16671669
})()

src/core/webview/generateSystemPrompt.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as vscode from "vscode"
12
import { WebviewMessage } from "../../shared/WebviewMessage"
23
import { defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
34
import { buildApiHandler } from "../../api"
@@ -81,7 +82,9 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
8182
rooIgnoreInstructions,
8283
maxReadFileLine !== -1,
8384
{
84-
maxConcurrentFileReads,
85+
maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
86+
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
87+
useAgentRules: vscode.workspace.getConfiguration("roo-cline").get<boolean>("useAgentRules") ?? true,
8588
},
8689
)
8790

0 commit comments

Comments
 (0)