diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index f981ba7bf9a..08dfbee0e02 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -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 diff --git a/src/core/config/__tests__/ModeConfig.spec.ts b/src/core/config/__tests__/ModeConfig.spec.ts index 74cbc0c4373..02b59b174a0 100644 --- a/src/core/config/__tests__/ModeConfig.spec.ts +++ b/src/core/config/__tests__/ModeConfig.spec.ts @@ -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) + }) + }) }) diff --git a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts index 68fa2d37f5a..30775df860a 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts +++ b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts @@ -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") + }) }) diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 46cf1bf1f9e..1ccff4663d7 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -388,6 +388,7 @@ export async function addCustomInstructions( language?: string rooIgnoreInstructions?: string settings?: SystemPromptSettings + disableDefaultRules?: boolean } = {}, ): Promise { 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) { diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 0d6071644a9..8386e76e311 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -104,6 +104,7 @@ ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions, settings, + disableDefaultRules: modeConfig.disableDefaultRules, })}` return basePrompt diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index eeeaf026cc2..298dd0e5689 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -1293,6 +1293,33 @@ const ModesView = () => { + {/* Disable default rules toggle - only for custom modes */} + {findModeBySlug(visualMode, customModes) && ( +
+ ) => { + 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"> + {t("prompts:disableDefaultRules.label")} + +
+ {t("prompts:disableDefaultRules.description", { + slug: getCurrentMode()?.slug || "mode", + })} +
+
+ )} +