mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
d8cfbfdb05
commit
7686d288a6
7 changed files with 198 additions and 11 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "",
|
|||
language: language ?? formatLanguage(vscode.env.language),
|
||||
rooIgnoreInstructions,
|
||||
settings,
|
||||
disableDefaultRules: modeConfig.disableDefaultRules,
|
||||
})}`
|
||||
|
||||
return basePrompt
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue