Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
56 changes: 56 additions & 0 deletions src/core/config/__tests__/ModeConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
93 changes: 93 additions & 0 deletions src/core/prompts/sections/__tests__/custom-instructions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
27 changes: 16 additions & 11 deletions src/core/prompts/sections/custom-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ export async function addCustomInstructions(
language?: string
rooIgnoreInstructions?: string
settings?: SystemPromptSettings
disableDefaultRules?: boolean
} = {},
): Promise<string> {
const sections = []
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "",
language: language ?? formatLanguage(vscode.env.language),
rooIgnoreInstructions,
settings,
disableDefaultRules: modeConfig.disableDefaultRules,
})}`

return basePrompt
Expand Down
27 changes: 27 additions & 0 deletions webview-ui/src/components/modes/ModesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/ca/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/de/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/es/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/fr/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/hi/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/id/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/it/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/ja/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/ko/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/nl/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/pl/prompts.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading