@@ -2,6 +2,7 @@ import fs from "fs/promises"
22import path from "path"
33import * as os from "os"
44import { Dirent } from "fs"
5+ import { createHash } from "crypto"
56
67import { isLanguage } from "@roo-code/types"
78
@@ -10,6 +11,42 @@ import type { SystemPromptSettings } from "../types"
1011import { LANGUAGES } from "../../../shared/language"
1112import { getRooDirectoriesForCwd , getGlobalRooDirectory } from "../../../services/roo-config"
1213
14+ /**
15+ * File tracking system to prevent duplicate inclusions
16+ */
17+ interface FileTracker {
18+ resolvedPaths : Set < string >
19+ contentHashes : Set < string >
20+ }
21+
22+ /**
23+ * Create MD5 hash of file content
24+ */
25+ function createContentHash ( content : string ) : string {
26+ return createHash ( "md5" ) . update ( content , "utf8" ) . digest ( "hex" )
27+ }
28+
29+ /**
30+ * Check if a file should be included based on resolved path and content hash
31+ */
32+ function shouldIncludeFile ( resolvedPath : string , content : string , tracker : FileTracker ) : boolean {
33+ // Skip if we've already included this resolved path
34+ if ( tracker . resolvedPaths . has ( resolvedPath ) ) {
35+ return false
36+ }
37+
38+ // Skip if we've already included content with this hash
39+ const contentHash = createContentHash ( content )
40+ if ( tracker . contentHashes . has ( contentHash ) ) {
41+ return false
42+ }
43+
44+ // Add to tracking sets
45+ tracker . resolvedPaths . add ( resolvedPath )
46+ tracker . contentHashes . add ( contentHash )
47+ return true
48+ }
49+
1350/**
1451 * Safely read a file and return its trimmed content
1552 */
@@ -108,7 +145,10 @@ async function resolveSymLink(
108145/**
109146 * Read all text files from a directory in alphabetical order
110147 */
111- async function readTextFilesFromDirectory ( dirPath : string ) : Promise < Array < { filename : string ; content : string } > > {
148+ async function readTextFilesFromDirectory (
149+ dirPath : string ,
150+ tracker : FileTracker ,
151+ ) : Promise < Array < { filename : string ; content : string } > > {
112152 try {
113153 const entries = await fs . readdir ( dirPath , { withFileTypes : true , recursive : true } )
114154
@@ -136,6 +176,12 @@ async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ file
136176 return null
137177 }
138178 const content = await safeReadFile ( resolvedPath )
179+
180+ // Skip if file was already included or content is duplicate
181+ if ( ! shouldIncludeFile ( resolvedPath , content , tracker ) ) {
182+ return null
183+ }
184+
139185 // Use resolvedPath for display to maintain existing behavior
140186 return { filename : resolvedPath , content, sortKey : originalPath }
141187 }
@@ -182,15 +228,21 @@ function formatDirectoryContent(dirPath: string, files: Array<{ filename: string
182228 * Load rule files from global and project-local directories
183229 * Global rules are loaded first, then project-local rules which can override global ones
184230 */
185- export async function loadRuleFiles ( cwd : string ) : Promise < string > {
231+ export async function loadRuleFiles ( cwd : string , tracker ?: FileTracker ) : Promise < string > {
232+ // Create a default tracker if none provided (for backward compatibility with tests)
233+ const fileTracker = tracker || {
234+ resolvedPaths : new Set < string > ( ) ,
235+ contentHashes : new Set < string > ( ) ,
236+ }
237+
186238 const rules : string [ ] = [ ]
187239 const rooDirectories = getRooDirectoriesForCwd ( cwd )
188240
189241 // Check for .roo/rules/ directories in order (global first, then project-local)
190242 for ( const rooDir of rooDirectories ) {
191243 const rulesDir = path . join ( rooDir , "rules" )
192244 if ( await directoryExists ( rulesDir ) ) {
193- const files = await readTextFilesFromDirectory ( rulesDir )
245+ const files = await readTextFilesFromDirectory ( rulesDir , fileTracker )
194246 if ( files . length > 0 ) {
195247 const content = formatDirectoryContent ( rulesDir , files )
196248 rules . push ( content )
@@ -207,8 +259,9 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
207259 const ruleFiles = [ ".roorules" , ".clinerules" ]
208260
209261 for ( const file of ruleFiles ) {
210- const content = await safeReadFile ( path . join ( cwd , file ) )
211- if ( content ) {
262+ const filePath = path . join ( cwd , file )
263+ const content = await safeReadFile ( filePath )
264+ if ( content && shouldIncludeFile ( filePath , content , fileTracker ) ) {
212265 return `\n# Rules from ${ file } :\n${ content } \n`
213266 }
214267 }
@@ -220,7 +273,7 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
220273 * Load AGENTS.md or AGENT.md file from the project root if it exists
221274 * Checks for both AGENTS.md (standard) and AGENT.md (alternative) for compatibility
222275 */
223- async function loadAgentRulesFile ( cwd : string ) : Promise < string > {
276+ async function loadAgentRulesFile ( cwd : string , tracker : FileTracker ) : Promise < string > {
224277 // Try both filenames - AGENTS.md (standard) first, then AGENT.md (alternative)
225278 const filenames = [ "AGENTS.md" , "AGENT.md" ]
226279
@@ -251,7 +304,7 @@ async function loadAgentRulesFile(cwd: string): Promise<string> {
251304
252305 // Read the content from the resolved path
253306 const content = await safeReadFile ( resolvedPath )
254- if ( content ) {
307+ if ( content && shouldIncludeFile ( resolvedPath , content , tracker ) ) {
255308 return `# Agent Rules Standard (${ filename } ):\n${ content } `
256309 }
257310 } catch ( err ) {
@@ -274,6 +327,12 @@ export async function addCustomInstructions(
274327) : Promise < string > {
275328 const sections = [ ]
276329
330+ // Create file tracker to prevent duplicates
331+ const fileTracker : FileTracker = {
332+ resolvedPaths : new Set < string > ( ) ,
333+ contentHashes : new Set < string > ( ) ,
334+ }
335+
277336 // Load mode-specific rules if mode is provided
278337 let modeRuleContent = ""
279338 let usedRuleFile = ""
@@ -286,7 +345,7 @@ export async function addCustomInstructions(
286345 for ( const rooDir of rooDirectories ) {
287346 const modeRulesDir = path . join ( rooDir , `rules-${ mode } ` )
288347 if ( await directoryExists ( modeRulesDir ) ) {
289- const files = await readTextFilesFromDirectory ( modeRulesDir )
348+ const files = await readTextFilesFromDirectory ( modeRulesDir , fileTracker )
290349 if ( files . length > 0 ) {
291350 const content = formatDirectoryContent ( modeRulesDir , files )
292351 modeRules . push ( content )
@@ -301,13 +360,17 @@ export async function addCustomInstructions(
301360 } else {
302361 // Fall back to existing behavior for legacy files
303362 const rooModeRuleFile = `.roorules-${ mode } `
304- modeRuleContent = await safeReadFile ( path . join ( cwd , rooModeRuleFile ) )
305- if ( modeRuleContent ) {
363+ const rooModeRuleFilePath = path . join ( cwd , rooModeRuleFile )
364+ const rooModeContent = await safeReadFile ( rooModeRuleFilePath )
365+ if ( rooModeContent && shouldIncludeFile ( rooModeRuleFilePath , rooModeContent , fileTracker ) ) {
366+ modeRuleContent = rooModeContent
306367 usedRuleFile = rooModeRuleFile
307368 } else {
308369 const clineModeRuleFile = `.clinerules-${ mode } `
309- modeRuleContent = await safeReadFile ( path . join ( cwd , clineModeRuleFile ) )
310- if ( modeRuleContent ) {
370+ const clineModeRuleFilePath = path . join ( cwd , clineModeRuleFile )
371+ const clineModeContent = await safeReadFile ( clineModeRuleFilePath )
372+ if ( clineModeContent && shouldIncludeFile ( clineModeRuleFilePath , clineModeContent , fileTracker ) ) {
373+ modeRuleContent = clineModeContent
311374 usedRuleFile = clineModeRuleFile
312375 }
313376 }
@@ -350,14 +413,14 @@ export async function addCustomInstructions(
350413
351414 // Add AGENTS.md content if enabled (default: true)
352415 if ( options . settings ?. useAgentRules !== false ) {
353- const agentRulesContent = await loadAgentRulesFile ( cwd )
416+ const agentRulesContent = await loadAgentRulesFile ( cwd , fileTracker )
354417 if ( agentRulesContent && agentRulesContent . trim ( ) ) {
355418 rules . push ( agentRulesContent . trim ( ) )
356419 }
357420 }
358421
359422 // Add generic rules
360- const genericRuleContent = await loadRuleFiles ( cwd )
423+ const genericRuleContent = await loadRuleFiles ( cwd , fileTracker )
361424 if ( genericRuleContent && genericRuleContent . trim ( ) ) {
362425 rules . push ( genericRuleContent . trim ( ) )
363426 }
0 commit comments