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

# Environment
.env
Comment on lines +1 to +37
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 is placed in the repository root, but the Docker build context in docker-compose.yml is set to ./backend. Docker will look for .dockerignore in the build context directory (backend/), not in the repository root. This .dockerignore file will not be used during the Docker build. Either move this file to the backend/ directory, or change the build context to the repository root and adjust the Dockerfile accordingly.

Copilot uses AI. Check for mistakes.
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 .env files from the Docker build, but the docker-compose.yml uses env_file: - .env to load environment variables. This creates a dependency on a .env file that must exist at the repository root. While excluding .env from the image is correct for security, users need clear documentation about creating this file. The README should include instructions to copy .env.example to .env and configure it before running docker compose.

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 are missing critical steps. Before running docker compose up --build, users need to:

  1. Create a .env file at the repository root (copy from .env.example or backend/.env.example)
  2. Configure the required environment variables (GITHUB_TOKEN, GITHUB_MODELS_ENDPOINT, GITHUB_MODEL)

Without these steps, the container will start but the backend won't be functional due to missing API credentials. Add a step before line 361 instructing users to set up the .env file.

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
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.

Using a specific Python version (3.11-slim) in the Dockerfile is good, but this should match the Python version requirements in the project documentation. Consider documenting the required Python version in the README if not already present, or using a more flexible base image tag like python:3.11-slim (which auto-updates patch versions) versus a fully pinned version like python:3.11.x-slim depending on your stability requirements.

Suggested change
# Use official Python 3.11 slim image
FROM python:3.11-slim
# Use official Python 3.11.x slim image (pinned patch version for reproducibility)
FROM python:3.11.8-slim

Copilot uses AI. Check for mistakes.

# Set working directory
WORKDIR /app

# Set environment variables
Comment on lines +6 to +7
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 description extensively discusses Skills.md support, prompt construction changes, and unit tests (mentioning files like src/agent.js, Skills.md, and skills.test.js), but none of these changes are present in the actual diff. The diff only contains Docker-related files. This creates confusion about what is actually being changed in this PR. Please update the PR description to accurately reflect only the Docker-related changes, or include the missing Skills.md-related changes in the PR if they should be part of it.

Copilot uses AI. Check for mistakes.
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/*
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 commented line suggests that gcc might be needed for building some Python packages, but it's unclear if this is actually required. If any of the dependencies in requirements.txt need compilation (like some cryptography or data science libraries), the build will fail without these system dependencies. Either uncomment this line if build tools are needed, or remove the comment if all dependencies are pure Python packages. You can verify by testing the Docker build.

Suggested change
# 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/*
# Install system dependencies required for building Python packages
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*

Copilot uses AI. Check for mistakes.

# 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"]
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 has two empty lines at the beginning. While this doesn't affect functionality, it's unconventional and should be removed for cleaner code formatting.

Suggested change

Copilot uses AI. Check for mistakes.
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 a version specification. While Docker Compose v2+ doesn't strictly require a version field (it's optional), it's considered best practice to include it for clarity and backwards compatibility. Add version: '3.8' or version: '3.9' at the top of the file (after removing the empty lines) to explicitly declare the Compose file format version being used.

Suggested change
version: '3.9'

Copilot uses AI. Check for mistakes.
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: coderrr-backend
ports:
- "5000:5000"
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 overrides the entire /app directory in the container, including the Python dependencies installed by the Dockerfile. This defeats the purpose of the pip install step in the Dockerfile. For development with hot-reloading to work properly, you have two options:

  1. Use a named volume for Python packages: add - backend-deps:/usr/local/lib/python3.11/site-packages to preserve installed packages
  2. Document that developers need to run pip install -r requirements.txt in their local backend/ directory

Without this, the container will fail to start because the required Python packages won't be available at runtime.

Copilot uses AI. Check for mistakes.
environment:
- HOST=0.0.0.0
- PORT=5000
# 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 docker-compose.yml file references a .env file that should be located at the repository root, but this .env file path is not documented in the README. Users following the Docker setup instructions will need to create this .env file with the appropriate backend environment variables (like GITHUB_TOKEN, GITHUB_MODELS_ENDPOINT, GITHUB_MODEL, etc.). Consider adding explicit instructions in the README about creating the root .env file from .env.example before running docker compose.

Suggested change
# Load environment variables from .env file if it exists
env_file:
- .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
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.

Consider adding a health check to monitor the backend service status. The backend has a /health endpoint (see backend/main.py line 267) that can be used. Add a healthcheck configuration like:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

This will help Docker monitor the service and restart it if it becomes unhealthy. Note: you'll need to add curl to the Dockerfile if not already present.

Suggested change
command: uvicorn main:app --host 0.0.0.0 --port 5000 --reload
command: uvicorn main:app --host 0.0.0.0 --port 5000 --reload
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

Copilot uses AI. Check for mistakes.
restart: unless-stopped
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 restart policy unless-stopped is appropriate for development, but it means the container will automatically restart on system boot. For a development environment, restart: "no" or restart: on-failure might be more appropriate to avoid unnecessary resource usage when not actively developing. The current setting is more suited for production deployments.

Suggested change
restart: unless-stopped
restart: "no"

Copilot uses AI. Check for mistakes.
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