Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Git
.git
.gitignore

# Node
node_modules
npm-debug.log
dist
coverage

# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env
venv
pip-log.txt
pip-delete-this-directory.txt
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.pytest_cache

# Docker
docker-compose.yml
Dockerfile
.dockerignore
Comment on lines +32 to +34
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .dockerignore file excludes docker-compose.yml, Dockerfile, and .dockerignore itself from the Docker build context. However, these files are in the root directory while the Docker build context is set to "./backend" in docker-compose.yml. These exclusions have no effect since the files aren't in the backend directory. Additionally, excluding docker-compose.yml and Dockerfile from the build context is unnecessary since they wouldn't be copied anyway - the COPY commands in the Dockerfile only copy requirements.txt and the application code.

Suggested change
docker-compose.yml
Dockerfile
.dockerignore

Copilot uses AI. Check for mistakes.

# Environment
.env
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .dockerignore file excludes the .env file, which means environment variables won't be available in the container. Combined with the env_file issue in docker-compose.yml, this ensures the backend will not have access to required API credentials (GITHUB_TOKEN, MISTRAL_API_KEY, etc.). While excluding .env from the Docker image is a security best practice, the docker-compose.yml must correctly mount or load the environment file for the container to access these variables at runtime.

Suggested change
.env

Copilot uses AI. Check for mistakes.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,17 @@ This section explains how to run Coderrr locally from source for development or
git clone https://github.com/Akash-nath29/Coderrr.git
cd Coderrr
```
### 2. Backend Setup (FastAPI)
### 2. Backend Setup
You can run the backend using Docker (recommended) or set it up manually.

#### Option A: Docker (Recommended)

```bash
docker compose up --build
```
The backend will be started at `http://localhost:5000` with hot-reloading enabled.
Comment on lines +358 to +363
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Docker setup instructions mention that the backend will be available at http://localhost:5000, but don't include any instructions for setting up the required environment variables (GITHUB_TOKEN, MISTRAL_API_KEY, etc.) that the backend needs to function. Users following the Docker setup will encounter errors when the backend tries to connect to AI providers without these credentials. The documentation should mention creating a backend/.env file from backend/.env.example before running docker compose up.

Copilot uses AI. Check for mistakes.

#### Option B: Manual Setup (FastAPI)

```bash
cd backend
Expand Down
27 changes: 27 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use official Python 3.11 slim image
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

# Install system dependencies (if any needed, e.g. for build tools)
# RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*

# Copy requirements first to leverage cache
COPY requirements.txt .

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Expose the API port
EXPOSE 5000

# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"]
Comment on lines +26 to +27
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dockerfile doesn't specify a non-root user to run the application. Running containers as root is a security risk. Best practice is to create a non-privileged user and switch to it before running the application. This is especially important for production deployments.

Copilot uses AI. Check for mistakes.
21 changes: 21 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@


Comment on lines +1 to +2
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker-compose.yml file is missing the version field at the top. While this is optional in Docker Compose v2+, it's considered a best practice to include it for clarity and compatibility. Most Docker Compose files in production use "version: '3.8'" or similar to explicitly declare the Compose file format version.

Suggested change
version: '3.8'

Copilot uses AI. Check for mistakes.
services:
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title is "Dockerized the project" and the description extensively mentions Skills.md prompt enhancements, test suite additions, and changes to agent.js and fileOps.js. However, the actual code changes in this PR only include Docker configuration files (Dockerfile, docker-compose.yml, .dockerignore) and documentation updates. The Skills.md-related changes mentioned in the description are not present in this PR's diffs, suggesting they may have been merged previously or are being mistakenly included in this PR's description. The PR description should be updated to accurately reflect only the Docker-related changes.

Copilot uses AI. Check for mistakes.
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: coderrr-backend
ports:
- "5000:5000"
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend port configuration is inconsistent. The docker-compose.yml maps port 5000:5000 and the backend Dockerfile exposes port 5000, but the root .env.example file shows CODERRR_BACKEND=http://localhost:8000. According to the coding guidelines, the backend runs on port 5000, not 8000. When users run the backend via Docker, the CLI will try to connect to port 8000 by default (from .env.example), which will fail to connect to the Dockerized backend on port 5000.

Suggested change
- "5000:5000"
- "8000:5000"

Copilot uses AI. Check for mistakes.
volumes:
- ./backend:/app
Comment on lines +11 to +12
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The volume mount "./backend:/app" creates a bind mount that overwrites the /app directory in the container with the local backend directory. This means that the Python dependencies installed during the Docker build (in the image) will be overwritten by the local directory contents, which doesn't include the installed packages. This will cause import errors when the backend tries to import dependencies like fastapi, uvicorn, etc.

A common solution is to use a named volume for the Python packages directory or exclude it from the bind mount. Alternatively, dependencies could be installed in the command, but this defeats the purpose of using Docker layers for caching.

Copilot uses AI. Check for mistakes.
environment:
- HOST=0.0.0.0
- PORT=5000
Comment on lines +13 to +15
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variables HOST and PORT are set in docker-compose.yml but these are not standard environment variables used by uvicorn. Uvicorn reads command-line arguments or standard variables. The --host and --port flags in the command override these environment variables anyway, making them redundant. If the intention was to make these configurable, they should be used in the command like: command: uvicorn main:app --host ${HOST:-0.0.0.0} --port ${PORT:-5000} --reload

Suggested change
environment:
- HOST=0.0.0.0
- PORT=5000

Copilot uses AI. Check for mistakes.
# Load environment variables from .env file if it exists
env_file:
- .env
Comment on lines +16 to +18
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .env file path is specified as a root-level file, but based on the project structure, API keys and secrets should be in backend/.env (as indicated by backend/.env.example). The env_file path should be "./backend/.env" to correctly load the backend's environment variables containing GITHUB_TOKEN, MISTRAL_API_KEY, etc. The current configuration will fail to load API credentials needed for the AI backend to function.

Suggested change
# Load environment variables from .env file if it exists
env_file:
- .env
# Load environment variables from backend/.env file if it exists
env_file:
- ./backend/.env

Copilot uses AI. Check for mistakes.
# Override command to enable hot reloading for development
command: uvicorn main:app --host 0.0.0.0 --port 5000 --reload
restart: unless-stopped
42 changes: 36 additions & 6 deletions src/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/fileOps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
];

Expand Down
194 changes: 194 additions & 0 deletions tests/unit/skills.test.js
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 });
}
});

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');
});
});
});
Loading