feat: add disableDefaultRules option to ModeConfig

Adds a new optional `disableDefaultRules` boolean field to the ModeConfig
schema. When set to true on a custom mode, only mode-specific rules
(.roo/rules-{mode}/) are loaded -- generic/default rules (.roo/rules/,
AGENTS.md, .roorules, .clinerules) are skipped.

This addresses the need for focused modes that should not inherit the
default rule set, reducing rule noise and improving model adherence.

Changes:
- packages/types/src/mode.ts: Add disableDefaultRules to modeConfigSchema
- src/core/prompts/sections/custom-instructions.ts: Respect the flag
- src/core/prompts/system.ts: Thread the flag from mode config
- webview-ui: Add UI toggle for custom modes
- Tests: Schema validation and addCustomInstructions behavior

Closes #11611
This commit is contained in:
Roo Code 2026-02-19 20:54:50 +00:00
parent d8cfbfdb05
commit 7686d288a6
7 changed files with 198 additions and 11 deletions

View file

@ -102,6 +102,7 @@ export const modeConfigSchema = z.object({
customInstructions: z.string().optional(),
groups: groupEntryArraySchema,
source: z.enum(["global", "project"]).optional(),
disableDefaultRules: z.boolean().optional(),
})
export type ModeConfig = z.infer<typeof modeConfigSchema>

View file

@ -290,4 +290,60 @@ describe("CustomModeSchema", () => {
expect(result.success).toBe(false)
})
})
describe("disableDefaultRules", () => {
test("accepts mode with disableDefaultRules set to true", () => {
const mode = {
slug: "focused-mode",
name: "Focused Mode",
roleDefinition: "A focused mode",
groups: ["read"] as const,
disableDefaultRules: true,
} satisfies ModeConfig
expect(() => validateCustomMode(mode)).not.toThrow()
const parsed = modeConfigSchema.parse(mode)
expect(parsed.disableDefaultRules).toBe(true)
})
test("accepts mode with disableDefaultRules set to false", () => {
const mode = {
slug: "normal-mode",
name: "Normal Mode",
roleDefinition: "A normal mode",
groups: ["read"] as const,
disableDefaultRules: false,
} satisfies ModeConfig
expect(() => validateCustomMode(mode)).not.toThrow()
const parsed = modeConfigSchema.parse(mode)
expect(parsed.disableDefaultRules).toBe(false)
})
test("accepts mode without disableDefaultRules (optional field)", () => {
const mode = {
slug: "default-mode",
name: "Default Mode",
roleDefinition: "A default mode",
groups: ["read"] as const,
} satisfies ModeConfig
expect(() => validateCustomMode(mode)).not.toThrow()
const parsed = modeConfigSchema.parse(mode)
expect(parsed.disableDefaultRules).toBeUndefined()
})
test("rejects non-boolean disableDefaultRules", () => {
const mode = {
slug: "bad-mode",
name: "Bad Mode",
roleDefinition: "A bad mode",
groups: ["read"],
disableDefaultRules: "yes" as any,
}
const result = modeConfigSchema.safeParse(mode)
expect(result.success).toBe(false)
})
})
})

View file

@ -1719,4 +1719,97 @@ describe("Rules directory reading", () => {
expect(result).toContain("Base rules from AGENTS.md only")
expect(result).not.toContain("AGENTS.local.md")
})
it("should skip generic rules and AGENTS.md when disableDefaultRules is true", async () => {
// Simulate .roo/rules-test-mode directory exists with mode-specific rules
statMock.mockResolvedValueOnce({
isDirectory: () => true,
} as any)
readdirMock.mockResolvedValueOnce([
{
name: "mode-rule.md",
isFile: () => true,
isSymbolicLink: () => false,
parentPath: "/fake/path/.roo/rules-test-mode",
},
] as any)
statMock.mockResolvedValueOnce({
isFile: () => true,
} as any)
readFileMock.mockImplementation((filePath: PathLike) => {
const pathStr = filePath.toString().replace(/\\/g, "/")
if (pathStr === "/fake/path/.roo/rules-test-mode/mode-rule.md") {
return Promise.resolve("mode specific rule content")
}
if (pathStr.endsWith("AGENTS.md")) {
return Promise.resolve("Agent rules that should be skipped")
}
if (pathStr.endsWith(".roorules")) {
return Promise.resolve("Generic rules that should be skipped")
}
return Promise.reject({ code: "ENOENT" })
})
const result = await addCustomInstructions(
"mode instructions",
"global instructions",
"/fake/path",
"test-mode",
{
disableDefaultRules: true,
settings: {
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
// Should contain mode-specific rules
expect(result).toContain("mode specific rule content")
// Should NOT contain AGENTS.md or generic rules
expect(result).not.toContain("Agent rules that should be skipped")
expect(result).not.toContain("Generic rules that should be skipped")
expect(result).not.toContain("# Agent Rules Standard")
expect(result).not.toContain("# Rules from .roo directories")
expect(result).not.toContain("# Rules from .roorules")
// Should still contain custom instructions
expect(result).toContain("Mode-specific Instructions:\nmode instructions")
expect(result).toContain("Global Instructions:\nglobal instructions")
})
it("should load generic rules when disableDefaultRules is false", async () => {
// Simulate no .roo/rules-test-mode directory
statMock.mockRejectedValueOnce({ code: "ENOENT" })
// Simulate no .roo/rules directory
statMock.mockRejectedValueOnce({ code: "ENOENT" })
readFileMock.mockImplementation((filePath: PathLike) => {
const pathStr = filePath.toString()
if (pathStr.endsWith(".roorules")) {
return Promise.resolve("Generic rules content")
}
return Promise.reject({ code: "ENOENT" })
})
lstatMock.mockImplementation(() => {
return Promise.reject({ code: "ENOENT" })
})
const result = await addCustomInstructions("", "", "/fake/path", "test-mode", {
disableDefaultRules: false,
settings: {
todoListEnabled: true,
useAgentRules: false,
newTaskRequireTodos: false,
},
})
// Should contain generic rules when disableDefaultRules is false
expect(result).toContain("Generic rules content")
expect(result).toContain("# Rules from .roorules")
})
})

View file

@ -388,6 +388,7 @@ export async function addCustomInstructions(
language?: string
rooIgnoreInstructions?: string
settings?: SystemPromptSettings
disableDefaultRules?: boolean
} = {},
): Promise<string> {
const sections = []
@ -472,19 +473,23 @@ export async function addCustomInstructions(
rules.push(options.rooIgnoreInstructions)
}
// Add AGENTS.md content if enabled (default: true)
// Load from root and optionally subdirectories with .roo folders based on enableSubfolderRules setting
if (options.settings?.useAgentRules !== false) {
const agentRulesContent = await loadAllAgentRulesFiles(cwd, enableSubfolderRules)
if (agentRulesContent && agentRulesContent.trim()) {
rules.push(agentRulesContent.trim())
// When disableDefaultRules is true, skip AGENTS.md and generic rules —
// only mode-specific rules (loaded above) are included.
if (!options.disableDefaultRules) {
// Add AGENTS.md content if enabled (default: true)
// Load from root and optionally subdirectories with .roo folders based on enableSubfolderRules setting
if (options.settings?.useAgentRules !== false) {
const agentRulesContent = await loadAllAgentRulesFiles(cwd, enableSubfolderRules)
if (agentRulesContent && agentRulesContent.trim()) {
rules.push(agentRulesContent.trim())
}
}
}
// Add generic rules
const genericRuleContent = await loadRuleFiles(cwd, enableSubfolderRules)
if (genericRuleContent && genericRuleContent.trim()) {
rules.push(genericRuleContent.trim())
// Add generic rules
const genericRuleContent = await loadRuleFiles(cwd, enableSubfolderRules)
if (genericRuleContent && genericRuleContent.trim()) {
rules.push(genericRuleContent.trim())
}
}
if (rules.length > 0) {

View file

@ -104,6 +104,7 @@ ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "",
language: language ?? formatLanguage(vscode.env.language),
rooIgnoreInstructions,
settings,
disableDefaultRules: modeConfig.disableDefaultRules,
})}`
return basePrompt

View file

@ -1293,6 +1293,33 @@ const ModesView = () => {
</div>
</div>
{/* Disable default rules toggle - only for custom modes */}
{findModeBySlug(visualMode, customModes) && (
<div className="mb-2">
<VSCodeCheckbox
checked={findModeBySlug(visualMode, customModes)?.disableDefaultRules ?? false}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target = e.target as HTMLInputElement
const customMode = findModeBySlug(visualMode, customModes)
if (customMode) {
updateCustomMode(visualMode, {
...customMode,
disableDefaultRules: target.checked,
source: customMode.source || "global",
})
}
}}
data-testid="disable-default-rules-checkbox">
<span className="font-medium">{t("prompts:disableDefaultRules.label")}</span>
</VSCodeCheckbox>
<div className="text-xs text-vscode-descriptionForeground mt-1 ml-6">
{t("prompts:disableDefaultRules.description", {
slug: getCurrentMode()?.slug || "mode",
})}
</div>
</div>
)}
<div className="pb-4 border-b border-vscode-input-border">
<div className="flex gap-2 mb-4">
<Button

View file

@ -51,6 +51,10 @@
"description": "Add behavioral guidelines specific to {{modeName}} mode.",
"loadFromFile": "Custom instructions specific to {{mode}} mode can also be loaded from the <span>.roo/rules-{{slug}}/</span> folder in your workspace or from the global <0>.roo/rules-{{slug}}/</0> (.roorules-{{slug}} and .clinerules-{{slug}} are deprecated and will stop working soon)."
},
"disableDefaultRules": {
"label": "Disable default rules",
"description": "When enabled, only mode-specific rules (.roo/rules-{{slug}}/) are loaded. Default rules (.roo/rules/, AGENTS.md, .roorules) are skipped."
},
"exportMode": {
"title": "Export Mode",
"description": "Export this mode with rules from the .roo/rules-{{slug}}/ folder combined into a shareable YAML file. The original files remain unchanged.",