-
-
Notifications
You must be signed in to change notification settings - Fork 29
Add Skills feature: Custom Skills in Skills.md #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
| ]; | ||
|
Comment on lines
14
to
18
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| } | ||
| }); | ||
|
Comment on lines
+33
to
+40
|
||
|
|
||
| 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]')); | ||
|
Comment on lines
+111
to
+127
|
||
| }); | ||
|
|
||
| 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'); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loadSkillsPrompt()logs "Loaded skills from Skills.md" even when the file is present but trims down to an empty string. Sincechat()later checksif (this.skillsPrompt)(truthy) before prepending, an empty Skills.md results in a "loaded" log but no skills section in the prompt. Consider treating an all-whitespace file as absent (e.g., setskillsPromptback to null and/or only log when the trimmed content is non-empty) to keep behavior consistent.