From 9defd4036024cb7877857c508721cacd5273b178 Mon Sep 17 00:00:00 2001 From: Akash-nath29 Date: Tue, 27 Jan 2026 01:06:06 +0530 Subject: [PATCH] Add Skills feature --- src/agent.js | 42 +++++++-- src/fileOps.js | 1 + tests/unit/skills.test.js | 194 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 tests/unit/skills.test.js diff --git a/src/agent.js b/src/agent.js index 68c7c8a..8651137 100644 --- a/src/agent.js +++ b/src/agent.js @@ -46,7 +46,8 @@ class Agent { this.scanOnFirstRequest = options.scanOnFirstRequest !== false; // Default to true this.gitEnabled = options.gitEnabled || false; // Git auto-commit feature (opt-in) this.maxHistoryLength = options.maxHistoryLength || 10; // Max conversation turns to keep - this.customPrompt = null; // Custom system prompt from Coderrr.md + this.skillsPrompt = null; // Skills prompt from Skills.md (persistent skills) + this.customPrompt = null; // Custom system prompt from Coderrr.md (task-specific) // Initialize project-local storage and load cross-session memory configManager.initializeProjectStorage(this.workingDir); @@ -84,15 +85,32 @@ class Agent { ui.info('Conversation history cleared'); } + /** + * Load skills prompt from Skills.md in project directory + * Skills are persistent guidance that applies to all tasks (e.g., design guidelines) + */ + loadSkillsPrompt() { + try { + const skillsPath = path.join(this.workingDir, 'Skills.md'); + if (fs.existsSync(skillsPath)) { + this.skillsPrompt = fs.readFileSync(skillsPath, 'utf8').trim(); + ui.info('Loaded skills from Skills.md'); + } + } catch (error) { + ui.warning(`Could not load Skills.md: ${error.message}`); + } + } + /** * Load custom system prompt from Coderrr.md in project directory + * This is task-specific guidance that may change per task */ loadCustomPrompt() { try { const customPromptPath = path.join(this.workingDir, 'Coderrr.md'); if (fs.existsSync(customPromptPath)) { this.customPrompt = fs.readFileSync(customPromptPath, 'utf8').trim(); - ui.info('Loaded custom system prompt from Coderrr.md'); + ui.info('Loaded task prompt from Coderrr.md'); } } catch (error) { ui.warning(`Could not load Coderrr.md: ${error.message}`); @@ -124,6 +142,11 @@ class Agent { */ async chat(prompt, options = {}) { try { + // Load skills prompt on first request if not already loaded + if (this.skillsPrompt === null) { + this.loadSkillsPrompt(); + } + // Load custom prompt on first request if not already loaded if (this.customPrompt === null) { this.loadCustomPrompt(); @@ -144,14 +167,21 @@ class Agent { } } - // Enhance prompt with custom prompt and codebase context + // Build enhanced prompt with priority: + // 1. System Prompt (embedded in backend) + // 2. Skills.md (persistent skills) + // 3. Coderrr.md (task-specific guidance) + // 4. User prompt let enhancedPrompt = prompt; - // Prepend custom prompt if available + // Prepend task-specific prompt (Coderrr.md) if available if (this.customPrompt) { - enhancedPrompt = `${this.customPrompt} + enhancedPrompt = `[TASK GUIDANCE]\n${this.customPrompt}\n\n[USER REQUEST]\n${enhancedPrompt}`; + } -${prompt}`; + // Prepend skills prompt (Skills.md) if available - comes before task prompt + if (this.skillsPrompt) { + enhancedPrompt = `[SKILLS]\n${this.skillsPrompt}\n\n${enhancedPrompt}`; } if (this.codebaseContext) { diff --git a/src/fileOps.js b/src/fileOps.js index 3c030da..af9223e 100644 --- a/src/fileOps.js +++ b/src/fileOps.js @@ -13,6 +13,7 @@ const ui = require('./ui'); // Protected paths that should never be deleted by Coderrr const PROTECTED_PATHS = [ 'Coderrr.md', + 'Skills.md', '.coderrr' ]; diff --git a/tests/unit/skills.test.js b/tests/unit/skills.test.js new file mode 100644 index 0000000..a9c66f3 --- /dev/null +++ b/tests/unit/skills.test.js @@ -0,0 +1,194 @@ +const fs = require('fs'); +const path = require('path'); +const Agent = require('../../src/agent'); + +// Mock dependencies +jest.mock('../../src/ui', () => ({ + info: jest.fn(), + warning: jest.fn(), + success: jest.fn(), + error: jest.fn(), + spinner: jest.fn(() => ({ start: jest.fn(), stop: jest.fn() })), + section: jest.fn(), + space: jest.fn(), + confirm: jest.fn(), + displayFileOp: jest.fn(), + displayDiff: jest.fn() +})); + +jest.mock('../../src/configManager', () => ({ + initializeProjectStorage: jest.fn(), + loadProjectMemory: jest.fn(() => []), + saveProjectMemory: jest.fn(), + clearProjectMemory: jest.fn(), + getConfig: jest.fn() +})); + +jest.mock('axios'); + +describe('Skills.md Loading', () => { + let tempDir; + const originalCwd = process.cwd(); + + beforeEach(() => { + jest.clearAllMocks(); + // Create a temporary directory for testing + tempDir = path.join(originalCwd, 'test-temp-skills'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + }); + + afterEach(() => { + // Clean up temporary directory + if (fs.existsSync(tempDir)) { + const files = fs.readdirSync(tempDir); + for (const file of files) { + fs.unlinkSync(path.join(tempDir, file)); + } + fs.rmdirSync(tempDir); + } + }); + + describe('loadSkillsPrompt', () => { + it('should load Skills.md when it exists', () => { + const skillsContent = '# Frontend Skills\n- Use modern design patterns'; + fs.writeFileSync(path.join(tempDir, 'Skills.md'), skillsContent); + + const agent = new Agent({ workingDir: tempDir }); + agent.loadSkillsPrompt(); + + expect(agent.skillsPrompt).toBe(skillsContent); + }); + + it('should not set skillsPrompt when Skills.md does not exist', () => { + const agent = new Agent({ workingDir: tempDir }); + agent.loadSkillsPrompt(); + + expect(agent.skillsPrompt).toBeNull(); + }); + + it('should handle empty Skills.md gracefully', () => { + fs.writeFileSync(path.join(tempDir, 'Skills.md'), ' '); + + const agent = new Agent({ workingDir: tempDir }); + agent.loadSkillsPrompt(); + + expect(agent.skillsPrompt).toBe(''); + }); + }); + + describe('loadCustomPrompt', () => { + it('should load Coderrr.md when it exists', () => { + const taskContent = '# Task: Build landing page'; + fs.writeFileSync(path.join(tempDir, 'Coderrr.md'), taskContent); + + const agent = new Agent({ workingDir: tempDir }); + agent.loadCustomPrompt(); + + expect(agent.customPrompt).toBe(taskContent); + }); + + it('should not set customPrompt when Coderrr.md does not exist', () => { + const agent = new Agent({ workingDir: tempDir }); + agent.loadCustomPrompt(); + + expect(agent.customPrompt).toBeNull(); + }); + }); + + describe('Prompt Priority Order', () => { + it('should construct prompt with Skills.md before Coderrr.md', () => { + const skillsContent = '# Skills\n- Be modern'; + const taskContent = '# Task\n- Build a form'; + fs.writeFileSync(path.join(tempDir, 'Skills.md'), skillsContent); + fs.writeFileSync(path.join(tempDir, 'Coderrr.md'), taskContent); + + const agent = new Agent({ workingDir: tempDir, scanOnFirstRequest: false }); + agent.loadSkillsPrompt(); + agent.loadCustomPrompt(); + + // Simulate prompt construction (from chat method logic) + let enhancedPrompt = 'User request'; + + // Task guidance first (will be wrapped by skills) + if (agent.customPrompt) { + enhancedPrompt = `[TASK GUIDANCE]\n${agent.customPrompt}\n\n[USER REQUEST]\n${enhancedPrompt}`; + } + + // Skills prepended (comes before everything else) + if (agent.skillsPrompt) { + enhancedPrompt = `[SKILLS]\n${agent.skillsPrompt}\n\n${enhancedPrompt}`; + } + + // Verify priority order: Skills comes first + expect(enhancedPrompt.startsWith('[SKILLS]')).toBe(true); + expect(enhancedPrompt.indexOf('[SKILLS]')).toBeLessThan(enhancedPrompt.indexOf('[TASK GUIDANCE]')); + expect(enhancedPrompt.indexOf('[TASK GUIDANCE]')).toBeLessThan(enhancedPrompt.indexOf('[USER REQUEST]')); + }); + + it('should work with only Skills.md (no Coderrr.md)', () => { + const skillsContent = '# Skills\n- Be creative'; + fs.writeFileSync(path.join(tempDir, 'Skills.md'), skillsContent); + + const agent = new Agent({ workingDir: tempDir, scanOnFirstRequest: false }); + agent.loadSkillsPrompt(); + agent.loadCustomPrompt(); + + let enhancedPrompt = 'User request'; + + if (agent.customPrompt) { + enhancedPrompt = `[TASK GUIDANCE]\n${agent.customPrompt}\n\n[USER REQUEST]\n${enhancedPrompt}`; + } + + if (agent.skillsPrompt) { + enhancedPrompt = `[SKILLS]\n${agent.skillsPrompt}\n\n${enhancedPrompt}`; + } + + expect(enhancedPrompt).toContain('[SKILLS]'); + expect(enhancedPrompt).not.toContain('[TASK GUIDANCE]'); + expect(enhancedPrompt).toContain('User request'); + }); + + it('should work with only Coderrr.md (no Skills.md)', () => { + const taskContent = '# Task\n- Do something'; + fs.writeFileSync(path.join(tempDir, 'Coderrr.md'), taskContent); + + const agent = new Agent({ workingDir: tempDir, scanOnFirstRequest: false }); + agent.loadSkillsPrompt(); + agent.loadCustomPrompt(); + + let enhancedPrompt = 'User request'; + + if (agent.customPrompt) { + enhancedPrompt = `[TASK GUIDANCE]\n${agent.customPrompt}\n\n[USER REQUEST]\n${enhancedPrompt}`; + } + + if (agent.skillsPrompt) { + enhancedPrompt = `[SKILLS]\n${agent.skillsPrompt}\n\n${enhancedPrompt}`; + } + + expect(enhancedPrompt).not.toContain('[SKILLS]'); + expect(enhancedPrompt).toContain('[TASK GUIDANCE]'); + expect(enhancedPrompt).toContain('User request'); + }); + + it('should work with neither Skills.md nor Coderrr.md', () => { + const agent = new Agent({ workingDir: tempDir, scanOnFirstRequest: false }); + agent.loadSkillsPrompt(); + agent.loadCustomPrompt(); + + let enhancedPrompt = 'User request'; + + if (agent.customPrompt) { + enhancedPrompt = `[TASK GUIDANCE]\n${agent.customPrompt}\n\n[USER REQUEST]\n${enhancedPrompt}`; + } + + if (agent.skillsPrompt) { + enhancedPrompt = `[SKILLS]\n${agent.skillsPrompt}\n\n${enhancedPrompt}`; + } + + expect(enhancedPrompt).toBe('User request'); + }); + }); +});