feat: Implement intelligent semantic rule injection for context-aware rule selection

- Add SmartRule interface and types for rule metadata
- Implement smart rule loading from .roo/smart-rules directories
- Create semantic matching using Jaccard similarity algorithm
- Integrate smart rules into prompt generation system
- Add VSCode configuration settings for smart rules
- Support mode-specific smart rules directories
- Add comprehensive unit tests for all functionality
- Update documentation with smart rules usage

Fixes #6707
This commit is contained in:
Roo Code 2025-08-05 10:08:16 +00:00
parent d90bab71ff
commit 000dcda56e
12 changed files with 1596 additions and 12 deletions

260
docs/smart-rules.md Normal file
View file

@ -0,0 +1,260 @@
# Smart Rules - Intelligent Semantic Rule Injection
Smart Rules is a feature that automatically selects and injects relevant rules into the AI context based on the current task, dramatically improving token efficiency and response quality.
## Overview
Traditional rule systems inject all rules into every conversation, leading to:
- **Token waste**: Irrelevant rules consume valuable context tokens
- **Context pollution**: Unrelated rules can mislead AI responses
- **Poor scalability**: Rule overhead grows linearly with project complexity
- **Limited rule depth**: Users avoid detailed rules due to constant overhead
Smart Rules solves these problems by intelligently matching user queries against rule triggers and only including relevant rules.
## How It Works
1. **Rule Definition**: Create markdown files with YAML frontmatter specifying when rules should apply
2. **Semantic Matching**: The system analyzes user queries and calculates similarity scores against rule triggers
3. **Automatic Selection**: Only rules exceeding the similarity threshold are included in the context
4. **Dependency Resolution**: Rules can specify dependencies that are automatically included
## Creating Smart Rules
### File Structure
Smart rules are stored in special directories within your `.roo` folder:
```
.roo/
├── smart-rules/ # General smart rules
│ ├── supabase.md
│ ├── testing.md
│ └── api-design.md
└── smart-rules-{mode}/ # Mode-specific smart rules
├── database.md
└── frontend.md
```
### Rule Format
Each smart rule is a markdown file with YAML frontmatter:
````markdown
---
use-when: "interacting with Supabase client, database queries, or authentication"
priority: 10
dependencies:
- "typescript.md"
---
# Supabase Best Practices
When working with Supabase:
## Authentication
- Always use TypeScript for better type safety
- Prefer RLS policies over client-side filtering
- Store sensitive configuration in environment variables
## Database Queries
```typescript
// Good: Type-safe query with error handling
const { data, error } = await supabase.from<User>("users").select("*").eq("id", userId).single()
if (error) throw error
```
````
## Real-time Subscriptions
- Always clean up subscriptions in useEffect cleanup
- Use proper TypeScript types for real-time payloads
````
### Frontmatter Fields
- **use-when** (required): Description of when this rule should be applied. This is matched semantically against user queries.
- **priority** (optional): Higher priority rules are selected first when multiple rules match (default: 0)
- **dependencies** (optional): Array of other rule filenames that should be included when this rule is selected
## Configuration
Smart Rules can be configured in VS Code settings:
```json
{
"roo-cline.smartRules.enabled": true,
"roo-cline.smartRules.minSimilarity": 0.7,
"roo-cline.smartRules.maxRules": 5,
"roo-cline.smartRules.showSelectedRules": false,
"roo-cline.smartRules.debugRuleSelection": false
}
````
### Settings
- **enabled**: Enable/disable smart rules functionality
- **minSimilarity**: Minimum similarity score (0-1) for rule selection. Lower values include more rules.
- **maxRules**: Maximum number of smart rules to include in a single prompt
- **showSelectedRules**: Display which rules were selected in the UI
- **debugRuleSelection**: Enable detailed logging of the rule selection process
## Examples
### Example 1: Database Operations
**User Query**: "Set up Supabase authentication in my Next.js app"
**Selected Rules**:
1. `supabase.md` (score: 0.85) - Matched "Supabase" and "authentication"
2. `nextjs-app-router.md` (score: 0.72) - Matched "Next.js app"
3. `typescript.md` (score: 0.70) - Included as dependency of supabase.md
### Example 2: API Development
**User Query**: "Create a REST API endpoint for user management"
**Selected Rules**:
1. `api-design.md` (score: 0.88) - Matched "REST API" and "endpoint"
2. `testing.md` (score: 0.70) - Included as dependency
3. `validation.md` (score: 0.71) - Matched "user management"
## Best Practices
### Writing Effective use-when Triggers
1. **Be Specific**: Include key terms and phrases that users would naturally use
```yaml
# Good
use-when: "working with React hooks, useState, useEffect, or custom hooks"
# Too vague
use-when: "React development"
```
2. **Include Variations**: Account for different ways users might describe the same task
```yaml
use-when: "database queries, SQL operations, data fetching, or ORM usage"
```
3. **Use Natural Language**: Write triggers as if describing when a human would need the rule
```yaml
use-when: "debugging performance issues, optimizing slow code, or profiling applications"
```
### Organizing Rules
1. **Granular Rules**: Create focused rules for specific topics rather than large, general rules
2. **Use Dependencies**: Link related rules instead of duplicating content
3. **Mode-Specific Rules**: Place mode-specific rules in `smart-rules-{mode}` directories
4. **Prioritize Important Rules**: Use the priority field for rules that should take precedence
### Performance Considerations
1. **Rule Count**: While there's no hard limit, 50-100 well-organized rules perform well
2. **Content Size**: Keep individual rules focused; very large rules still consume tokens when selected
3. **Similarity Threshold**: Adjust `minSimilarity` based on your rule specificity:
- 0.8-1.0: Very strict matching, fewer rules selected
- 0.6-0.8: Balanced matching (recommended)
- 0.4-0.6: Loose matching, more rules selected
## Migration Guide
### From Traditional Rules
1. **Identify Rule Categories**: Group your existing rules by topic or use case
2. **Create Smart Rule Files**: Convert each group into a smart rule with appropriate `use-when` trigger
3. **Add Dependencies**: Link related rules using the dependencies field
4. **Test Selection**: Use debug mode to verify rules are selected appropriately
5. **Adjust Triggers**: Refine `use-when` descriptions based on actual usage
### Example Migration
**Before** (`.roo/rules/database.md`):
```markdown
# Database Rules
Always use prepared statements...
```
**After** (`.roo/smart-rules/database.md`):
```markdown
---
use-when: "database operations, SQL queries, data persistence, or ORM configuration"
priority: 5
---
# Database Rules
Always use prepared statements...
```
## Troubleshooting
### Rules Not Being Selected
1. **Check Similarity Score**: Enable `debugRuleSelection` to see similarity scores
2. **Lower Threshold**: Temporarily reduce `minSimilarity` to test
3. **Improve Triggers**: Add more relevant keywords to `use-when`
4. **Verify File Location**: Ensure rules are in correct directories
### Too Many Rules Selected
1. **Increase Threshold**: Raise `minSimilarity` value
2. **Reduce Max Rules**: Lower `maxRules` setting
3. **Refine Triggers**: Make `use-when` descriptions more specific
### Performance Issues
1. **Check Rule Count**: Large numbers of rules may slow selection
2. **Optimize Content**: Keep rule content focused and concise
3. **Review Dependencies**: Avoid circular or excessive dependencies
## Advanced Usage
### Dynamic Rule Generation
Smart rules can be generated programmatically for large projects:
```javascript
const generateSmartRule = (component, guidelines) => ({
filename: `${component.toLowerCase()}.md`,
frontmatter: {
"use-when": `working with ${component} component, ${component} API, or ${component} configuration`,
priority: component.critical ? 10 : 5,
},
content: guidelines,
})
```
### Integration with CI/CD
Validate smart rules in your pipeline:
```bash
# Check for required frontmatter
find .roo/smart-rules -name "*.md" -exec grep -L "use-when:" {} \;
# Validate YAML frontmatter
for file in .roo/smart-rules/*.md; do
head -n 20 "$file" | sed -n '/^---$/,/^---$/p' | yaml-lint
done
```
## Future Enhancements
- **Machine Learning**: Improve matching using embeddings and vector similarity
- **Rule Analytics**: Track which rules are most frequently selected
- **Auto-generation**: Generate rules from codebase patterns and documentation
- **Rule Sharing**: Community marketplace for smart rule templates

View file

@ -0,0 +1,312 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import fs from "fs/promises"
import path from "path"
import { loadSmartRules, hasSmartRules } from "../smart-rules-loader"
import * as rooConfig from "../../../../services/roo-config"
// Mock the modules
vi.mock("fs/promises", () => ({
default: {
stat: vi.fn(),
readdir: vi.fn(),
readFile: vi.fn(),
},
stat: vi.fn(),
readdir: vi.fn(),
readFile: vi.fn(),
}))
vi.mock("../../../../services/roo-config")
vi.mock("../../../../utils/logging", () => ({
logger: {
error: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
},
}))
describe("smart-rules-loader", () => {
const mockCwd = "/test/project"
const mockGlobalDir = "/home/user/.roo"
const mockProjectDir = "/test/project/.roo"
beforeEach(() => {
vi.clearAllMocks()
// Mock getRooDirectoriesForCwd to return both global and project directories by default
vi.mocked(rooConfig.getRooDirectoriesForCwd).mockReturnValue([mockGlobalDir, mockProjectDir])
})
afterEach(() => {
vi.restoreAllMocks()
})
describe("loadSmartRules", () => {
it("should load smart rules from .roo/smart-rules directory", async () => {
// Mock directory existence
vi.mocked(fs.stat).mockImplementation(async (path) => {
if (path === `${mockProjectDir}/smart-rules`) {
return { isDirectory: () => true } as any
}
throw new Error("Not found")
})
// Mock directory reading
vi.mocked(fs.readdir).mockResolvedValue([
{ name: "rule1.md", isFile: () => true, isDirectory: () => false } as any,
{ name: "rule2.md", isFile: () => true, isDirectory: () => false } as any,
{ name: "not-markdown.txt", isFile: () => true, isDirectory: () => false } as any,
])
// Mock file reading
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if (filePath.toString().includes("rule1.md")) {
return `---
use-when: working with databases
priority: 2
---
# Database Rules
Always use prepared statements`
}
if (filePath.toString().includes("rule2.md")) {
return `---
use-when: writing tests
---
# Testing Rules
Write tests first`
}
return ""
})
const rules = await loadSmartRules(mockCwd)
expect(rules).toHaveLength(2)
expect(rules[0].filename).toBe("rule1.md")
expect(rules[0].useWhen).toBe("working with databases")
expect(rules[0].priority).toBe(2)
expect(rules[0].content).toContain("Always use prepared statements")
expect(rules[1].filename).toBe("rule2.md")
expect(rules[1].useWhen).toBe("writing tests")
expect(rules[1].content).toContain("Write tests first")
})
it("should load mode-specific smart rules", async () => {
// Mock directory existence
vi.mocked(fs.stat).mockImplementation(async (path) => {
if (path === `${mockProjectDir}/smart-rules-code`) {
return { isDirectory: () => true } as any
}
throw new Error("Not found")
})
// Mock directory reading
vi.mocked(fs.readdir).mockResolvedValue([
{ name: "code-rule.md", isFile: () => true, isDirectory: () => false } as any,
])
// Mock file reading
vi.mocked(fs.readFile).mockResolvedValue(`---
use-when: writing code
---
# Code Mode Rules
Follow coding standards`)
const rules = await loadSmartRules(mockCwd, "code")
expect(rules).toHaveLength(1)
expect(rules[0].filename).toBe("code-rule.md")
expect(rules[0].useWhen).toBe("writing code")
})
it("should merge global and project smart rules", async () => {
// Mock directory existence
vi.mocked(fs.stat).mockImplementation(async (path) => {
if (path === `${mockGlobalDir}/smart-rules` || path === `${mockProjectDir}/smart-rules`) {
return { isDirectory: () => true } as any
}
throw new Error("Not found")
})
// Mock directory reading
vi.mocked(fs.readdir).mockImplementation(async (dirPath) => {
if (dirPath === `${mockGlobalDir}/smart-rules`) {
return [{ name: "global-rule.md", isFile: () => true, isDirectory: () => false } as any]
}
if (dirPath === `${mockProjectDir}/smart-rules`) {
return [{ name: "project-rule.md", isFile: () => true, isDirectory: () => false } as any]
}
return []
})
// Mock file reading
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if (filePath.toString().includes("global-rule.md")) {
return `---
use-when: global rule
priority: 1
---
Global content`
}
if (filePath.toString().includes("project-rule.md")) {
return `---
use-when: project rule
priority: 2
---
Project content`
}
return ""
})
const rules = await loadSmartRules(mockCwd)
expect(rules).toHaveLength(2)
// Project rule should come first due to higher priority
expect(rules[0].filename).toBe("project-rule.md")
expect(rules[1].filename).toBe("global-rule.md")
})
it("should skip files without use-when frontmatter", async () => {
// For this test, only return project directory
vi.mocked(rooConfig.getRooDirectoriesForCwd).mockReturnValue([mockProjectDir])
// Mock directory existence - only project dir has smart-rules
vi.mocked(fs.stat).mockImplementation(async (path) => {
if (path === `${mockProjectDir}/smart-rules`) {
return { isDirectory: () => true } as any
}
throw new Error("Not found")
})
// Mock directory reading - only return files when reading project dir
vi.mocked(fs.readdir).mockImplementation(async (dirPath) => {
if (dirPath === `${mockProjectDir}/smart-rules`) {
return [
{ name: "valid.md", isFile: () => true, isDirectory: () => false } as any,
{ name: "invalid.md", isFile: () => true, isDirectory: () => false } as any,
]
}
return []
})
// Mock file reading - make sure we return different content for each file
vi.mocked(fs.readFile).mockImplementation(async (filePath, encoding) => {
const pathStr = filePath.toString()
// Valid file with use-when
if (pathStr === path.join(mockProjectDir, "smart-rules", "valid.md")) {
return `---
use-when: valid rule
---
Valid content`
}
// Invalid file without use-when
if (pathStr === path.join(mockProjectDir, "smart-rules", "invalid.md")) {
return `---
priority: 1
---
No use-when field`
}
throw new Error(`Unexpected file read: ${filePath}`)
})
const rules = await loadSmartRules(mockCwd)
// The test should check that only the valid rule is loaded
// The invalid.md file should be skipped because it doesn't have use-when
expect(rules).toHaveLength(1)
expect(rules[0].filename).toBe("valid.md")
expect(rules[0].useWhen).toBe("valid rule")
expect(rules[0].content).toBe("Valid content")
})
it("should handle nested directories", async () => {
// Mock directory existence
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as any)
// Mock directory reading
vi.mocked(fs.readdir).mockImplementation(async (dirPath) => {
if (dirPath === `${mockProjectDir}/smart-rules`) {
return [
{ name: "subdir", isFile: () => false, isDirectory: () => true } as any,
{ name: "root.md", isFile: () => true, isDirectory: () => false } as any,
]
}
if (dirPath === path.join(`${mockProjectDir}/smart-rules`, "subdir")) {
return [{ name: "nested.md", isFile: () => true, isDirectory: () => false } as any]
}
return []
})
// Mock file reading
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if (filePath.toString().includes("root.md")) {
return `---
use-when: root rule
---
Root content`
}
if (filePath.toString().includes("nested.md")) {
return `---
use-when: nested rule
---
Nested content`
}
return ""
})
const rules = await loadSmartRules(mockCwd)
expect(rules).toHaveLength(2)
const filenames = rules.map((r) => r.filename)
expect(filenames).toContain("root.md")
expect(filenames).toContain("nested.md")
})
})
describe("hasSmartRules", () => {
it("should return true if smart rules exist", async () => {
// Mock directory existence
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as any)
// Mock directory reading
vi.mocked(fs.readdir).mockResolvedValue([
{ name: "rule.md", isFile: () => true, isDirectory: () => false } as any,
])
const result = await hasSmartRules(mockCwd)
expect(result).toBe(true)
})
it("should return false if no smart rules exist", async () => {
// Mock directory doesn't exist
vi.mocked(fs.stat).mockRejectedValue(new Error("Not found"))
const result = await hasSmartRules(mockCwd)
expect(result).toBe(false)
})
it("should check mode-specific directories when mode is provided", async () => {
// Mock directory existence
vi.mocked(fs.stat).mockImplementation(async (path) => {
if (path === `${mockProjectDir}/smart-rules-code`) {
return { isDirectory: () => true } as any
}
throw new Error("Not found")
})
// Mock directory reading
vi.mocked(fs.readdir).mockResolvedValue([
{ name: "code-rule.md", isFile: () => true, isDirectory: () => false } as any,
])
const result = await hasSmartRules(mockCwd, "code")
expect(result).toBe(true)
expect(fs.stat).toHaveBeenCalledWith(`${mockProjectDir}/smart-rules-code`)
})
})
})

View file

@ -0,0 +1,373 @@
import { describe, it, expect, beforeEach, vi } from "vitest"
import { selectSmartRules, formatSmartRules } from "../smart-rules-matcher"
import type { SmartRule, SmartRulesConfig } from "../../types/smart-rules"
describe("smart-rules-matcher", () => {
let mockRules: SmartRule[]
let mockConfig: SmartRulesConfig
beforeEach(() => {
mockRules = [
{
filename: "database.md",
useWhen: "working with database queries, SQL operations, or data persistence",
priority: 5,
dependencies: ["typescript.md"],
content: "# Database Rules\nAlways use prepared statements...",
},
{
filename: "typescript.md",
useWhen: "TypeScript development, type definitions, or interfaces",
priority: 3,
dependencies: [],
content: "# TypeScript Rules\nUse strict mode...",
},
{
filename: "api-design.md",
useWhen: "designing REST APIs, endpoints, or HTTP services",
priority: 7,
dependencies: ["validation.md"],
content: "# API Design Rules\nFollow RESTful conventions...",
},
{
filename: "validation.md",
useWhen: "input validation, data sanitization, or security checks",
priority: 4,
dependencies: [],
content: "# Validation Rules\nAlways validate user input...",
},
{
filename: "testing.md",
useWhen: "writing tests, unit testing, or test coverage",
priority: 2,
dependencies: [],
content: "# Testing Rules\nAim for 80% coverage...",
},
]
mockConfig = {
enabled: true,
minSimilarity: 0.2, // Lower threshold for tests
maxRules: 5,
showSelectedRules: false,
debugRuleSelection: false,
}
})
describe("selectSmartRules", () => {
it("should select rules based on query similarity", () => {
const query = "How do I create a REST API endpoint?"
// Use even lower threshold for this test
const testConfig = { ...mockConfig, minSimilarity: 0.05 }
const result = selectSmartRules(query, mockRules, testConfig)
expect(result.rules).toContainEqual(expect.objectContaining({ filename: "api-design.md" }))
expect(result.rules.length).toBeGreaterThan(0)
})
it("should include dependencies of selected rules", () => {
const query = "How do I design a REST API?"
// Use lower threshold for this test
const testConfig = { ...mockConfig, minSimilarity: 0.05 }
const result = selectSmartRules(query, mockRules, testConfig)
// Should include api-design.md and its dependency validation.md
const filenames = result.rules.map((r) => r.filename)
expect(filenames).toContain("api-design.md")
expect(filenames).toContain("validation.md")
})
it("should respect maxRules configuration", () => {
const config = { ...mockConfig, maxRules: 2 }
const query = "database SQL TypeScript API testing"
const result = selectSmartRules(query, mockRules, config)
// Note: maxRules applies before dependencies, so total might exceed maxRules
expect(result.rules.filter((r) => !r.filename.includes("validation.md")).length).toBeLessThanOrEqual(2)
})
it("should filter rules below minSimilarity threshold", () => {
const config = { ...mockConfig, minSimilarity: 0.8 }
const query = "unrelated query about something else"
const result = selectSmartRules(query, mockRules, config)
expect(result.rules).toHaveLength(0)
})
it("should handle empty query gracefully", () => {
const result = selectSmartRules("", mockRules, mockConfig)
expect(result.rules).toHaveLength(0)
})
it("should handle empty rules array", () => {
const result = selectSmartRules("test query", [], mockConfig)
expect(result.rules).toHaveLength(0)
})
it("should prioritize rules with higher priority scores", () => {
const query = "API endpoint validation"
const result = selectSmartRules(query, mockRules, mockConfig)
// Both api-design.md and validation.md should match
// api-design.md has higher priority (7 vs 4)
const filenames = result.rules.map((r) => r.filename)
const apiIndex = filenames.indexOf("api-design.md")
const validationIndex = filenames.indexOf("validation.md")
if (apiIndex !== -1 && validationIndex !== -1) {
expect(apiIndex).toBeLessThan(validationIndex)
}
})
it("should handle circular dependencies gracefully", () => {
const circularRules: SmartRule[] = [
{
filename: "rule1.md",
useWhen: "rule one",
priority: 1,
dependencies: ["rule2.md"],
content: "Rule 1",
},
{
filename: "rule2.md",
useWhen: "rule two",
priority: 1,
dependencies: ["rule1.md"],
content: "Rule 2",
},
]
const result = selectSmartRules("rule one", circularRules, mockConfig)
const filenames = result.rules.map((r) => r.filename)
// Should include both rules but not get stuck in infinite loop
expect(filenames).toContain("rule1.md")
expect(filenames).toContain("rule2.md")
expect(result.rules.length).toBe(2)
})
it("should handle missing dependencies gracefully", () => {
const rulesWithMissingDeps: SmartRule[] = [
{
filename: "main.md",
useWhen: "main rule",
priority: 1,
dependencies: ["missing.md"],
content: "Main rule",
},
]
const result = selectSmartRules("main rule", rulesWithMissingDeps, mockConfig)
// Should include the main rule even if dependency is missing
expect(result.rules).toHaveLength(1)
expect(result.rules[0].filename).toBe("main.md")
})
it("should calculate Jaccard similarity correctly", () => {
const query = "database SQL queries"
const result = selectSmartRules(query, mockRules, mockConfig)
// database.md should have high similarity
const dbRule = result.rules.find((r) => r.filename === "database.md")
expect(dbRule).toBeDefined()
})
it("should boost scores for exact phrase matches", () => {
const query = "REST APIs and database queries"
// Use very low threshold since we're testing phrase matching
const testConfig = { ...mockConfig, minSimilarity: 0.01 }
const result = selectSmartRules(query, mockRules, testConfig)
// Both should be selected due to exact phrase matches
const filenames = result.rules.map((r) => r.filename)
expect(filenames).toContain("api-design.md") // matches "REST APIs"
expect(filenames).toContain("database.md") // matches "database queries"
})
it("should handle case-insensitive matching", () => {
const query = "TYPESCRIPT DEVELOPMENT"
const result = selectSmartRules(query, mockRules, mockConfig)
const filenames = result.rules.map((r) => r.filename)
expect(filenames).toContain("typescript.md")
})
it("should deduplicate selected rules", () => {
// Create rules where multiple rules have the same dependency
const rulesWithSharedDeps: SmartRule[] = [
{
filename: "feature1.md",
useWhen: "feature one implementation",
priority: 1,
dependencies: ["common.md"],
content: "Feature 1",
},
{
filename: "feature2.md",
useWhen: "feature two implementation",
priority: 1,
dependencies: ["common.md"],
content: "Feature 2",
},
{
filename: "common.md",
useWhen: "common utilities",
priority: 1,
dependencies: [],
content: "Common",
},
]
const query = "feature one implementation feature two implementation"
const result = selectSmartRules(query, rulesWithSharedDeps, mockConfig)
// Should include all three rules, but common.md only once
const filenames = result.rules.map((r) => r.filename)
const commonCount = filenames.filter((f) => f === "common.md").length
expect(commonCount).toBe(1)
})
it("should include reasoning when debugRuleSelection is enabled", () => {
const config = { ...mockConfig, debugRuleSelection: true, minSimilarity: 0.1 }
const query = "REST API design"
const result = selectSmartRules(query, mockRules, config)
expect(result.reasoning).toBeDefined()
expect(result.reasoning!.length).toBeGreaterThan(0)
expect(result.reasoning![0]).toHaveProperty("rule")
expect(result.reasoning![0]).toHaveProperty("score")
expect(result.reasoning![0]).toHaveProperty("reason")
})
it("should include reasoning when showSelectedRules is enabled", () => {
const config = { ...mockConfig, showSelectedRules: true, minSimilarity: 0.1 }
const query = "database operations"
const result = selectSmartRules(query, mockRules, config)
expect(result.reasoning).toBeDefined()
expect(result.reasoning!.length).toBeGreaterThan(0)
})
it("should return empty array when disabled", () => {
const config = { ...mockConfig, enabled: false }
const query = "database SQL"
const result = selectSmartRules(query, mockRules, config)
expect(result.rules).toHaveLength(0)
})
})
describe("formatSmartRules", () => {
it("should format selected rules correctly without rule names", () => {
const selectedRules: SmartRule[] = [
{
filename: "rule1.md",
useWhen: "test rule",
priority: 1,
dependencies: [],
content: "# Rule 1\nContent of rule 1",
},
{
filename: "rule2.md",
useWhen: "another rule",
priority: 2,
dependencies: [],
content: "# Rule 2\nContent of rule 2",
},
]
const formatted = formatSmartRules(selectedRules)
expect(formatted).not.toContain("# Smart Rule from")
expect(formatted).toContain("# Rule 1\nContent of rule 1")
expect(formatted).toContain("# Rule 2\nContent of rule 2")
})
it("should format selected rules with rule names when showRuleNames is true", () => {
const selectedRules: SmartRule[] = [
{
filename: "rule1.md",
useWhen: "test rule",
priority: 1,
dependencies: [],
content: "# Rule 1\nContent of rule 1",
},
{
filename: "rule2.md",
useWhen: "another rule",
priority: 2,
dependencies: [],
content: "# Rule 2\nContent of rule 2",
},
]
const formatted = formatSmartRules(selectedRules, true)
expect(formatted).toContain("# Smart Rule from rule1.md:")
expect(formatted).toContain("# Rule 1\nContent of rule 1")
expect(formatted).toContain("# Smart Rule from rule2.md:")
expect(formatted).toContain("# Rule 2\nContent of rule 2")
})
it("should handle empty rules array", () => {
const formatted = formatSmartRules([])
expect(formatted).toBe("")
})
it("should preserve rule content formatting", () => {
const selectedRules: SmartRule[] = [
{
filename: "formatted.md",
useWhen: "formatted content",
priority: 1,
dependencies: [],
content: "# Header\n\n```typescript\nconst x = 1;\n```\n\n- List item\n- Another item",
},
]
const formatted = formatSmartRules(selectedRules)
expect(formatted).toContain("```typescript")
expect(formatted).toContain("const x = 1;")
expect(formatted).toContain("- List item")
})
})
describe("edge cases", () => {
it("should handle rules with empty useWhen", () => {
const rulesWithEmptyUseWhen: SmartRule[] = [
{
filename: "empty.md",
useWhen: "",
priority: 1,
dependencies: [],
content: "Empty use-when",
},
]
const result = selectSmartRules("test query", rulesWithEmptyUseWhen, mockConfig)
expect(result.rules).toHaveLength(0)
})
it("should handle very long queries", () => {
const longQuery = "database ".repeat(100) + "SQL"
const config = { ...mockConfig, minSimilarity: 0.1 } // Lower threshold for long query
const result = selectSmartRules(longQuery, mockRules, config)
// Should still match database.md
const filenames = result.rules.map((r) => r.filename)
expect(filenames).toContain("database.md")
})
it("should handle special characters in queries", () => {
const specialQuery = "REST API @#$%^&*() endpoint!"
const config = { ...mockConfig, minSimilarity: 0.1 } // Lower threshold for special chars
const result = selectSmartRules(specialQuery, mockRules, config)
// Should still match api-design.md
const filenames = result.rules.map((r) => r.filename)
expect(filenames).toContain("api-design.md")
})
})
})

View file

@ -6,9 +6,12 @@ import { Dirent } from "fs"
import { isLanguage } from "@roo-code/types"
import type { SystemPromptSettings } from "../types"
import type { SmartRulesConfig } from "../types/smart-rules"
import { LANGUAGES } from "../../../shared/language"
import { getRooDirectoriesForCwd, getGlobalRooDirectory } from "../../../services/roo-config"
import { loadSmartRules, hasSmartRules } from "./smart-rules-loader"
import { selectSmartRules, formatSmartRules } from "./smart-rules-matcher"
/**
* Safely read a file and return its trimmed content
@ -264,6 +267,8 @@ export async function addCustomInstructions(
language?: string
rooIgnoreInstructions?: string
settings?: SystemPromptSettings
userQuery?: string
smartRulesConfig?: Partial<SmartRulesConfig>
} = {},
): Promise<string> {
const sections = []
@ -356,6 +361,24 @@ export async function addCustomInstructions(
rules.push(genericRuleContent.trim())
}
// Add smart rules if enabled and user query is provided
if (options.userQuery && options.smartRulesConfig?.enabled !== false) {
const availableSmartRules = await loadSmartRules(cwd, mode)
if (availableSmartRules.length > 0) {
const selectionResult = selectSmartRules(options.userQuery, availableSmartRules, options.smartRulesConfig)
if (selectionResult.rules.length > 0) {
const formattedSmartRules = formatSmartRules(
selectionResult.rules,
options.smartRulesConfig?.showSelectedRules,
)
if (formattedSmartRules) {
rules.push(formattedSmartRules)
}
}
}
}
if (rules.length > 0) {
sections.push(`Rules:\n\n${rules.join("\n\n")}`)
}

View file

@ -0,0 +1,204 @@
import fs from "fs/promises"
import path from "path"
import * as yaml from "yaml"
import type { SmartRule, SmartRuleFile } from "../types/smart-rules"
import { getRooDirectoriesForCwd } from "../../../services/roo-config"
import { logger } from "../../../utils/logging"
/**
* Check if a directory exists
*/
async function directoryExists(dirPath: string): Promise<boolean> {
try {
const stats = await fs.stat(dirPath)
return stats.isDirectory()
} catch (err) {
return false
}
}
/**
* Parse YAML frontmatter from a markdown file
* @param content The file content
* @returns Object containing frontmatter data and markdown content
*/
function parseFrontmatter(content: string): { frontmatter: SmartRuleFile | null; markdown: string } {
const lines = content.split("\n")
// Check if file starts with frontmatter delimiter
if (lines[0] !== "---") {
return { frontmatter: null, markdown: content }
}
// Find the closing delimiter
let endIndex = -1
for (let i = 1; i < lines.length; i++) {
if (lines[i] === "---") {
endIndex = i
break
}
}
// No closing delimiter found
if (endIndex === -1) {
return { frontmatter: null, markdown: content }
}
// Extract and parse frontmatter
const frontmatterContent = lines.slice(1, endIndex).join("\n")
const markdownContent = lines
.slice(endIndex + 1)
.join("\n")
.trim()
try {
const frontmatter = yaml.parse(frontmatterContent) as SmartRuleFile
return { frontmatter, markdown: markdownContent }
} catch (error) {
logger.error("Failed to parse frontmatter", { error })
return { frontmatter: null, markdown: content }
}
}
/**
* Load a smart rule from a file
* @param filePath The path to the rule file
* @returns The parsed smart rule or null if invalid
*/
async function loadSmartRuleFromFile(filePath: string): Promise<SmartRule | null> {
try {
const content = await fs.readFile(filePath, "utf-8")
const { frontmatter, markdown } = parseFrontmatter(content)
// Skip files without frontmatter or use-when field
if (!frontmatter || !frontmatter["use-when"]) {
return null
}
const filename = path.basename(filePath)
return {
filename,
useWhen: frontmatter["use-when"],
content: markdown,
priority: frontmatter.priority,
dependencies: frontmatter.dependencies,
metadata: Object.fromEntries(
Object.entries(frontmatter).filter(([key]) => !["use-when", "priority", "dependencies"].includes(key)),
),
}
} catch (error) {
logger.error("Failed to load smart rule from file", { filePath, error })
return null
}
}
/**
* Recursively load smart rules from a directory
* @param dirPath The directory path
* @returns Array of loaded smart rules
*/
async function loadSmartRulesFromDirectory(dirPath: string): Promise<SmartRule[]> {
const rules: SmartRule[] = []
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name)
if (entry.isDirectory()) {
// Recursively load from subdirectories
const subRules = await loadSmartRulesFromDirectory(fullPath)
rules.push(...subRules)
} else if (entry.isFile() && entry.name.endsWith(".md")) {
// Load rule from markdown file
const rule = await loadSmartRuleFromFile(fullPath)
if (rule) {
rules.push(rule)
}
}
}
} catch (error) {
logger.error("Failed to load smart rules from directory", { dirPath, error })
}
return rules
}
/**
* Load all smart rules from the appropriate directories
* @param cwd The current working directory
* @param mode The current mode (optional, for mode-specific smart rules)
* @returns Array of all loaded smart rules
*/
export async function loadSmartRules(cwd: string, mode?: string): Promise<SmartRule[]> {
const allRules: SmartRule[] = []
const rooDirectories = getRooDirectoriesForCwd(cwd)
// Load global and project smart rules
for (const rooDir of rooDirectories) {
const smartRulesDir = path.join(rooDir, "smart-rules")
if (await directoryExists(smartRulesDir)) {
const rules = await loadSmartRulesFromDirectory(smartRulesDir)
allRules.push(...rules)
}
}
// Load mode-specific smart rules if mode is provided
if (mode) {
for (const rooDir of rooDirectories) {
const modeSmartRulesDir = path.join(rooDir, `smart-rules-${mode}`)
if (await directoryExists(modeSmartRulesDir)) {
const rules = await loadSmartRulesFromDirectory(modeSmartRulesDir)
allRules.push(...rules)
}
}
}
// Sort by priority (higher priority first) and then by filename for stability
allRules.sort((a, b) => {
const priorityDiff = (b.priority ?? 0) - (a.priority ?? 0)
if (priorityDiff !== 0) return priorityDiff
return a.filename.localeCompare(b.filename)
})
return allRules
}
/**
* Check if smart rules are available for the current context
* @param cwd The current working directory
* @param mode The current mode (optional)
* @returns True if any smart rules exist
*/
export async function hasSmartRules(cwd: string, mode?: string): Promise<boolean> {
const rooDirectories = getRooDirectoriesForCwd(cwd)
// Check for general smart rules
for (const rooDir of rooDirectories) {
const smartRulesDir = path.join(rooDir, "smart-rules")
if (await directoryExists(smartRulesDir)) {
const entries = await fs.readdir(smartRulesDir, { withFileTypes: true })
if (entries.some((e) => e.isFile() && e.name.endsWith(".md"))) {
return true
}
}
}
// Check for mode-specific smart rules
if (mode) {
for (const rooDir of rooDirectories) {
const modeSmartRulesDir = path.join(rooDir, `smart-rules-${mode}`)
if (await directoryExists(modeSmartRulesDir)) {
const entries = await fs.readdir(modeSmartRulesDir, { withFileTypes: true })
if (entries.some((e) => e.isFile() && e.name.endsWith(".md"))) {
return true
}
}
}
}
return false
}

View file

@ -0,0 +1,192 @@
import type { SmartRule, SmartRuleSelectionResult, SmartRulesConfig } from "../types/smart-rules"
import { logger } from "../../../utils/logging"
/**
* Default configuration for smart rules
*/
const DEFAULT_CONFIG: Required<SmartRulesConfig> = {
enabled: true,
minSimilarity: 0.7,
maxRules: 5,
showSelectedRules: false,
debugRuleSelection: false,
}
/**
* Tokenize text for similarity comparison
* @param text The text to tokenize
* @returns Array of normalized tokens
*/
function tokenize(text: string): string[] {
// Convert to lowercase and split on word boundaries
return text
.toLowerCase()
.replace(/[^\w\s]/g, " ") // Replace punctuation with spaces
.split(/\s+/)
.filter((token) => token.length > 2) // Filter out very short tokens
}
/**
* Calculate Jaccard similarity between two sets of tokens
* @param tokens1 First set of tokens
* @param tokens2 Second set of tokens
* @returns Similarity score between 0 and 1
*/
function jaccardSimilarity(tokens1: string[], tokens2: string[]): number {
const set1 = new Set(tokens1)
const set2 = new Set(tokens2)
// Calculate intersection
const intersection = new Set()
set1.forEach((item) => {
if (set2.has(item)) {
intersection.add(item)
}
})
// Calculate union
const union = new Set(set1)
set2.forEach((item) => union.add(item))
if (union.size === 0) return 0
return intersection.size / union.size
}
/**
* Calculate semantic similarity between query and rule trigger
* This is a simple implementation that can be enhanced with more sophisticated NLP
* @param query The user query
* @param trigger The rule trigger text
* @returns Similarity score between 0 and 1
*/
function calculateSimilarity(query: string, trigger: string): number {
const queryTokens = tokenize(query)
const triggerTokens = tokenize(trigger)
// Calculate Jaccard similarity
const jaccardScore = jaccardSimilarity(queryTokens, triggerTokens)
// Check for key phrase matches (boost score if trigger phrases appear in query)
const triggerPhrases = trigger
.toLowerCase()
.split(/,|\sor\s/)
.map((phrase) => phrase.trim())
.filter((phrase) => phrase.length > 0)
let phraseMatchBoost = 0
const queryLower = query.toLowerCase()
for (const phrase of triggerPhrases) {
if (queryLower.includes(phrase)) {
phraseMatchBoost += 0.3
}
}
// Combine scores (cap at 1.0)
return Math.min(1.0, jaccardScore + phraseMatchBoost)
}
/**
* Select smart rules based on user query
* @param query The user query/task description
* @param availableRules All available smart rules
* @param config Smart rules configuration
* @returns Selection result with matched rules and reasoning
*/
export function selectSmartRules(
query: string,
availableRules: SmartRule[],
config: Partial<SmartRulesConfig> = {},
): SmartRuleSelectionResult {
const finalConfig = { ...DEFAULT_CONFIG, ...config }
if (!finalConfig.enabled || availableRules.length === 0) {
return { rules: [] }
}
// Calculate similarity scores for all rules
const scoredRules = availableRules.map((rule) => {
const score = calculateSimilarity(query, rule.useWhen)
return { rule, score }
})
// Filter by minimum similarity and sort by score (descending)
const eligibleRules = scoredRules
.filter(({ score }) => score >= finalConfig.minSimilarity)
.sort((a, b) => {
// First sort by score
const scoreDiff = b.score - a.score
if (scoreDiff !== 0) return scoreDiff
// Then by priority
const priorityDiff = (b.rule.priority ?? 0) - (a.rule.priority ?? 0)
if (priorityDiff !== 0) return priorityDiff
// Finally by filename for stability
return a.rule.filename.localeCompare(b.rule.filename)
})
.slice(0, finalConfig.maxRules)
// Collect selected rules and handle dependencies
const selectedRules = new Map<string, SmartRule>()
const reasoning: SmartRuleSelectionResult["reasoning"] = []
for (const { rule, score } of eligibleRules) {
selectedRules.set(rule.filename, rule)
if (finalConfig.debugRuleSelection || finalConfig.showSelectedRules) {
reasoning.push({
rule: rule.filename,
score: Math.round(score * 100) / 100,
reason: `Matched "${rule.useWhen}" with score ${(score * 100).toFixed(0)}%`,
})
}
// Add dependencies
if (rule.dependencies) {
for (const depFilename of rule.dependencies) {
const depRule = availableRules.find((r) => r.filename === depFilename)
if (depRule && !selectedRules.has(depFilename)) {
selectedRules.set(depFilename, depRule)
if (finalConfig.debugRuleSelection || finalConfig.showSelectedRules) {
reasoning.push({
rule: depFilename,
score: 0,
reason: `Included as dependency of ${rule.filename}`,
})
}
}
}
}
}
// Log selection results if debugging is enabled
if (finalConfig.debugRuleSelection) {
logger.info("Smart rule selection completed", {
query: query.substring(0, 100) + "...",
totalRules: availableRules.length,
selectedCount: selectedRules.size,
reasoning,
})
}
return {
rules: Array.from(selectedRules.values()),
reasoning: reasoning.length > 0 ? reasoning : undefined,
}
}
/**
* Format selected smart rules for inclusion in the prompt
* @param rules The selected smart rules
* @param showRuleNames Whether to include rule filenames in the output
* @returns Formatted rules content
*/
export function formatSmartRules(rules: SmartRule[], showRuleNames = false): string {
if (rules.length === 0) return ""
const sections = rules.map((rule) => {
const header = showRuleNames ? `# Smart Rule from ${rule.filename}:\n` : ""
return header + rule.content
})
return sections.join("\n\n")
}

View file

@ -4,6 +4,7 @@ import * as os from "os"
import type { ModeConfig, PromptComponent, CustomModePrompts, TodoItem } from "@roo-code/types"
import type { SystemPromptSettings } from "./types"
import type { SmartRulesConfig } from "./types/smart-rules"
import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes"
import { DiffStrategy } from "../../shared/tools"
@ -61,6 +62,8 @@ async function generatePrompt(
partialReadsEnabled?: boolean,
settings?: SystemPromptSettings,
todoList?: TodoItem[],
userQuery?: string,
smartRulesConfig?: Partial<SmartRulesConfig>,
): Promise<string> {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@ -126,6 +129,8 @@ ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "",
language: language ?? formatLanguage(vscode.env.language),
rooIgnoreInstructions,
settings,
userQuery,
smartRulesConfig,
})}`
return basePrompt
@ -150,6 +155,8 @@ export const SYSTEM_PROMPT = async (
partialReadsEnabled?: boolean,
settings?: SystemPromptSettings,
todoList?: TodoItem[],
userQuery?: string,
smartRulesConfig?: Partial<SmartRulesConfig>,
): Promise<string> => {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@ -188,6 +195,8 @@ export const SYSTEM_PROMPT = async (
language: language ?? formatLanguage(vscode.env.language),
rooIgnoreInstructions,
settings,
userQuery,
smartRulesConfig,
},
)
@ -221,5 +230,7 @@ ${customInstructions}`
partialReadsEnabled,
settings,
todoList,
userQuery,
smartRulesConfig,
)
}

View file

@ -0,0 +1,118 @@
/**
* Smart Rules types and interfaces for intelligent semantic rule injection
*/
/**
* Represents a smart rule with context triggers and content
*/
export interface SmartRule {
/**
* The filename where this rule was loaded from
*/
filename: string
/**
* The context trigger that describes when this rule should be used
* This is matched against user queries to determine relevance
*/
useWhen: string
/**
* The actual rule content (markdown)
*/
content: string
/**
* Optional priority for rule ordering (higher = more important)
* Default is 0
*/
priority?: number
/**
* Optional dependencies - other rules that should be included when this rule is selected
*/
dependencies?: string[]
/**
* Optional metadata for future extensibility
*/
metadata?: Record<string, any>
}
/**
* Configuration options for smart rules
*/
export interface SmartRulesConfig {
/**
* Whether smart rules are enabled
*/
enabled: boolean
/**
* Minimum similarity score for rule matching (0-1)
* Default is 0.7
*/
minSimilarity?: number
/**
* Maximum number of smart rules to include in a single prompt
* Default is 5
*/
maxRules?: number
/**
* Whether to show which rules were selected in the UI
* Default is false
*/
showSelectedRules?: boolean
/**
* Whether to include rule selection reasoning in debug logs
* Default is false
*/
debugRuleSelection?: boolean
}
/**
* Result of smart rule selection
*/
export interface SmartRuleSelectionResult {
/**
* The selected rules
*/
rules: SmartRule[]
/**
* Reasoning for why each rule was selected (for debugging/transparency)
*/
reasoning?: Array<{
rule: string
score: number
reason: string
}>
}
/**
* Smart rule file format (YAML frontmatter + markdown content)
*/
export interface SmartRuleFile {
/**
* When to use this rule (from frontmatter)
*/
"use-when": string
/**
* Optional priority (from frontmatter)
*/
priority?: number
/**
* Optional dependencies (from frontmatter)
*/
dependencies?: string[]
/**
* Any other metadata fields
*/
[key: string]: any
}

View file

@ -1842,7 +1842,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}
private async getSystemPrompt(): Promise<string> {
private async getSystemPrompt(userQuery?: string): Promise<string> {
const { mcpEnabled } = (await this.providerRef.deref()?.getState()) ?? {}
let mcpHub: McpHub | undefined
if (mcpEnabled ?? true) {
@ -1913,6 +1913,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
useAgentRules: vscode.workspace.getConfiguration("roo-cline").get<boolean>("useAgentRules") ?? true,
},
undefined, // todoList
userQuery,
{
enabled: vscode.workspace.getConfiguration("roo-cline").get<boolean>("smartRules.enabled") ?? true,
minSimilarity:
vscode.workspace.getConfiguration("roo-cline").get<number>("smartRules.minSimilarity") ?? 0.7,
maxRules: vscode.workspace.getConfiguration("roo-cline").get<number>("smartRules.maxRules") ?? 5,
showSelectedRules:
vscode.workspace.getConfiguration("roo-cline").get<boolean>("smartRules.showSelectedRules") ??
false,
debugRuleSelection:
vscode.workspace.getConfiguration("roo-cline").get<boolean>("smartRules.debugRuleSelection") ??
false,
},
)
})()
}
@ -1980,7 +1994,22 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// requests — even from new subtasks — will honour the provider's rate-limit.
Task.lastGlobalApiRequestTime = Date.now()
const systemPrompt = await this.getSystemPrompt()
// Extract the user query from the most recent user message
let userQuery: string | undefined
const lastUserMessage = this.apiConversationHistory
.slice()
.reverse()
.find((msg) => msg.role === "user")
if (lastUserMessage && Array.isArray(lastUserMessage.content)) {
const textBlocks = lastUserMessage.content.filter((block: any) => block.type === "text")
if (textBlocks.length > 0) {
// Combine all text blocks to form the user query
userQuery = textBlocks.map((block: any) => block.text).join("\n")
}
}
const systemPrompt = await this.getSystemPrompt(userQuery)
const { contextTokens } = this.getTokenUsage()
if (contextTokens) {

View file

@ -391,6 +391,35 @@
"type": "boolean",
"default": true,
"description": "%settings.useAgentRules.description%"
},
"roo-cline.smartRules.enabled": {
"type": "boolean",
"default": true,
"description": "%settings.smartRules.enabled.description%"
},
"roo-cline.smartRules.minSimilarity": {
"type": "number",
"default": 0.7,
"minimum": 0,
"maximum": 1,
"description": "%settings.smartRules.minSimilarity.description%"
},
"roo-cline.smartRules.maxRules": {
"type": "number",
"default": 5,
"minimum": 1,
"maximum": 20,
"description": "%settings.smartRules.maxRules.description%"
},
"roo-cline.smartRules.showSelectedRules": {
"type": "boolean",
"default": false,
"description": "%settings.smartRules.showSelectedRules.description%"
},
"roo-cline.smartRules.debugRuleSelection": {
"type": "boolean",
"default": false,
"description": "%settings.smartRules.debugRuleSelection.description%"
}
}
}

View file

@ -37,5 +37,10 @@
"settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Enable Roo Code quick fixes",
"settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.",
"settings.useAgentRules.description": "Enable loading of AGENTS.md files for agent-specific rules (see https://agent-rules.org/)"
"settings.useAgentRules.description": "Enable loading of AGENTS.md files for agent-specific rules (see https://agent-rules.org/)",
"settings.smartRules.enabled.description": "Enable intelligent semantic rule injection that automatically selects relevant rules based on the current task",
"settings.smartRules.minSimilarity.description": "Minimum similarity score (0-1) required for a smart rule to be selected. Lower values include more rules",
"settings.smartRules.maxRules.description": "Maximum number of smart rules to include in a single prompt",
"settings.smartRules.showSelectedRules.description": "Show which smart rules were selected in the UI",
"settings.smartRules.debugRuleSelection.description": "Enable debug logging for smart rule selection process"
}

View file

@ -11,11 +11,26 @@ import {
DEFAULT_MODES,
} from "@roo-code/types"
import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
import { EXPERIMENT_IDS } from "./experiments"
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "./tools"
// Conditional import to avoid bundling Node.js modules in webview
let addCustomInstructions:
| typeof import("../core/prompts/sections/custom-instructions").addCustomInstructions
| undefined
// Dynamic import for Node.js environment
async function loadAddCustomInstructions() {
if (typeof window === "undefined" && !addCustomInstructions) {
try {
const module = await import("../core/prompts/sections/custom-instructions")
addCustomInstructions = module.addCustomInstructions
} catch (error) {
console.error("Failed to load addCustomInstructions:", error)
}
}
}
export type Mode = string
// Helper to extract group name regardless of format
@ -307,6 +322,10 @@ export async function getFullModeDetails(
cwd?: string
globalCustomInstructions?: string
language?: string
rooIgnoreInstructions?: string
settings?: any
userQuery?: string
smartRulesConfig?: any
},
): Promise<ModeConfig> {
// First get the base mode config from custom modes or built-in modes
@ -323,13 +342,22 @@ export async function getFullModeDetails(
// If we have cwd, load and combine all custom instructions
let fullCustomInstructions = baseCustomInstructions
if (options?.cwd) {
fullCustomInstructions = await addCustomInstructions(
baseCustomInstructions,
options.globalCustomInstructions || "",
options.cwd,
modeSlug,
{ language: options.language },
)
await loadAddCustomInstructions()
if (addCustomInstructions) {
fullCustomInstructions = await addCustomInstructions(
baseCustomInstructions,
options.globalCustomInstructions || "",
options.cwd,
modeSlug,
{
language: options.language,
rooIgnoreInstructions: options.rooIgnoreInstructions,
settings: options.settings,
userQuery: options.userQuery,
smartRulesConfig: options.smartRulesConfig,
},
)
}
}
// Return mode with any overrides applied