Skip to content

Commit 580672f

Browse files
upamunemrubens
andauthored
feat: enhance rule file loading with .roo/rules directory support (#2354)
* feat: enhance rule file loading with .roo/rules directory support - Introduced functions to safely read files and check for directory existence. - Added capability to read all text files from a specified directory in alphabetical order. - Updated `loadRuleFiles` to prioritize loading rules from a `.roo/rules/` directory, falling back to existing rule files if necessary. - Enhanced `addCustomInstructions` to support loading mode-specific rules from a `.roo/rules-{mode}/` directory, improving flexibility in rule management. This change improves the organization and retrieval of rule files, allowing for better modularity and maintainability. * Updated strings and translations * Add tests * Revert changes to system prompt translations * Fix path resolution * Make instruction structure clearer --------- Co-authored-by: Matt Rubens <[email protected]>
1 parent 0e4be83 commit 580672f

File tree

18 files changed

+563
-83
lines changed

18 files changed

+563
-83
lines changed

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

Lines changed: 419 additions & 34 deletions
Large diffs are not rendered by default.

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

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import path from "path"
33

44
import { LANGUAGES, isLanguage } from "../../../shared/language"
55

6+
/**
7+
* Safely read a file and return its trimmed content
8+
*/
69
async function safeReadFile(filePath: string): Promise<string> {
710
try {
811
const content = await fs.readFile(filePath, "utf-8")
@@ -16,7 +19,81 @@ async function safeReadFile(filePath: string): Promise<string> {
1619
}
1720
}
1821

22+
/**
23+
* Check if a directory exists
24+
*/
25+
async function directoryExists(dirPath: string): Promise<boolean> {
26+
try {
27+
const stats = await fs.stat(dirPath)
28+
return stats.isDirectory()
29+
} catch (err) {
30+
return false
31+
}
32+
}
33+
34+
/**
35+
* Read all text files from a directory in alphabetical order
36+
*/
37+
async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ filename: string; content: string }>> {
38+
try {
39+
const files = await fs
40+
.readdir(dirPath, { withFileTypes: true, recursive: true })
41+
.then((files) => files.filter((file) => file.isFile()))
42+
.then((files) => files.map((file) => path.resolve(dirPath, file.name)))
43+
44+
const fileContents = await Promise.all(
45+
files.map(async (file) => {
46+
try {
47+
// Check if it's a file (not a directory)
48+
const stats = await fs.stat(file)
49+
if (stats.isFile()) {
50+
const content = await safeReadFile(file)
51+
return { filename: file, content }
52+
}
53+
return null
54+
} catch (err) {
55+
return null
56+
}
57+
}),
58+
)
59+
60+
// Filter out null values (directories or failed reads)
61+
return fileContents.filter((item): item is { filename: string; content: string } => item !== null)
62+
} catch (err) {
63+
return []
64+
}
65+
}
66+
67+
/**
68+
* Format content from multiple files with filenames as headers
69+
*/
70+
function formatDirectoryContent(dirPath: string, files: Array<{ filename: string; content: string }>): string {
71+
if (files.length === 0) return ""
72+
73+
return (
74+
"\n\n" +
75+
files
76+
.map((file) => {
77+
return `# Rules from ${file.filename}:\n${file.content}:`
78+
})
79+
.join("\n\n")
80+
)
81+
}
82+
83+
/**
84+
* Load rule files from the specified directory
85+
*/
1986
export async function loadRuleFiles(cwd: string): Promise<string> {
87+
// Check for .roo/rules/ directory
88+
const rooRulesDir = path.join(cwd, ".roo", "rules")
89+
if (await directoryExists(rooRulesDir)) {
90+
const files = await readTextFilesFromDirectory(rooRulesDir)
91+
if (files.length > 0) {
92+
return formatDirectoryContent(rooRulesDir, files)
93+
}
94+
}
95+
96+
// Fall back to existing behavior
2097
const ruleFiles = [".roorules", ".clinerules"]
2198

2299
for (const file of ruleFiles) {
@@ -41,16 +118,30 @@ export async function addCustomInstructions(
41118
// Load mode-specific rules if mode is provided
42119
let modeRuleContent = ""
43120
let usedRuleFile = ""
121+
44122
if (mode) {
45-
const rooModeRuleFile = `.roorules-${mode}`
46-
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
47-
if (modeRuleContent) {
48-
usedRuleFile = rooModeRuleFile
49-
} else {
50-
const clineModeRuleFile = `.clinerules-${mode}`
51-
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
123+
// Check for .roo/rules-${mode}/ directory
124+
const modeRulesDir = path.join(cwd, ".roo", `rules-${mode}`)
125+
if (await directoryExists(modeRulesDir)) {
126+
const files = await readTextFilesFromDirectory(modeRulesDir)
127+
if (files.length > 0) {
128+
modeRuleContent = formatDirectoryContent(modeRulesDir, files)
129+
usedRuleFile = modeRulesDir
130+
}
131+
}
132+
133+
// If no directory exists, fall back to existing behavior
134+
if (!modeRuleContent) {
135+
const rooModeRuleFile = `.roorules-${mode}`
136+
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
52137
if (modeRuleContent) {
53-
usedRuleFile = clineModeRuleFile
138+
usedRuleFile = rooModeRuleFile
139+
} else {
140+
const clineModeRuleFile = `.clinerules-${mode}`
141+
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
142+
if (modeRuleContent) {
143+
usedRuleFile = clineModeRuleFile
144+
}
54145
}
55146
}
56147
}
@@ -78,7 +169,11 @@ export async function addCustomInstructions(
78169

79170
// Add mode-specific rules first if they exist
80171
if (modeRuleContent && modeRuleContent.trim()) {
81-
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
172+
if (usedRuleFile.includes(path.join(".roo", `rules-${mode}`))) {
173+
rules.push(modeRuleContent.trim())
174+
} else {
175+
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
176+
}
82177
}
83178

84179
if (options.rooIgnoreInstructions) {

webview-ui/src/components/prompts/PromptsView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
798798
// Open or create an empty file
799799
vscode.postMessage({
800800
type: "openFile",
801-
text: `./.roorules-${currentMode.slug}`,
801+
text: `./.roo/rules-${currentMode.slug}/rules.md`,
802802
values: {
803803
create: true,
804804
content: "",
@@ -935,7 +935,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
935935
onClick={() =>
936936
vscode.postMessage({
937937
type: "openFile",
938-
text: "./.roorules",
938+
text: "./.roo/rules/rules.md",
939939
values: {
940940
create: true,
941941
content: "",

webview-ui/src/i18n/locales/ca/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Instruccions personalitzades específiques del mode (opcional)",
3737
"resetToDefault": "Restablir a valors predeterminats",
3838
"description": "Afegiu directrius de comportament específiques per al mode {{modeName}}.",
39-
"loadFromFile": "Les instruccions personalitzades específiques per al mode {{mode}} també es poden carregar des de <span>.roorules-{{slug}}</span> al vostre espai de treball (.clinerules-{{slug}} està obsolet i deixarà de funcionar aviat)."
39+
"loadFromFile": "Les instruccions personalitzades específiques per al mode {{mode}} també es poden carregar des de la carpeta <span>.roo/rules-{{slug}}/</span> al vostre espai de treball (.roorules-{{slug}} i .clinerules-{{slug}} estan obsolets i deixaran de funcionar aviat)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Instruccions personalitzades per a tots els modes",
4343
"description": "Aquestes instruccions s'apliquen a tots els modes. Proporcionen un conjunt bàsic de comportaments que es poden millorar amb instruccions específiques de cada mode a continuació.\nSi voleu que Roo pensi i parli en un idioma diferent al de la visualització del vostre editor ({{language}}), podeu especificar-ho aquí.",
44-
"loadFromFile": "Les instruccions també es poden carregar des de <span>.roorules</span> al vostre espai de treball (.clinerules està obsolet i deixarà de funcionar aviat)."
44+
"loadFromFile": "Les instruccions també es poden carregar des de la carpeta <span>.roo/rules/</span> al vostre espai de treball (.roorules i .clinerules estan obsolets i deixaran de funcionar aviat)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Previsualització del prompt del sistema",

webview-ui/src/i18n/locales/de/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Modusspezifische benutzerdefinierte Anweisungen (optional)",
3737
"resetToDefault": "Auf Standardwerte zurücksetzen",
3838
"description": "Fügen Sie verhaltensspezifische Richtlinien für den Modus {{modeName}} hinzu.",
39-
"loadFromFile": "Benutzerdefinierte Anweisungen für den Modus {{mode}} können auch aus <span>.roorules-{{slug}}</span> in deinem Arbeitsbereich geladen werden (.clinerules-{{slug}} ist veraltet und wird bald nicht mehr funktionieren)."
39+
"loadFromFile": "Benutzerdefinierte Anweisungen für den Modus {{mode}} können auch aus dem Ordner <span>.roo/rules-{{slug}}/</span> in deinem Arbeitsbereich geladen werden (.roorules-{{slug}} und .clinerules-{{slug}} sind veraltet und werden bald nicht mehr funktionieren)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Benutzerdefinierte Anweisungen für alle Modi",
4343
"description": "Diese Anweisungen gelten für alle Modi. Sie bieten einen grundlegenden Satz von Verhaltensweisen, die durch modusspezifische Anweisungen unten erweitert werden können.\nWenn du möchtest, dass Roo in einer anderen Sprache als deiner Editor-Anzeigesprache ({{language}}) denkt und spricht, kannst du das hier angeben.",
44-
"loadFromFile": "Anweisungen können auch aus <span>.roorules</span> in deinem Arbeitsbereich geladen werden (.clinerules ist veraltet und wird bald nicht mehr funktionieren)."
44+
"loadFromFile": "Anweisungen können auch aus dem Ordner <span>.roo/rules/</span> in deinem Arbeitsbereich geladen werden (.roorules und .clinerules sind veraltet und werden bald nicht mehr funktionieren)."
4545
},
4646
"systemPrompt": {
4747
"preview": "System-Prompt Vorschau",

webview-ui/src/i18n/locales/en/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Mode-specific Custom Instructions (optional)",
3737
"resetToDefault": "Reset to default",
3838
"description": "Add behavioral guidelines specific to {{modeName}} mode.",
39-
"loadFromFile": "Custom instructions specific to {{mode}} mode can also be loaded from <span>.roorules-{{slug}}</span> in your workspace (.clinerules-{{slug}} is deprecated and will stop working soon)."
39+
"loadFromFile": "Custom instructions specific to {{mode}} mode can also be loaded from the <span>.roo/rules-{{slug}}/</span> folder in your workspace (.roorules-{{slug}} and .clinerules-{{slug}} are deprecated and will stop working soon)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Custom Instructions for All Modes",
4343
"description": "These instructions apply to all modes. They provide a base set of behaviors that can be enhanced by mode-specific instructions below.\nIf you would like Roo to think and speak in a different language than your editor display language ({{language}}), you can specify it here.",
44-
"loadFromFile": "Instructions can also be loaded from <span>.roorules</span> in your workspace (.clinerules is deprecated and will stop working soon)."
44+
"loadFromFile": "Instructions can also be loaded from the <span>.roo/rules/</span> folder in your workspace (.roorules and .clinerules are deprecated and will stop working soon)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Preview System Prompt",

webview-ui/src/i18n/locales/es/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Instrucciones personalizadas para el modo (opcional)",
3737
"resetToDefault": "Restablecer a valores predeterminados",
3838
"description": "Agrega directrices de comportamiento específicas para el modo {{modeName}}.",
39-
"loadFromFile": "Las instrucciones personalizadas para el modo {{mode}} también se pueden cargar desde <span>.roorules-{{slug}}</span> en tu espacio de trabajo (.clinerules-{{slug}} está obsoleto y dejará de funcionar pronto)."
39+
"loadFromFile": "Las instrucciones personalizadas para el modo {{mode}} también se pueden cargar desde la carpeta <span>.roo/rules-{{slug}}/</span> en tu espacio de trabajo (.roorules-{{slug}} y .clinerules-{{slug}} están obsoletos y dejarán de funcionar pronto)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Instrucciones personalizadas para todos los modos",
4343
"description": "Estas instrucciones se aplican a todos los modos. Proporcionan un conjunto base de comportamientos que pueden ser mejorados por instrucciones específicas de cada modo.\nSi quieres que Roo piense y hable en un idioma diferente al idioma de visualización de tu editor ({{language}}), puedes especificarlo aquí.",
44-
"loadFromFile": "Las instrucciones también se pueden cargar desde <span>.roorules</span> en tu espacio de trabajo (.clinerules está obsoleto y dejará de funcionar pronto)."
44+
"loadFromFile": "Las instrucciones también se pueden cargar desde la carpeta <span>.roo/rules/</span> en tu espacio de trabajo (.roorules y .clinerules están obsoletos y dejarán de funcionar pronto)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Vista previa de la solicitud del sistema",

webview-ui/src/i18n/locales/fr/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Instructions personnalisées spécifiques au mode (optionnel)",
3737
"resetToDefault": "Réinitialiser aux valeurs par défaut",
3838
"description": "Ajoutez des directives comportementales spécifiques au mode {{modeName}}.",
39-
"loadFromFile": "Les instructions personnalisées spécifiques au mode {{mode}} peuvent également être chargées depuis <span>.roorules-{{slug}}</span> dans votre espace de travail (.clinerules-{{slug}} est obsolète et cessera de fonctionner bientôt)."
39+
"loadFromFile": "Les instructions personnalisées spécifiques au mode {{mode}} peuvent également être chargées depuis le dossier <span>.roo/rules-{{slug}}/</span> dans votre espace de travail (.roorules-{{slug}} et .clinerules-{{slug}} sont obsolètes et cesseront de fonctionner bientôt)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Instructions personnalisées pour tous les modes",
4343
"description": "Ces instructions s'appliquent à tous les modes. Elles fournissent un ensemble de comportements de base qui peuvent être améliorés par des instructions spécifiques au mode ci-dessous.\nSi vous souhaitez que Roo pense et parle dans une langue différente de celle de votre éditeur ({{language}}), vous pouvez le spécifier ici.",
44-
"loadFromFile": "Les instructions peuvent également être chargées depuis <span>.roorules</span> dans votre espace de travail (.clinerules est obsolète et cessera de fonctionner bientôt)."
44+
"loadFromFile": "Les instructions peuvent également être chargées depuis le dossier <span>.roo/rules/</span> dans votre espace de travail (.roorules et .clinerules sont obsolètes et cesseront de fonctionner bientôt)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Aperçu du prompt système",

webview-ui/src/i18n/locales/hi/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "मोड-विशिष्ट कस्टम निर्देश (वैकल्पिक)",
3737
"resetToDefault": "डिफ़ॉल्ट पर रीसेट करें",
3838
"description": "{{modeName}} मोड के लिए विशिष्ट व्यवहार दिशानिर्देश जोड़ें।",
39-
"loadFromFile": "{{mode}} मोड के लिए विशिष्ट कस्टम निर्देश आपके वर्कस्पेस में <span>.roorules-{{slug}}</span> से भी लोड किए जा सकते हैं (.clinerules-{{slug}} पुराना हो गया है और जल्द ही काम करना बंद कर देगा)।"
39+
"loadFromFile": "{{mode}} मोड के लिए विशिष्ट कस्टम निर्देश आपके वर्कस्पेस में <span>.roo/rules-{{slug}}/</span> फ़ोल्डर से भी लोड किए जा सकते हैं (.roorules-{{slug}} और .clinerules-{{slug}} पुराने हो गए हैं और जल्द ही काम करना बंद कर देंगे)।"
4040
},
4141
"globalCustomInstructions": {
4242
"title": "सभी मोड्स के लिए कस्टम निर्देश",
4343
"description": "ये निर्देश सभी मोड्स पर लागू होते हैं। वे व्यवहारों का एक आधार सेट प्रदान करते हैं जिन्हें नीचे दिए गए मोड-विशिष्ट निर्देशों द्वारा बढ़ाया जा सकता है।\nयदि आप चाहते हैं कि Roo आपके एडिटर की प्रदर्शन भाषा ({{language}}) से अलग भाषा में सोचे और बोले, तो आप यहां इसे निर्दिष्ट कर सकते हैं।",
44-
"loadFromFile": "निर्देश आपके वर्कस्पेस में <span>.roorules</span> से भी लोड किए जा सकते हैं (.clinerules पुराना हो गया है और जल्द ही काम करना बंद कर देगा)।"
44+
"loadFromFile": "निर्देश आपके वर्कस्पेस में <span>.roo/rules/</span> फ़ोल्डर से भी लोड किए जा सकते हैं (.roorules और .clinerules पुराने हो गए हैं और जल्द ही काम करना बंद कर देंगे)।"
4545
},
4646
"systemPrompt": {
4747
"preview": "सिस्टम प्रॉम्प्ट का पूर्वावलोकन",

webview-ui/src/i18n/locales/it/prompts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"title": "Istruzioni personalizzate specifiche per la modalità (opzionale)",
3737
"resetToDefault": "Ripristina predefiniti",
3838
"description": "Aggiungi linee guida comportamentali specifiche per la modalità {{modeName}}.",
39-
"loadFromFile": "Le istruzioni personalizzate specifiche per la modalità {{mode}} possono essere caricate anche da <span>.roorules-{{slug}}</span> nel tuo spazio di lavoro (.clinerules-{{slug}} è obsoleto e smetterà di funzionare presto)."
39+
"loadFromFile": "Le istruzioni personalizzate specifiche per la modalità {{mode}} possono essere caricate anche dalla cartella <span>.roo/rules-{{slug}}/</span> nel tuo spazio di lavoro (.roorules-{{slug}} e .clinerules-{{slug}} sono obsoleti e smetteranno di funzionare presto)."
4040
},
4141
"globalCustomInstructions": {
4242
"title": "Istruzioni personalizzate per tutte le modalità",
4343
"description": "Queste istruzioni si applicano a tutte le modalità. Forniscono un insieme base di comportamenti che possono essere migliorati dalle istruzioni specifiche per modalità qui sotto.\nSe desideri che Roo pensi e parli in una lingua diversa dalla lingua di visualizzazione del tuo editor ({{language}}), puoi specificarlo qui.",
44-
"loadFromFile": "Le istruzioni possono essere caricate anche da <span>.roorules</span> nel tuo spazio di lavoro (.clinerules è obsoleto e smetterà di funzionare presto)."
44+
"loadFromFile": "Le istruzioni possono essere caricate anche dalla cartella <span>.roo/rules/</span> nel tuo spazio di lavoro (.roorules e .clinerules sono obsoleti e smetteranno di funzionare presto)."
4545
},
4646
"systemPrompt": {
4747
"preview": "Anteprima prompt di sistema",

0 commit comments

Comments
 (0)