diff --git a/docs/smart-rules.md b/docs/smart-rules.md new file mode 100644 index 0000000000..2fb209d922 --- /dev/null +++ b/docs/smart-rules.md @@ -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("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 diff --git a/src/core/prompts/sections/__tests__/smart-rules-loader.spec.ts b/src/core/prompts/sections/__tests__/smart-rules-loader.spec.ts new file mode 100644 index 0000000000..0d1eae8182 --- /dev/null +++ b/src/core/prompts/sections/__tests__/smart-rules-loader.spec.ts @@ -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`) + }) + }) +}) diff --git a/src/core/prompts/sections/__tests__/smart-rules-matcher.spec.ts b/src/core/prompts/sections/__tests__/smart-rules-matcher.spec.ts new file mode 100644 index 0000000000..1302e1abee --- /dev/null +++ b/src/core/prompts/sections/__tests__/smart-rules-matcher.spec.ts @@ -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") + }) + }) +}) diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 22fb6122ec..9e2e1c985a 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -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 } = {}, ): Promise { 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")}`) } diff --git a/src/core/prompts/sections/smart-rules-loader.ts b/src/core/prompts/sections/smart-rules-loader.ts new file mode 100644 index 0000000000..ba76994cd1 --- /dev/null +++ b/src/core/prompts/sections/smart-rules-loader.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 +} diff --git a/src/core/prompts/sections/smart-rules-matcher.ts b/src/core/prompts/sections/smart-rules-matcher.ts new file mode 100644 index 0000000000..b8e8505520 --- /dev/null +++ b/src/core/prompts/sections/smart-rules-matcher.ts @@ -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 = { + 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 = {}, +): 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() + 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") +} diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 4ed1185da7..669f2c3062 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -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, ): Promise { 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, ): Promise => { 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, ) } diff --git a/src/core/prompts/types/smart-rules.ts b/src/core/prompts/types/smart-rules.ts new file mode 100644 index 0000000000..08b868693e --- /dev/null +++ b/src/core/prompts/types/smart-rules.ts @@ -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 +} + +/** + * 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 +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index e0c332d16f..c3587bf3bc 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1842,7 +1842,7 @@ export class Task extends EventEmitter implements TaskLike { } } - private async getSystemPrompt(): Promise { + private async getSystemPrompt(userQuery?: string): Promise { const { mcpEnabled } = (await this.providerRef.deref()?.getState()) ?? {} let mcpHub: McpHub | undefined if (mcpEnabled ?? true) { @@ -1913,6 +1913,20 @@ export class Task extends EventEmitter implements TaskLike { todoListEnabled: apiConfiguration?.todoListEnabled ?? true, useAgentRules: vscode.workspace.getConfiguration("roo-cline").get("useAgentRules") ?? true, }, + undefined, // todoList + userQuery, + { + enabled: vscode.workspace.getConfiguration("roo-cline").get("smartRules.enabled") ?? true, + minSimilarity: + vscode.workspace.getConfiguration("roo-cline").get("smartRules.minSimilarity") ?? 0.7, + maxRules: vscode.workspace.getConfiguration("roo-cline").get("smartRules.maxRules") ?? 5, + showSelectedRules: + vscode.workspace.getConfiguration("roo-cline").get("smartRules.showSelectedRules") ?? + false, + debugRuleSelection: + vscode.workspace.getConfiguration("roo-cline").get("smartRules.debugRuleSelection") ?? + false, + }, ) })() } @@ -1980,7 +1994,22 @@ export class Task extends EventEmitter 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) { diff --git a/src/package.json b/src/package.json index d35f6f34dd..1b5a02fc63 100644 --- a/src/package.json +++ b/src/package.json @@ -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%" } } } diff --git a/src/package.nls.json b/src/package.nls.json index 36ef72a823..158028b0b0 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -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" } diff --git a/src/shared/modes.ts b/src/shared/modes.ts index f68d25c682..1628a972d4 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -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 { // 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