Thoroughly explore the Autocoder-2.0 project at C:/Users/MikeSwanson/claude-projects/Autocode-remix/Autocoder-2.0

Document:
1. Complete project structure (all directories and key files)
2. All Python modules and their purposes
3. All server API endpoints (from server/routers/)
4. All UI components (from ui/src/components/)
5. Database schema (from api/database.py)
6. MCP server tools (from mcp_server/)
7. Prompt templates (from .claude/templates/)
8. Configuration options (from config.py)

Be thorough - read key files to understand their full functionality. This is for documentation purposes.


I'll thoroughly explore the Autocoder-2.0 project and document its structure, components, and functionality. Let me start by examining the project structure and key files.


Bash

cd "C:/Users/MikeSwanson/claude-projects/Autocode-remix/Autocoder-2.0" && find . -type f -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.json" -o -name "*.md" | head -100


./.claude/commands/checkpoint.md
./.claude/commands/create-spec.md
./.claude/commands/import-spec.md
./.claude/settings.json
./.claude/skills/frontend-design/SKILL.md
./.claude/templates/coding_prompt.template.md
./.claude/templates/coding_prompt_yolo.template.md
./.claude/templates/initializer_prompt.template.md
./agent.py
./api/database.py
./api/migration.py
./api/__init__.py
./autonomous_agent_demo.py
./CLAUDE.md
./client.py
./config.py
./LICENSE.md
./mcp_server/feature_mcp.py
./mcp_server/__init__.py
./parallel_agent.py
./progress.py
./prompts.py
./README.md
./registry.py
./SAMPLE_PROMPT.md
./security.py
./server/agents/git_agent.py
./server/agents/__init__.py
./server/main.py
./server/routers/agent.py
./server/routers/assistant_chat.py
./server/routers/features.py
./server/routers/filesystem.py
./server/routers/git.py
./server/routers/projects.py
./server/routers/scheduler.py
./server/routers/spec_creation.py
./server/routers/terminal.py
./server/routers/__init__.py
./server/schemas.py
./server/services/assistant_chat_session.py
./server/services/assistant_database.py
./server/services/credential_resolver.py
./server/services/feature_graph.py
./server/services/file_lock.py
./server/services/git_operations.py
./server/services/process_manager.py
./server/services/resource_manager.py
./server/services/smart_scheduler.py
./server/services/spec_chat_session.py
./server/services/terminal_session.py
./server/services/__init__.py
./server/websocket.py
./server/__init__.py
./smart_orchestrator.py
./start.py
./start_ui.py
./test_security.py
./ui/node_modules/.package-lock.json
./ui/node_modules/@babel/code-frame/package.json
./ui/node_modules/@babel/code-frame/README.md
./ui/node_modules/@babel/compat-data/data/corejs2-built-ins.json
./ui/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.json
./ui/node_modules/@babel/compat-data/data/native-modules.json
./ui/node_modules/@babel/compat-data/data/overlapping-plugins.json
./ui/node_modules/@babel/compat-data/data/plugin-bugfixes.json
./ui/node_modules/@babel/compat-data/data/plugins.json
./ui/node_modules/@babel/compat-data/package.json
./ui/node_modules/@babel/compat-data/README.md
./ui/node_modules/@babel/core/package.json
./ui/node_modules/@babel/core/README.md
./ui/node_modules/@babel/core/src/config/files/index-browser.ts
./ui/node_modules/@babel/core/src/config/files/index.ts
./ui/node_modules/@babel/core/src/config/resolve-targets-browser.ts
./ui/node_modules/@babel/core/src/config/resolve-targets.ts
./ui/node_modules/@babel/core/src/transform-file-browser.ts
./ui/node_modules/@babel/core/src/transform-file.ts
./ui/node_modules/@babel/generator/package.json
./ui/node_modules/@babel/generator/README.md
./ui/node_modules/@babel/helper-compilation-targets/package.json
./ui/node_modules/@babel/helper-compilation-targets/README.md
./ui/node_modules/@babel/helper-globals/data/browser-upper.json
./ui/node_modules/@babel/helper-globals/data/builtin-lower.json
./ui/node_modules/@babel/helper-globals/data/builtin-upper.json
./ui/node_modules/@babel/helper-globals/package.json
./ui/node_modules/@babel/helper-globals/README.md
./ui/node_modules/@babel/helper-module-imports/package.json
./ui/node_modules/@babel/helper-module-imports/README.md
./ui/node_modules/@babel/helper-module-transforms/package.json
./ui/node_modules/@babel/helper-module-transforms/README.md
./ui/node_modules/@babel/helper-plugin-utils/package.json
./ui/node_modules/@babel/helper-plugin-utils/README.md
./ui/node_modules/@babel/helper-string-parser/package.json
./ui/node_modules/@babel/helper-string-parser/README.md
./ui/node_modules/@babel/helper-validator-identifier/package.json
./ui/node_modules/@babel/helper-validator-identifier/README.md
./ui/node_modules/@babel/helper-validator-option/package.json
./ui/node_modules/@babel/helper-validator-option/README.md
./ui/node_modules/@babel/helpers/package.json
./ui/node_modules/@babel/helpers/README.md


Let me get a cleaner directory structure view and explore the key files:


cd "C:/Users/MikeSwanson/claude-projects/Autocode-remix/Autocoder-2.0" && find . -maxdepth 3 -type d | grep -v node_modules | sort


.
./.claude
./.claude/commands
./.claude/skills
./.claude/skills/frontend-design
./.claude/templates
./.github
./.github/workflows
./__pycache__
./api
./api/__pycache__
./mcp_server
./server
./server/__pycache__
./server/agents
./server/agents/__pycache__
./server/routers
./server/routers/__pycache__
./server/services
./server/services/__pycache__
./ui
./ui/dist
./ui/dist/assets
./ui/public
./ui/src
./ui/src/components
./ui/src/hooks
./ui/src/lib
./ui/src/styles
./venv
./venv/Include
./venv/Include/site
./venv/Lib
./venv/Lib/site-packages
./venv/Scripts
./venv/Scripts/__pycache__


Now let me read the main documentation and key files:


     1→# AutoCoder
     2→
     3→[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl)
     4→
     5→A long-running autonomous coding agent powered by the Claude Agent SDK. This tool can build complete applications over multiple sessions using a two-agent pattern (initializer + coding agent). Includes a React-based UI for monitoring progress in real-time.
     6→
     7→## Video Tutorial
     8→
     9→[![Watch the tutorial](https://img.youtube.com/vi/lGWFlpffWk4/hqdefault.jpg)](https://youtu.be/lGWFlpffWk4)
    10→
    11→> **[Watch the setup and usage guide →](https://youtu.be/lGWFlpffWk4)**
    12→
    13→---
    14→
    15→## Prerequisites
    16→
    17→### Claude Code CLI (Required)
    18→
    19→This project requires the Claude Code CLI to be installed. Install it using one of these methods:
    20→
    21→**macOS / Linux:**
    22→```bash
    23→curl -fsSL https://claude.ai/install.sh | bash
    24→```
    25→
    26→**Windows (PowerShell):**
    27→```powershell
    28→irm https://claude.ai/install.ps1 | iex
    29→```
    30→
    31→### Authentication
    32→
    33→You need one of the following:
    34→
    35→- **Claude Pro/Max Subscription** - Use `claude login` to authenticate (recommended)
    36→- **Anthropic API Key** - Pay-per-use from https://console.anthropic.com/
    37→
    38→---
    39→
    40→## Quick Start
    41→
    42→### Option 1: Web UI (Recommended)
    43→
    44→**Windows:**
    45→```cmd
    46→start_ui.bat
    47→```
    48→
    49→**macOS / Linux:**
    50→```bash
    51→./start_ui.sh
    52→```
    53→
    54→This launches the React-based web UI at `http://localhost:5173` with:
    55→- Project selection and creation
    56→- Kanban board view of features
    57→- Real-time agent output streaming
    58→- Start/pause/stop controls
    59→
    60→### Option 2: CLI Mode
    61→
    62→**Windows:**
    63→```cmd
    64→start.bat
    65→```
    66→
    67→**macOS / Linux:**
    68→```bash
    69→./start.sh
    70→```
    71→
    72→The start script will:
    73→1. Check if Claude CLI is installed
    74→2. Check if you're authenticated (prompt to run `claude login` if not)
    75→3. Create a Python virtual environment
    76→4. Install dependencies
    77→5. Launch the main menu
    78→
    79→### Creating or Continuing a Project
    80→
    81→You'll see options to:
    82→- **Create new project** - Start a fresh project with AI-assisted spec generation
    83→- **Continue existing project** - Resume work on a previous project
    84→
    85→For new projects, you can use the built-in `/create-spec` command to interactively create your app specification with Claude's help.
    86→
    87→---
    88→
    89→## How It Works
    90→
    91→### Two-Agent Pattern
    92→
    93→1. **Initializer Agent (First Session):** Reads your app specification, creates features in a SQLite database (`features.db`), sets up the project structure, and initializes git.
    94→
    95→2. **Coding Agent (Subsequent Sessions):** Picks up where the previous session left off, implements features one by one, and marks them as passing in the database.
    96→
    97→### Feature Management
    98→
    99→Features are stored in SQLite via SQLAlchemy and managed through an MCP server that exposes tools to the agent:
   100→- `feature_get_stats` - Progress statistics
   101→- `feature_get_next` - Get highest-priority pending feature
   102→- `feature_get_for_regression` - Random passing features for regression testing
   103→- `feature_mark_passing` - Mark feature complete
   104→- `feature_skip` - Move feature to end of queue
   105→- `feature_create_bulk` - Initialize all features (used by initializer)
   106→
   107→### Session Management
   108→
   109→- Each session runs with a fresh context window
   110→- Progress is persisted via SQLite database and git commits
   111→- The agent auto-continues between sessions (3 second delay)
   112→- Press `Ctrl+C` to pause; run the start script again to resume
   113→
   114→---
   115→
   116→## Important Timing Expectations
   117→
   118→> **Note: Building complete applications takes time!**
   119→
   120→- **First session (initialization):** The agent generates feature test cases. This takes several minutes and may appear to hang - this is normal.
   121→
   122→- **Subsequent sessions:** Each coding iteration can take **5-15 minutes** depending on complexity.
   123→
   124→- **Full app:** Building all features typically requires **many hours** of total runtime across multiple sessions.
   125→
   126→**Tip:** The feature count in the prompts determines scope. For faster demos, you can modify your app spec to target fewer features (e.g., 20-50 features for a quick demo).
   127→
   128→---
   129→
   130→## Project Structure
   131→
   132→```
   133→autonomous-coding/
   134→├── start.bat                 # Windows CLI start script
   135→├── start.sh                  # macOS/Linux CLI start script
   136→├── start_ui.bat              # Windows Web UI start script
   137→├── start_ui.sh               # macOS/Linux Web UI start script
   138→├── start.py                  # CLI menu and project management
   139→├── start_ui.py               # Web UI backend (FastAPI server launcher)
   140→├── autonomous_agent_demo.py  # Agent entry point
   141→├── agent.py                  # Agent session logic
   142→├── client.py                 # Claude SDK client configuration
   143→├── security.py               # Bash command allowlist and validation
   144→├── progress.py               # Progress tracking utilities
   145→├── prompts.py                # Prompt loading utilities
   146→├── api/
   147→│   └── database.py           # SQLAlchemy models (Feature table)
   148→├── mcp_server/
   149→│   └── feature_mcp.py        # MCP server for feature management tools
   150→├── server/
   151→│   ├── main.py               # FastAPI REST API server
   152→│   ├── websocket.py          # WebSocket handler for real-time updates
   153→│   ├── schemas.py            # Pydantic schemas
   154→│   ├── routers/              # API route handlers
   155→│   └── services/             # Business logic services
   156→├── ui/                       # React frontend
   157→│   ├── src/
   158→│   │   ├── App.tsx           # Main app component
   159→│   │   ├── hooks/            # React Query and WebSocket hooks
   160→│   │   └── lib/              # API client and types
   161→│   ├── package.json
   162→│   └── vite.config.ts
   163→├── .claude/
   164→│   ├── commands/
   165→│   │   └── create-spec.md    # /create-spec slash command
   166→│   ├── skills/               # Claude Code skills
   167→│   └── templates/            # Prompt templates
   168→├── generations/              # Generated projects go here
   169→├── requirements.txt          # Python dependencies
   170→└── .env                      # Optional configuration (N8N webhook)
   171→```
   172→
   173→---
   174→
   175→## Generated Project Structure
   176→
   177→After the agent runs, your project directory will contain:
   178→
   179→```
   180→generations/my_project/
   181→├── features.db               # SQLite database (feature test cases)
   182→├── prompts/
   183→│   ├── app_spec.txt          # Your app specification
   184→│   ├── initializer_prompt.md # First session prompt
   185→│   └── coding_prompt.md      # Continuation session prompt
   186→├── init.sh                   # Environment setup script
   187→├── claude-progress.txt       # Session progress notes
   188→└── [application files]       # Generated application code
   189→```
   190→
   191→---
   192→
   193→## Running the Generated Application
   194→
   195→After the agent completes (or pauses), you can run the generated application:
   196→
   197→```bash
   198→cd generations/my_project
   199→
   200→# Run the setup script created by the agent
   201→./init.sh
   202→
   203→# Or manually (typical for Node.js apps):
   204→npm install
   205→npm run dev
   206→```
   207→
   208→The application will typically be available at `http://localhost:3000` or similar.
   209→
   210→---
   211→
   212→## Security Model
   213→
   214→This project uses a defense-in-depth security approach (see `security.py` and `client.py`):
   215→
   216→1. **OS-level Sandbox:** Bash commands run in an isolated environment
   217→2. **Filesystem Restrictions:** File operations restricted to the project directory only
   218→3. **Bash Allowlist:** Only specific commands are permitted:
   219→   - File inspection: `ls`, `cat`, `head`, `tail`, `wc`, `grep`
   220→   - Node.js: `npm`, `node`
   221→   - Version control: `git`
   222→   - Process management: `ps`, `lsof`, `sleep`, `pkill` (dev processes only)
   223→
   224→Commands not in the allowlist are blocked by the security hook.
   225→
   226→---
   227→
   228→## Web UI Development
   229→
   230→The React UI is located in the `ui/` directory.
   231→
   232→### Development Mode
   233→
   234→```bash
   235→cd ui
   236→npm install
   237→npm run dev      # Development server with hot reload
   238→```
   239→
   240→### Building for Production
   241→
   242→```bash
   243→cd ui
   244→npm run build    # Builds to ui/dist/
   245→```
   246→
   247→**Note:** The `start_ui.bat`/`start_ui.sh` scripts serve the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` to see them when using the start scripts.
   248→
   249→### Tech Stack
   250→
   251→- React 18 with TypeScript
   252→- TanStack Query for data fetching
   253→- Tailwind CSS v4 with neobrutalism design
   254→- Radix UI components
   255→- WebSocket for real-time updates
   256→
   257→### Real-time Updates
   258→
   259→The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
   260→- `progress` - Test pass counts
   261→- `agent_status` - Running/paused/stopped/crashed
   262→- `log` - Agent output lines (streamed from subprocess stdout)
   263→- `feature_update` - Feature status changes
   264→
   265→---
   266→
   267→## Configuration (Optional)
   268→
   269→### N8N Webhook Integration
   270→
   271→The agent can send progress notifications to an N8N webhook. Create a `.env` file:
   272→
   273→```bash
   274→# Optional: N8N webhook for progress notifications
   275→PROGRESS_N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/your-webhook-id
   276→```
   277→
   278→When test progress increases, the agent sends:
   279→
   280→```json
   281→{
   282→  "event": "test_progress",
   283→  "passing": 45,
   284→  "total": 200,
   285→  "percentage": 22.5,
   286→  "project": "my_project",
   287→  "timestamp": "2025-01-15T14:30:00.000Z"
   288→}
   289→```
   290→
   291→---
   292→
   293→## Customization
   294→
   295→### Changing the Application
   296→
   297→Use the `/create-spec` command when creating a new project, or manually edit the files in your project's `prompts/` directory:
   298→- `app_spec.txt` - Your application specification
   299→- `initializer_prompt.md` - Controls feature generation
   300→
   301→### Modifying Allowed Commands
   302→
   303→Edit `security.py` to add or remove commands from `ALLOWED_COMMANDS`.
   304→
   305→---
   306→
   307→## Troubleshooting
   308→
   309→**"Claude CLI not found"**
   310→Install the Claude Code CLI using the instructions in the Prerequisites section.
   311→
   312→**"Not authenticated with Claude"**
   313→Run `claude login` to authenticate. The start script will prompt you to do this automatically.
   314→
   315→**"Appears to hang on first run"**
   316→This is normal. The initializer agent is generating detailed test cases, which takes significant time. Watch for `[Tool: ...]` output to confirm the agent is working.
   317→
   318→**"Command blocked by security hook"**
   319→The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.
   320→
   321→---
   322→
   323→## License
   324→
   325→This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details.
   326→Copyright (C) 2026 Leon van Zyl (https://leonvanzyl.com)
   327→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

# AutoCoder

[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl)

A long-running autonomous coding agent powered by the Claude Agent SDK. This tool can build complete applications over multiple sessions using a two-agent pattern (initializer + coding agent). Includes a React-based UI for monitoring progress in real-time.

## Video Tutorial

[![Watch the tutorial](https://img.youtube.com/vi/lGWFlpffWk4/hqdefault.jpg)](https://youtu.be/lGWFlpffWk4)

> **[Watch the setup and usage guide →](https://youtu.be/lGWFlpffWk4)**

---

## Prerequisites

### Claude Code CLI (Required)

This project requires the Claude Code CLI to be installed. Install it using one of these methods:

**macOS / Linux:**
```bash
curl -fsSL https://claude.ai/install.sh | bash
```

**Windows (PowerShell):**
```powershell
irm https://claude.ai/install.ps1 | iex
```

### Authentication

You need one of the following:

- **Claude Pro/Max Subscription** - Use `claude login` to authenticate (recommended)
- **Anthropic API Key** - Pay-per-use from https://console.anthropic.com/

---

## Quick Start

### Option 1: Web UI (Recommended)

**Windows:**
```cmd
start_ui.bat
```

**macOS / Linux:**
```bash
./start_ui.sh
```

This launches the React-based web UI at `http://localhost:5173` with:
- Project selection and creation
- Kanban board view of features
- Real-time agent output streaming
- Start/pause/stop controls

### Option 2: CLI Mode

**Windows:**
```cmd
start.bat
```

**macOS / Linux:**
```bash
./start.sh
```

The start script will:
1. Check if Claude CLI is installed
2. Check if you're authenticated (prompt to run `claude login` if not)
3. Create a Python virtual environment
4. Install dependencies
5. Launch the main menu

### Creating or Continuing a Project

You'll see options to:
- **Create new project** - Start a fresh project with AI-assisted spec generation
- **Continue existing project** - Resume work on a previous project

For new projects, you can use the built-in `/create-spec` command to interactively create your app specification with Claude's help.

---

## How It Works

### Two-Agent Pattern

1. **Initializer Agent (First Session):** Reads your app specification, creates features in a SQLite database (`features.db`), sets up the project structure, and initializes git.

2. **Coding Agent (Subsequent Sessions):** Picks up where the previous session left off, implements features one by one, and marks them as passing in the database.

### Feature Management

Features are stored in SQLite via SQLAlchemy and managed through an MCP server that exposes tools to the agent:
- `feature_get_stats` - Progress statistics
- `feature_get_next` - Get highest-priority pending feature
- `feature_get_for_regression` - Random passing features for regression testing
- `feature_mark_passing` - Mark feature complete
- `feature_skip` - Move feature to end of queue
- `feature_create_bulk` - Initialize all features (used by initializer)

### Session Management

- Each session runs with a fresh context window
- Progress is persisted via SQLite database and git commits
- The agent auto-continues between sessions (3 second delay)
- Press `Ctrl+C` to pause; run the start script again to resume

---

## Important Timing Expectations

> **Note: Building complete applications takes time!**

- **First session (initialization):** The agent generates feature test cases. This takes several minutes and may appear to hang - this is normal.

- **Subsequent sessions:** Each coding iteration can take **5-15 minutes** depending on complexity.

- **Full app:** Building all features typically requires **many hours** of total runtime across multiple sessions.

**Tip:** The feature count in the prompts determines scope. For faster demos, you can modify your app spec to target fewer features (e.g., 20-50 features for a quick demo).

---

## Project Structure

```
autonomous-coding/
├── start.bat                 # Windows CLI start script
├── start.sh                  # macOS/Linux CLI start script
├── start_ui.bat              # Windows Web UI start script
├── start_ui.sh               # macOS/Linux Web UI start script
├── start.py                  # CLI menu and project management
├── start_ui.py               # Web UI backend (FastAPI server launcher)
├── autonomous_agent_demo.py  # Agent entry point
├── agent.py                  # Agent session logic
├── client.py                 # Claude SDK client configuration
├── security.py               # Bash command allowlist and validation
├── progress.py               # Progress tracking utilities
├── prompts.py                # Prompt loading utilities
├── api/
│   └── database.py           # SQLAlchemy models (Feature table)
├── mcp_server/
│   └── feature_mcp.py        # MCP server for feature management tools
├── server/
│   ├── main.py               # FastAPI REST API server
│   ├── websocket.py          # WebSocket handler for real-time updates
│   ├── schemas.py            # Pydantic schemas
│   ├── routers/              # API route handlers
│   └── services/             # Business logic services
├── ui/                       # React frontend
│   ├── src/
│   │   ├── App.tsx           # Main app component
│   │   ├── hooks/            # React Query and WebSocket hooks
│   │   └── lib/              # API client and types
│   ├── package.json
│   └── vite.config.ts
├── .claude/
│   ├── commands/
│   │   └── create-spec.md    # /create-spec slash command
│   ├── skills/               # Claude Code skills
│   └── templates/            # Prompt templates
├── generations/              # Generated projects go here
├── requirements.txt          # Python dependencies
└── .env                      # Optional configuration (N8N webhook)
```

---

## Generated Project Structure

After the agent runs, your project directory will contain:

```
generations/my_project/
├── features.db               # SQLite database (feature test cases)
├── prompts/
│   ├── app_spec.txt          # Your app specification
│   ├── initializer_prompt.md # First session prompt
│   └── coding_prompt.md      # Continuation session prompt
├── init.sh                   # Environment setup script
├── claude-progress.txt       # Session progress notes
└── [application files]       # Generated application code
```

---

## Running the Generated Application

After the agent completes (or pauses), you can run the generated application:

```bash
cd generations/my_project

# Run the setup script created by the agent
./init.sh

# Or manually (typical for Node.js apps):
npm install
npm run dev
```

The application will typically be available at `http://localhost:3000` or similar.

---

## Security Model

This project uses a defense-in-depth security approach (see `security.py` and `client.py`):

1. **OS-level Sandbox:** Bash commands run in an isolated environment
2. **Filesystem Restrictions:** File operations restricted to the project directory only
3. **Bash Allowlist:** Only specific commands are permitted:
   - File inspection: `ls`, `cat`, `head`, `tail`, `wc`, `grep`
   - Node.js: `npm`, `node`
   - Version control: `git`
   - Process management: `ps`, `lsof`, `sleep`, `pkill` (dev processes only)

Commands not in the allowlist are blocked by the security hook.

---

## Web UI Development

The React UI is located in the `ui/` directory.

### Development Mode

```bash
cd ui
npm install
npm run dev      # Development server with hot reload
```

### Building for Production

```bash
cd ui
npm run build    # Builds to ui/dist/
```

**Note:** The `start_ui.bat`/`start_ui.sh` scripts serve the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` to see them when using the start scripts.

### Tech Stack

- React 18 with TypeScript
- TanStack Query for data fetching
- Tailwind CSS v4 with neobrutalism design
- Radix UI components
- WebSocket for real-time updates

### Real-time Updates

The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
- `progress` - Test pass counts
- `agent_status` - Running/paused/stopped/crashed
- `log` - Agent output lines (streamed from subprocess stdout)
- `feature_update` - Feature status changes

---

## Configuration (Optional)

### N8N Webhook Integration

The agent can send progress notifications to an N8N webhook. Create a `.env` file:

```bash
# Optional: N8N webhook for progress notifications
PROGRESS_N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/your-webhook-id
```

When test progress increases, the agent sends:

```json
{
  "event": "test_progress",
  "passing": 45,
  "total": 200,
  "percentage": 22.5,
  "project": "my_project",
  "timestamp": "2025-01-15T14:30:00.000Z"
}
```

---

## Customization

### Changing the Application

Use the `/create-spec` command when creating a new project, or manually edit the files in your project's `prompts/` directory:
- `app_spec.txt` - Your application specification
- `initializer_prompt.md` - Controls feature generation

### Modifying Allowed Commands

Edit `security.py` to add or remove commands from `ALLOWED_COMMANDS`.

---

## Troubleshooting

**"Claude CLI not found"**
Install the Claude Code CLI using the instructions in the Prerequisites section.

**"Not authenticated with Claude"**
Run `claude login` to authenticate. The start script will prompt you to do this automatically.

**"Appears to hang on first run"**
This is normal. The initializer agent is generating detailed test cases, which takes significant time. Watch for `[Tool: ...]` output to confirm the agent is working.

**"Command blocked by security hook"**
The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.

---

## License

This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details.
Copyright (C) 2026 Leon van Zyl (https://leonvanzyl.com)


     1→# CLAUDE.md
     2→
     3→This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
     4→
     5→## Project Overview
     6→
     7→This is an autonomous coding agent system with a React-based UI. It uses the Claude Agent SDK to build complete applications over multiple sessions using a two-agent pattern:
     8→
     9→1. **Initializer Agent** - First session reads an app spec and creates features in a SQLite database
    10→2. **Coding Agent** - Subsequent sessions implement features one by one, marking them as passing
    11→
    12→## Commands
    13→
    14→### Quick Start (Recommended)
    15→
    16→```bash
    17→# Windows - launches CLI menu
    18→start.bat
    19→
    20→# macOS/Linux
    21→./start.sh
    22→
    23→# Launch Web UI (serves pre-built React app)
    24→start_ui.bat      # Windows
    25→./start_ui.sh     # macOS/Linux
    26→```
    27→
    28→### Python Backend (Manual)
    29→
    30→```bash
    31→# Create and activate virtual environment
    32→python -m venv venv
    33→venv\Scripts\activate  # Windows
    34→source venv/bin/activate  # macOS/Linux
    35→
    36→# Install dependencies
    37→pip install -r requirements.txt
    38→
    39→# Run the main CLI launcher
    40→python start.py
    41→
    42→# Run agent directly for a project (use absolute path or registered name)
    43→python autonomous_agent_demo.py --project-dir C:/Projects/my-app
    44→python autonomous_agent_demo.py --project-dir my-app  # if registered
    45→
    46→# YOLO mode: rapid prototyping without browser testing
    47→python autonomous_agent_demo.py --project-dir my-app --yolo
    48→```
    49→
    50→### YOLO Mode (Rapid Prototyping)
    51→
    52→YOLO mode skips all testing for faster feature iteration:
    53→
    54→```bash
    55→# CLI
    56→python autonomous_agent_demo.py --project-dir my-app --yolo
    57→
    58→# UI: Toggle the lightning bolt button before starting the agent
    59→```
    60→
    61→**What's different in YOLO mode:**
    62→- No regression testing (skips `feature_get_for_regression`)
    63→- No Playwright MCP server (browser automation disabled)
    64→- Features marked passing after lint/type-check succeeds
    65→- Faster iteration for prototyping
    66→
    67→**What's the same:**
    68→- Lint and type-check still run to verify code compiles
    69→- Feature MCP server for tracking progress
    70→- All other development tools available
    71→
    72→**When to use:** Early prototyping when you want to quickly scaffold features without verification overhead. Switch back to standard mode for production-quality development.
    73→
    74→### React UI (in ui/ directory)
    75→
    76→```bash
    77→cd ui
    78→npm install
    79→npm run dev      # Development server (hot reload)
    80→npm run build    # Production build (required for start_ui.bat)
    81→npm run lint     # Run ESLint
    82→```
    83→
    84→**Note:** The `start_ui.bat` script serves the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` in the `ui/` directory.
    85→
    86→## Architecture
    87→
    88→### Core Python Modules
    89→
    90→- `start.py` - CLI launcher with project creation/selection menu
    91→- `autonomous_agent_demo.py` - Entry point for running the agent
    92→- `agent.py` - Agent session loop using Claude Agent SDK
    93→- `client.py` - ClaudeSDKClient configuration with security hooks and MCP servers
    94→- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
    95→- `prompts.py` - Prompt template loading with project-specific fallback
    96→- `progress.py` - Progress tracking, database queries, webhook notifications
    97→- `registry.py` - Project registry for mapping names to paths (cross-platform)
    98→
    99→### Project Registry
   100→
   101→Projects can be stored in any directory. The registry maps project names to paths using SQLite:
   102→- **All platforms**: `~/.autocoder/registry.db`
   103→
   104→The registry uses:
   105→- SQLite database with SQLAlchemy ORM
   106→- POSIX path format (forward slashes) for cross-platform compatibility
   107→- SQLite's built-in transaction handling for concurrency safety
   108→
   109→### Server API (server/)
   110→
   111→The FastAPI server provides REST endpoints for the UI:
   112→
   113→- `server/routers/projects.py` - Project CRUD with registry integration
   114→- `server/routers/features.py` - Feature management
   115→- `server/routers/agent.py` - Agent control (start/stop/pause/resume)
   116→- `server/routers/filesystem.py` - Filesystem browser API with security controls
   117→- `server/routers/spec_creation.py` - WebSocket for interactive spec creation
   118→
   119→### Feature Management
   120→
   121→Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interacts with features through an MCP server:
   122→
   123→- `mcp_server/feature_mcp.py` - MCP server exposing feature management tools
   124→- `api/database.py` - SQLAlchemy models (Feature table with priority, category, name, description, steps, passes)
   125→
   126→MCP tools available to the agent:
   127→- `feature_get_stats` - Progress statistics
   128→- `feature_get_next` - Get highest-priority pending feature
   129→- `feature_get_for_regression` - Random passing features for regression testing
   130→- `feature_mark_passing` - Mark feature complete
   131→- `feature_skip` - Move feature to end of queue
   132→- `feature_create_bulk` - Initialize all features (used by initializer)
   133→
   134→### React UI (ui/)
   135→
   136→- Tech stack: React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
   137→- `src/App.tsx` - Main app with project selection, kanban board, agent controls
   138→- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket
   139→- `src/hooks/useProjects.ts` - React Query hooks for API calls
   140→- `src/lib/api.ts` - REST API client
   141→- `src/lib/types.ts` - TypeScript type definitions
   142→- `src/components/FolderBrowser.tsx` - Server-side filesystem browser for project folder selection
   143→- `src/components/NewProjectModal.tsx` - Multi-step project creation wizard
   144→
   145→### Project Structure for Generated Apps
   146→
   147→Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains:
   148→- `prompts/app_spec.txt` - Application specification (XML format)
   149→- `prompts/initializer_prompt.md` - First session prompt
   150→- `prompts/coding_prompt.md` - Continuation session prompt
   151→- `features.db` - SQLite database with feature test cases
   152→- `.agent.lock` - Lock file to prevent multiple agent instances
   153→
   154→### Security Model
   155→
   156→Defense-in-depth approach configured in `client.py`:
   157→1. OS-level sandbox for bash commands
   158→2. Filesystem restricted to project directory only
   159→3. Bash commands validated against `ALLOWED_COMMANDS` in `security.py`
   160→
   161→## Claude Code Integration
   162→
   163→- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation
   164→- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design
   165→- `.claude/templates/` - Prompt templates copied to new projects
   166→
   167→## Key Patterns
   168→
   169→### Prompt Loading Fallback Chain
   170→
   171→1. Project-specific: `{project_dir}/prompts/{name}.md`
   172→2. Base template: `.claude/templates/{name}.template.md`
   173→
   174→### Agent Session Flow
   175→
   176→1. Check if `features.db` has features (determines initializer vs coding agent)
   177→2. Create ClaudeSDKClient with security settings
   178→3. Send prompt and stream response
   179→4. Auto-continue with 3-second delay between sessions
   180→
   181→### Real-time UI Updates
   182→
   183→The UI receives updates via WebSocket (`/ws/projects/{project_name}`):
   184→- `progress` - Test pass counts
   185→- `agent_status` - Running/paused/stopped/crashed
   186→- `log` - Agent output lines (streamed from subprocess stdout)
   187→- `feature_update` - Feature status changes
   188→
   189→### Design System
   190→
   191→The UI uses a **neobrutalism** design with Tailwind CSS v4:
   192→- CSS variables defined in `ui/src/styles/globals.css` via `@theme` directive
   193→- Custom animations: `animate-slide-in`, `animate-pulse-neo`, `animate-shimmer`
   194→- Color tokens: `--color-neo-pending` (yellow), `--color-neo-progress` (cyan), `--color-neo-done` (green)
   195→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is an autonomous coding agent system with a React-based UI. It uses the Claude Agent SDK to build complete applications over multiple sessions using a two-agent pattern:

1. **Initializer Agent** - First session reads an app spec and creates features in a SQLite database
2. **Coding Agent** - Subsequent sessions implement features one by one, marking them as passing

## Commands

### Quick Start (Recommended)

```bash
# Windows - launches CLI menu
start.bat

# macOS/Linux
./start.sh

# Launch Web UI (serves pre-built React app)
start_ui.bat      # Windows
./start_ui.sh     # macOS/Linux
```

### Python Backend (Manual)

```bash
# Create and activate virtual environment
python -m venv venv
venv\Scripts\activate  # Windows
source venv/bin/activate  # macOS/Linux

# Install dependencies
pip install -r requirements.txt

# Run the main CLI launcher
python start.py

# Run agent directly for a project (use absolute path or registered name)
python autonomous_agent_demo.py --project-dir C:/Projects/my-app
python autonomous_agent_demo.py --project-dir my-app  # if registered

# YOLO mode: rapid prototyping without browser testing
python autonomous_agent_demo.py --project-dir my-app --yolo
```

### YOLO Mode (Rapid Prototyping)

YOLO mode skips all testing for faster feature iteration:

```bash
# CLI
python autonomous_agent_demo.py --project-dir my-app --yolo

# UI: Toggle the lightning bolt button before starting the agent
```

**What's different in YOLO mode:**
- No regression testing (skips `feature_get_for_regression`)
- No Playwright MCP server (browser automation disabled)
- Features marked passing after lint/type-check succeeds
- Faster iteration for prototyping

**What's the same:**
- Lint and type-check still run to verify code compiles
- Feature MCP server for tracking progress
- All other development tools available

**When to use:** Early prototyping when you want to quickly scaffold features without verification overhead. Switch back to standard mode for production-quality development.

### React UI (in ui/ directory)

```bash
cd ui
npm install
npm run dev      # Development server (hot reload)
npm run build    # Production build (required for start_ui.bat)
npm run lint     # Run ESLint
```

**Note:** The `start_ui.bat` script serves the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` in the `ui/` directory.

## Architecture

### Core Python Modules

- `start.py` - CLI launcher with project creation/selection menu
- `autonomous_agent_demo.py` - Entry point for running the agent
- `agent.py` - Agent session loop using Claude Agent SDK
- `client.py` - ClaudeSDKClient configuration with security hooks and MCP servers
- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
- `prompts.py` - Prompt template loading with project-specific fallback
- `progress.py` - Progress tracking, database queries, webhook notifications
- `registry.py` - Project registry for mapping names to paths (cross-platform)

### Project Registry

Projects can be stored in any directory. The registry maps project names to paths using SQLite:
- **All platforms**: `~/.autocoder/registry.db`

The registry uses:
- SQLite database with SQLAlchemy ORM
- POSIX path format (forward slashes) for cross-platform compatibility
- SQLite's built-in transaction handling for concurrency safety

### Server API (server/)

The FastAPI server provides REST endpoints for the UI:

- `server/routers/projects.py` - Project CRUD with registry integration
- `server/routers/features.py` - Feature management
- `server/routers/agent.py` - Agent control (start/stop/pause/resume)
- `server/routers/filesystem.py` - Filesystem browser API with security controls
- `server/routers/spec_creation.py` - WebSocket for interactive spec creation

### Feature Management

Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interacts with features through an MCP server:

- `mcp_server/feature_mcp.py` - MCP server exposing feature management tools
- `api/database.py` - SQLAlchemy models (Feature table with priority, category, name, description, steps, passes)

MCP tools available to the agent:
- `feature_get_stats` - Progress statistics
- `feature_get_next` - Get highest-priority pending feature
- `feature_get_for_regression` - Random passing features for regression testing
- `feature_mark_passing` - Mark feature complete
- `feature_skip` - Move feature to end of queue
- `feature_create_bulk` - Initialize all features (used by initializer)

### React UI (ui/)

- Tech stack: React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
- `src/App.tsx` - Main app with project selection, kanban board, agent controls
- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket
- `src/hooks/useProjects.ts` - React Query hooks for API calls
- `src/lib/api.ts` - REST API client
- `src/lib/types.ts` - TypeScript type definitions
- `src/components/FolderBrowser.tsx` - Server-side filesystem browser for project folder selection
- `src/components/NewProjectModal.tsx` - Multi-step project creation wizard

### Project Structure for Generated Apps

Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains:
- `prompts/app_spec.txt` - Application specification (XML format)
- `prompts/initializer_prompt.md` - First session prompt
- `prompts/coding_prompt.md` - Continuation session prompt
- `features.db` - SQLite database with feature test cases
- `.agent.lock` - Lock file to prevent multiple agent instances

### Security Model

Defense-in-depth approach configured in `client.py`:
1. OS-level sandbox for bash commands
2. Filesystem restricted to project directory only
3. Bash commands validated against `ALLOWED_COMMANDS` in `security.py`

## Claude Code Integration

- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation
- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design
- `.claude/templates/` - Prompt templates copied to new projects

## Key Patterns

### Prompt Loading Fallback Chain

1. Project-specific: `{project_dir}/prompts/{name}.md`
2. Base template: `.claude/templates/{name}.template.md`

### Agent Session Flow

1. Check if `features.db` has features (determines initializer vs coding agent)
2. Create ClaudeSDKClient with security settings
3. Send prompt and stream response
4. Auto-continue with 3-second delay between sessions

### Real-time UI Updates

The UI receives updates via WebSocket (`/ws/projects/{project_name}`):
- `progress` - Test pass counts
- `agent_status` - Running/paused/stopped/crashed
- `log` - Agent output lines (streamed from subprocess stdout)
- `feature_update` - Feature status changes

### Design System

The UI uses a **neobrutalism** design with Tailwind CSS v4:
- CSS variables defined in `ui/src/styles/globals.css` via `@theme` directive
- Custom animations: `animate-slide-in`, `animate-pulse-neo`, `animate-shimmer`
- Color tokens: `--color-neo-pending` (yellow), `--color-neo-progress` (cyan), `--color-neo-done` (green)


     1→"""
     2→Autocoder Configuration
     3→=======================
     4→
     5→Centralized configuration management for Autocoder 2.0.
     6→Loads settings from ~/.autocoder/config.json with sensible defaults.
     7→"""
     8→
     9→import json
    10→import logging
    11→from dataclasses import dataclass, field, asdict
    12→from pathlib import Path
    13→from typing import Optional
    14→
    15→logger = logging.getLogger(__name__)
    16→
    17→# Default config location
    18→CONFIG_DIR = Path.home() / ".autocoder"
    19→CONFIG_FILE = CONFIG_DIR / "config.json"
    20→
    21→
    22→@dataclass
    23→class GitAgentConfig:
    24→    """Git agent configuration."""
    25→    auto_commit: bool = True
    26→    auto_push: bool = False
    27→    commit_style: str = "conventional"  # "conventional", "simple", "detailed"
    28→    push_on: list[str] = field(default_factory=lambda: ["feature_complete", "session_end"])
    29→    pull_before: list[str] = field(default_factory=lambda: ["code_review", "feature_start"])
    30→
    31→
    32→@dataclass
    33→class ParallelConfig:
    34→    """Parallel agent configuration."""
    35→    enabled: bool = True
    36→    max_agents: int = 3
    37→    lock_timeout_seconds: int = 3600
    38→
    39→
    40→@dataclass
    41→class SecurityConfig:
    42→    """Security configuration."""
    43→    sandbox_enabled: bool = False  # Disabled for full system access
    44→    allowed_hosts: list[str] = field(default_factory=list)  # Empty = all allowed
    45→
    46→
    47→@dataclass
    48→class PathsConfig:
    49→    """Path configuration."""
    50→    credentials_path: str = "~/claude-projects/shared-data/credentials.md"
    51→    global_resources_path: str = "~/.autocoder/global_resources.json"
    52→    session_logs_path: str = "~/claude-projects/session-logs"
    53→
    54→
    55→@dataclass
    56→class UIConfig:
    57→    """UI configuration."""
    58→    theme: str = "neobrutalism"
    59→    show_agent_thoughts: bool = True
    60→    sound_enabled: bool = True
    61→    celebration_enabled: bool = True
    62→
    63→
    64→@dataclass
    65→class AutocoderConfig:
    66→    """Main Autocoder configuration."""
    67→    version: str = "2.0.0"
    68→    default_model: str = "claude-sonnet-4-20250514"
    69→    git_agent: GitAgentConfig = field(default_factory=GitAgentConfig)
    70→    parallel: ParallelConfig = field(default_factory=ParallelConfig)
    71→    security: SecurityConfig = field(default_factory=SecurityConfig)
    72→    paths: PathsConfig = field(default_factory=PathsConfig)
    73→    ui: UIConfig = field(default_factory=UIConfig)
    74→
    75→    def to_dict(self) -> dict:
    76→        """Convert to dictionary for JSON serialization."""
    77→        return {
    78→            "version": self.version,
    79→            "default_model": self.default_model,
    80→            "git_agent": asdict(self.git_agent),
    81→            "parallel": asdict(self.parallel),
    82→            "security": asdict(self.security),
    83→            "paths": asdict(self.paths),
    84→            "ui": asdict(self.ui),
    85→        }
    86→
    87→    @classmethod
    88→    def from_dict(cls, data: dict) -> "AutocoderConfig":
    89→        """Create from dictionary."""
    90→        return cls(
    91→            version=data.get("version", "2.0.0"),
    92→            default_model=data.get("default_model", "claude-sonnet-4-20250514"),
    93→            git_agent=GitAgentConfig(**data.get("git_agent", {})),
    94→            parallel=ParallelConfig(**data.get("parallel", {})),
    95→            security=SecurityConfig(**data.get("security", {})),
    96→            paths=PathsConfig(**data.get("paths", {})),
    97→            ui=UIConfig(**data.get("ui", {})),
    98→        )
    99→
   100→
   101→# Global config instance
   102→_config: Optional[AutocoderConfig] = None
   103→
   104→
   105→def get_config() -> AutocoderConfig:
   106→    """
   107→    Get the global configuration instance.
   108→
   109→    Loads from ~/.autocoder/config.json if it exists,
   110→    otherwise returns defaults.
   111→
   112→    Returns:
   113→        AutocoderConfig instance
   114→    """
   115→    global _config
   116→
   117→    if _config is not None:
   118→        return _config
   119→
   120→    _config = load_config()
   121→    return _config
   122→
   123→
   124→def load_config(path: Optional[Path] = None) -> AutocoderConfig:
   125→    """
   126→    Load configuration from file.
   127→
   128→    Args:
   129→        path: Optional path to config file (defaults to ~/.autocoder/config.json)
   130→
   131→    Returns:
   132→        AutocoderConfig instance
   133→    """
   134→    config_path = path or CONFIG_FILE
   135→
   136→    if config_path.exists():
   137→        try:
   138→            with open(config_path, "r", encoding="utf-8") as f:
   139→                data = json.load(f)
   140→            logger.info(f"Loaded config from {config_path}")
   141→            return AutocoderConfig.from_dict(data)
   142→        except Exception as e:
   143→            logger.warning(f"Failed to load config from {config_path}: {e}")
   144→            logger.info("Using default configuration")
   145→
   146→    return AutocoderConfig()
   147→
   148→
   149→def save_config(config: Optional[AutocoderConfig] = None, path: Optional[Path] = None) -> None:
   150→    """
   151→    Save configuration to file.
   152→
   153→    Args:
   154→        config: Config to save (defaults to global config)
   155→        path: Path to save to (defaults to ~/.autocoder/config.json)
   156→    """
   157→    config = config or get_config()
   158→    config_path = path or CONFIG_FILE
   159→
   160→    # Ensure directory exists
   161→    config_path.parent.mkdir(parents=True, exist_ok=True)
   162→
   163→    with open(config_path, "w", encoding="utf-8") as f:
   164→        json.dump(config.to_dict(), f, indent=2)
   165→
   166→    logger.info(f"Saved config to {config_path}")
   167→
   168→
   169→def reset_config() -> AutocoderConfig:
   170→    """
   171→    Reset configuration to defaults.
   172→
   173→    Returns:
   174→        New default AutocoderConfig
   175→    """
   176→    global _config
   177→    _config = AutocoderConfig()
   178→    return _config
   179→
   180→
   181→def update_config(**kwargs) -> AutocoderConfig:
   182→    """
   183→    Update configuration with new values.
   184→
   185→    Args:
   186→        **kwargs: Configuration values to update
   187→
   188→    Returns:
   189→        Updated AutocoderConfig
   190→    """
   191→    config = get_config()
   192→
   193→    for key, value in kwargs.items():
   194→        if hasattr(config, key):
   195→            setattr(config, key, value)
   196→
   197→    return config
   198→
   199→
   200→def ensure_config_dir() -> Path:
   201→    """
   202→    Ensure the config directory exists.
   203→
   204→    Returns:
   205→        Path to config directory
   206→    """
   207→    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
   208→    return CONFIG_DIR
   209→
   210→
   211→def create_default_config() -> None:
   212→    """Create default configuration file if it doesn't exist."""
   213→    if not CONFIG_FILE.exists():
   214→        ensure_config_dir()
   215→        save_config(AutocoderConfig())
   216→        logger.info(f"Created default config at {CONFIG_FILE}")
   217→
   218→
   219→# Initialize config directory on import
   220→ensure_config_dir()
   221→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

"""
Autocoder Configuration
=======================

Centralized configuration management for Autocoder 2.0.
Loads settings from ~/.autocoder/config.json with sensible defaults.
"""

import json
import logging
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# Default config location
CONFIG_DIR = Path.home() / ".autocoder"
CONFIG_FILE = CONFIG_DIR / "config.json"

@dataclass
class GitAgentConfig:
    """Git agent configuration."""
    auto_commit: bool = True
    auto_push: bool = False
    commit_style: str = "conventional"  # "conventional", "simple", "detailed"
    push_on: list[str] = field(default_factory=lambda: ["feature_complete", "session_end"])
    pull_before: list[str] = field(default_factory=lambda: ["code_review", "feature_start"])

@dataclass
class ParallelConfig:
    """Parallel agent configuration."""
    enabled: bool = True
    max_agents: int = 3
    lock_timeout_seconds: int = 3600

@dataclass
class SecurityConfig:
    """Security configuration."""
    sandbox_enabled: bool = False  # Disabled for full system access
    allowed_hosts: list[str] = field(default_factory=list)  # Empty = all allowed

@dataclass
class PathsConfig:
    """Path configuration."""
    credentials_path: str = "~/claude-projects/shared-data/credentials.md"
    global_resources_path: str = "~/.autocoder/global_resources.json"
    session_logs_path: str = "~/claude-projects/session-logs"

@dataclass
class UIConfig:
    """UI configuration."""
    theme: str = "neobrutalism"
    show_agent_thoughts: bool = True
    sound_enabled: bool = True
    celebration_enabled: bool = True

@dataclass
class AutocoderConfig:
    """Main Autocoder configuration."""
    version: str = "2.0.0"
    default_model: str = "claude-sonnet-4-20250514"
    git_agent: GitAgentConfig = field(default_factory=GitAgentConfig)
    parallel: ParallelConfig = field(default_factory=ParallelConfig)
    security: SecurityConfig = field(default_factory=SecurityConfig)
    paths: PathsConfig = field(default_factory=PathsConfig)
    ui: UIConfig = field(default_factory=UIConfig)

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return {
            "version": self.version,
            "default_model": self.default_model,
            "git_agent": asdict(self.git_agent),
            "parallel": asdict(self.parallel),
            "security": asdict(self.security),
            "paths": asdict(self.paths),
            "ui": asdict(self.ui),
        }

    @classmethod
    def from_dict(cls, data: dict) -> "AutocoderConfig":
        """Create from dictionary."""
        return cls(
            version=data.get("version", "2.0.0"),
            default_model=data.get("default_model", "claude-sonnet-4-20250514"),
            git_agent=GitAgentConfig(**data.get("git_agent", {})),
            parallel=ParallelConfig(**data.get("parallel", {})),
            security=SecurityConfig(**data.get("security", {})),
            paths=PathsConfig(**data.get("paths", {})),
            ui=UIConfig(**data.get("ui", {})),
        )

# Global config instance
_config: Optional[AutocoderConfig] = None

def get_config() -> AutocoderConfig:
    """
    Get the global configuration instance.

    Loads from ~/.autocoder/config.json if it exists,
    otherwise returns defaults.

    Returns:
        AutocoderConfig instance
    """
    global _config

    if _config is not None:
        return _config

    _config = load_config()
    return _config

def load_config(path: Optional[Path] = None) -> AutocoderConfig:
    """
    Load configuration from file.

    Args:
        path: Optional path to config file (defaults to ~/.autocoder/config.json)

    Returns:
        AutocoderConfig instance
    """
    config_path = path or CONFIG_FILE

    if config_path.exists():
        try:
            with open(config_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            logger.info(f"Loaded config from {config_path}")
            return AutocoderConfig.from_dict(data)
        except Exception as e:
            logger.warning(f"Failed to load config from {config_path}: {e}")
            logger.info("Using default configuration")

    return AutocoderConfig()

def save_config(config: Optional[AutocoderConfig] = None, path: Optional[Path] = None) -> None:
    """
    Save configuration to file.

    Args:
        config: Config to save (defaults to global config)
        path: Path to save to (defaults to ~/.autocoder/config.json)
    """
    config = config or get_config()
    config_path = path or CONFIG_FILE

    # Ensure directory exists
    config_path.parent.mkdir(parents=True, exist_ok=True)

    with open(config_path, "w", encoding="utf-8") as f:
        json.dump(config.to_dict(), f, indent=2)

    logger.info(f"Saved config to {config_path}")

def reset_config() -> AutocoderConfig:
    """
    Reset configuration to defaults.

    Returns:
        New default AutocoderConfig
    """
    global _config
    _config = AutocoderConfig()
    return _config

def update_config(**kwargs) -> AutocoderConfig:
    """
    Update configuration with new values.

    Args:
        **kwargs: Configuration values to update

    Returns:
        Updated AutocoderConfig
    """
    config = get_config()

    for key, value in kwargs.items():
        if hasattr(config, key):
            setattr(config, key, value)

    return config

def ensure_config_dir() -> Path:
    """
    Ensure the config directory exists.

    Returns:
        Path to config directory
    """
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    return CONFIG_DIR

def create_default_config() -> None:
    """Create default configuration file if it doesn't exist."""
    if not CONFIG_FILE.exists():
        ensure_config_dir()
        save_config(AutocoderConfig())
        logger.info(f"Created default config at {CONFIG_FILE}")

# Initialize config directory on import
ensure_config_dir()


Now let me explore the key Python modules and server API:


     1→"""
     2→Database Models and Connection
     3→==============================
     4→
     5→SQLite database schema for feature storage using SQLAlchemy.
     6→"""
     7→
     8→from pathlib import Path
     9→from typing import Optional
    10→
    11→from sqlalchemy import Boolean, Column, Integer, String, Text, create_engine
    12→from sqlalchemy.ext.declarative import declarative_base
    13→from sqlalchemy.orm import Session, sessionmaker
    14→from sqlalchemy.types import JSON
    15→
    16→Base = declarative_base()
    17→
    18→
    19→class Feature(Base):
    20→    """Feature model representing a test case/feature to implement."""
    21→
    22→    __tablename__ = "features"
    23→
    24→    id = Column(Integer, primary_key=True, index=True)
    25→    priority = Column(Integer, nullable=False, default=999, index=True)
    26→    category = Column(String(100), nullable=False)
    27→    name = Column(String(255), nullable=False)
    28→    description = Column(Text, nullable=False)
    29→    steps = Column(JSON, nullable=False)  # Stored as JSON array
    30→    passes = Column(Boolean, default=False, index=True)
    31→    in_progress = Column(Boolean, default=False, index=True)
    32→
    33→    def to_dict(self) -> dict:
    34→        """Convert feature to dictionary for JSON serialization."""
    35→        return {
    36→            "id": self.id,
    37→            "priority": self.priority,
    38→            "category": self.category,
    39→            "name": self.name,
    40→            "description": self.description,
    41→            "steps": self.steps,
    42→            "passes": self.passes,
    43→            "in_progress": self.in_progress,
    44→        }
    45→
    46→
    47→def get_database_path(project_dir: Path) -> Path:
    48→    """Return the path to the SQLite database for a project."""
    49→    return project_dir / "features.db"
    50→
    51→
    52→def get_database_url(project_dir: Path) -> str:
    53→    """Return the SQLAlchemy database URL for a project.
    54→
    55→    Uses POSIX-style paths (forward slashes) for cross-platform compatibility.
    56→    """
    57→    db_path = get_database_path(project_dir)
    58→    return f"sqlite:///{db_path.as_posix()}"
    59→
    60→
    61→def _migrate_add_in_progress_column(engine) -> None:
    62→    """Add in_progress column to existing databases that don't have it."""
    63→    from sqlalchemy import text
    64→
    65→    with engine.connect() as conn:
    66→        # Check if column exists
    67→        result = conn.execute(text("PRAGMA table_info(features)"))
    68→        columns = [row[1] for row in result.fetchall()]
    69→
    70→        if "in_progress" not in columns:
    71→            # Add the column with default value
    72→            conn.execute(text("ALTER TABLE features ADD COLUMN in_progress BOOLEAN DEFAULT 0"))
    73→            conn.commit()
    74→
    75→
    76→def create_database(project_dir: Path) -> tuple:
    77→    """
    78→    Create database and return engine + session maker.
    79→
    80→    Args:
    81→        project_dir: Directory containing the project
    82→
    83→    Returns:
    84→        Tuple of (engine, SessionLocal)
    85→    """
    86→    db_url = get_database_url(project_dir)
    87→    engine = create_engine(db_url, connect_args={"check_same_thread": False})
    88→    Base.metadata.create_all(bind=engine)
    89→
    90→    # Migrate existing databases to add in_progress column
    91→    _migrate_add_in_progress_column(engine)
    92→
    93→    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    94→    return engine, SessionLocal
    95→
    96→
    97→# Global session maker - will be set when server starts
    98→_session_maker: Optional[sessionmaker] = None
    99→
   100→
   101→def set_session_maker(session_maker: sessionmaker) -> None:
   102→    """Set the global session maker."""
   103→    global _session_maker
   104→    _session_maker = session_maker
   105→
   106→
   107→def get_db() -> Session:
   108→    """
   109→    Dependency for FastAPI to get database session.
   110→
   111→    Yields a database session and ensures it's closed after use.
   112→    """
   113→    if _session_maker is None:
   114→        raise RuntimeError("Database not initialized. Call set_session_maker first.")
   115→
   116→    db = _session_maker()
   117→    try:
   118→        yield db
   119→    finally:
   120→        db.close()
   121→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

"""
Database Models and Connection
==============================

SQLite database schema for feature storage using SQLAlchemy.
"""

from pathlib import Path
from typing import Optional

from sqlalchemy import Boolean, Column, Integer, String, Text, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.types import JSON

Base = declarative_base()

class Feature(Base):
    """Feature model representing a test case/feature to implement."""

    __tablename__ = "features"

    id = Column(Integer, primary_key=True, index=True)
    priority = Column(Integer, nullable=False, default=999, index=True)
    category = Column(String(100), nullable=False)
    name = Column(String(255), nullable=False)
    description = Column(Text, nullable=False)
    steps = Column(JSON, nullable=False)  # Stored as JSON array
    passes = Column(Boolean, default=False, index=True)
    in_progress = Column(Boolean, default=False, index=True)

    def to_dict(self) -> dict:
        """Convert feature to dictionary for JSON serialization."""
        return {
            "id": self.id,
            "priority": self.priority,
            "category": self.category,
            "name": self.name,
            "description": self.description,
            "steps": self.steps,
            "passes": self.passes,
            "in_progress": self.in_progress,
        }

def get_database_path(project_dir: Path) -> Path:
    """Return the path to the SQLite database for a project."""
    return project_dir / "features.db"

def get_database_url(project_dir: Path) -> str:
    """Return the SQLAlchemy database URL for a project.

    Uses POSIX-style paths (forward slashes) for cross-platform compatibility.
    """
    db_path = get_database_path(project_dir)
    return f"sqlite:///{db_path.as_posix()}"

def _migrate_add_in_progress_column(engine) -> None:
    """Add in_progress column to existing databases that don't have it."""
    from sqlalchemy import text

    with engine.connect() as conn:
        # Check if column exists
        result = conn.execute(text("PRAGMA table_info(features)"))
        columns = [row[1] for row in result.fetchall()]

        if "in_progress" not in columns:
            # Add the column with default value
            conn.execute(text("ALTER TABLE features ADD COLUMN in_progress BOOLEAN DEFAULT 0"))
            conn.commit()

def create_database(project_dir: Path) -> tuple:
    """
    Create database and return engine + session maker.

    Args:
        project_dir: Directory containing the project

    Returns:
        Tuple of (engine, SessionLocal)
    """
    db_url = get_database_url(project_dir)
    engine = create_engine(db_url, connect_args={"check_same_thread": False})
    Base.metadata.create_all(bind=engine)

    # Migrate existing databases to add in_progress column
    _migrate_add_in_progress_column(engine)

    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    return engine, SessionLocal

# Global session maker - will be set when server starts
_session_maker: Optional[sessionmaker] = None

def set_session_maker(session_maker: sessionmaker) -> None:
    """Set the global session maker."""
    global _session_maker
    _session_maker = session_maker

def get_db() -> Session:
    """
    Dependency for FastAPI to get database session.

    Yields a database session and ensures it's closed after use.
    """
    if _session_maker is None:
        raise RuntimeError("Database not initialized. Call set_session_maker first.")

    db = _session_maker()
    try:
        yield db
    finally:
        db.close()


     1→#!/usr/bin/env python3
     2→"""
     3→MCP Server for Feature Management
     4→==================================
     5→
     6→Provides tools to manage features in the autonomous coding system,
     7→replacing the previous FastAPI-based REST API.
     8→
     9→Tools:
    10→- feature_get_stats: Get progress statistics
    11→- feature_get_next: Get next feature to implement
    12→- feature_get_for_regression: Get random passing features for testing
    13→- feature_mark_passing: Mark a feature as passing
    14→- feature_skip: Skip a feature (move to end of queue)
    15→- feature_mark_in_progress: Mark a feature as in-progress
    16→- feature_clear_in_progress: Clear in-progress status
    17→- feature_create_bulk: Create multiple features at once
    18→"""
    19→
    20→import json
    21→import os
    22→import sys
    23→from contextlib import asynccontextmanager
    24→from pathlib import Path
    25→from typing import Annotated
    26→
    27→from mcp.server.fastmcp import FastMCP
    28→from pydantic import BaseModel, Field
    29→from sqlalchemy.sql.expression import func
    30→
    31→# Add parent directory to path so we can import from api module
    32→sys.path.insert(0, str(Path(__file__).parent.parent))
    33→
    34→from api.database import Feature, create_database
    35→from api.migration import migrate_json_to_sqlite
    36→
    37→# Configuration from environment
    38→PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()
    39→
    40→
    41→# Pydantic models for input validation
    42→class MarkPassingInput(BaseModel):
    43→    """Input for marking a feature as passing."""
    44→    feature_id: int = Field(..., description="The ID of the feature to mark as passing", ge=1)
    45→
    46→
    47→class SkipFeatureInput(BaseModel):
    48→    """Input for skipping a feature."""
    49→    feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)
    50→
    51→
    52→class MarkInProgressInput(BaseModel):
    53→    """Input for marking a feature as in-progress."""
    54→    feature_id: int = Field(..., description="The ID of the feature to mark as in-progress", ge=1)
    55→
    56→
    57→class ClearInProgressInput(BaseModel):
    58→    """Input for clearing in-progress status."""
    59→    feature_id: int = Field(..., description="The ID of the feature to clear in-progress status", ge=1)
    60→
    61→
    62→class RegressionInput(BaseModel):
    63→    """Input for getting regression features."""
    64→    limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")
    65→
    66→
    67→class FeatureCreateItem(BaseModel):
    68→    """Schema for creating a single feature."""
    69→    category: str = Field(..., min_length=1, max_length=100, description="Feature category")
    70→    name: str = Field(..., min_length=1, max_length=255, description="Feature name")
    71→    description: str = Field(..., min_length=1, description="Detailed description")
    72→    steps: list[str] = Field(..., min_length=1, description="Implementation/test steps")
    73→
    74→
    75→class BulkCreateInput(BaseModel):
    76→    """Input for bulk creating features."""
    77→    features: list[FeatureCreateItem] = Field(..., min_length=1, description="List of features to create")
    78→
    79→
    80→# Global database session maker (initialized on startup)
    81→_session_maker = None
    82→_engine = None
    83→
    84→
    85→@asynccontextmanager
    86→async def server_lifespan(server: FastMCP):
    87→    """Initialize database on startup, cleanup on shutdown."""
    88→    global _session_maker, _engine
    89→
    90→    # Create project directory if it doesn't exist
    91→    PROJECT_DIR.mkdir(parents=True, exist_ok=True)
    92→
    93→    # Initialize database
    94→    _engine, _session_maker = create_database(PROJECT_DIR)
    95→
    96→    # Run migration if needed (converts legacy JSON to SQLite)
    97→    migrate_json_to_sqlite(PROJECT_DIR, _session_maker)
    98→
    99→    yield
   100→
   101→    # Cleanup
   102→    if _engine:
   103→        _engine.dispose()
   104→
   105→
   106→# Initialize the MCP server
   107→mcp = FastMCP("features", lifespan=server_lifespan)
   108→
   109→
   110→def get_session():
   111→    """Get a new database session."""
   112→    if _session_maker is None:
   113→        raise RuntimeError("Database not initialized")
   114→    return _session_maker()
   115→
   116→
   117→@mcp.tool()
   118→def feature_get_stats() -> str:
   119→    """Get statistics about feature completion progress.
   120→
   121→    Returns the number of passing features, in-progress features, total features,
   122→    and completion percentage. Use this to track overall progress of the implementation.
   123→
   124→    Returns:
   125→        JSON with: passing (int), in_progress (int), total (int), percentage (float)
   126→    """
   127→    session = get_session()
   128→    try:
   129→        total = session.query(Feature).count()
   130→        passing = session.query(Feature).filter(Feature.passes == True).count()
   131→        in_progress = session.query(Feature).filter(Feature.in_progress == True).count()
   132→        percentage = round((passing / total) * 100, 1) if total > 0 else 0.0
   133→
   134→        return json.dumps({
   135→            "passing": passing,
   136→            "in_progress": in_progress,
   137→            "total": total,
   138→            "percentage": percentage
   139→        }, indent=2)
   140→    finally:
   141→        session.close()
   142→
   143→
   144→@mcp.tool()
   145→def feature_get_next() -> str:
   146→    """Get the highest-priority pending feature to work on.
   147→
   148→    Returns the feature with the lowest priority number that has passes=false.
   149→    Use this at the start of each coding session to determine what to implement next.
   150→
   151→    Returns:
   152→        JSON with feature details (id, priority, category, name, description, steps, passes, in_progress)
   153→        or error message if all features are passing.
   154→    """
   155→    session = get_session()
   156→    try:
   157→        feature = (
   158→            session.query(Feature)
   159→            .filter(Feature.passes == False)
   160→            .order_by(Feature.priority.asc(), Feature.id.asc())
   161→            .first()
   162→        )
   163→
   164→        if feature is None:
   165→            return json.dumps({"error": "All features are passing! No more work to do."})
   166→
   167→        return json.dumps(feature.to_dict(), indent=2)
   168→    finally:
   169→        session.close()
   170→
   171→
   172→@mcp.tool()
   173→def feature_get_for_regression(
   174→    limit: Annotated[int, Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")] = 3
   175→) -> str:
   176→    """Get random passing features for regression testing.
   177→
   178→    Returns a random selection of features that are currently passing.
   179→    Use this to verify that previously implemented features still work
   180→    after making changes.
   181→
   182→    Args:
   183→        limit: Maximum number of features to return (1-10, default 3)
   184→
   185→    Returns:
   186→        JSON with: features (list of feature objects), count (int)
   187→    """
   188→    session = get_session()
   189→    try:
   190→        features = (
   191→            session.query(Feature)
   192→            .filter(Feature.passes == True)
   193→            .order_by(func.random())
   194→            .limit(limit)
   195→            .all()
   196→        )
   197→
   198→        return json.dumps({
   199→            "features": [f.to_dict() for f in features],
   200→            "count": len(features)
   201→        }, indent=2)
   202→    finally:
   203→        session.close()
   204→
   205→
   206→@mcp.tool()
   207→def feature_mark_passing(
   208→    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
   209→) -> str:
   210→    """Mark a feature as passing after successful implementation.
   211→
   212→    Updates the feature's passes field to true and clears the in_progress flag.
   213→    Use this after you have implemented the feature and verified it works correctly.
   214→
   215→    Args:
   216→        feature_id: The ID of the feature to mark as passing
   217→
   218→    Returns:
   219→        JSON with the updated feature details, or error if not found.
   220→    """
   221→    session = get_session()
   222→    try:
   223→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   224→
   225→        if feature is None:
   226→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   227→
   228→        feature.passes = True
   229→        feature.in_progress = False
   230→        session.commit()
   231→        session.refresh(feature)
   232→
   233→        return json.dumps(feature.to_dict(), indent=2)
   234→    finally:
   235→        session.close()
   236→
   237→
   238→@mcp.tool()
   239→def feature_skip(
   240→    feature_id: Annotated[int, Field(description="The ID of the feature to skip", ge=1)]
   241→) -> str:
   242→    """Skip a feature by moving it to the end of the priority queue.
   243→
   244→    Use this when a feature cannot be implemented yet due to:
   245→    - Dependencies on other features that aren't implemented yet
   246→    - External blockers (missing assets, unclear requirements)
   247→    - Technical prerequisites that need to be addressed first
   248→
   249→    The feature's priority is set to max_priority + 1, so it will be
   250→    worked on after all other pending features. Also clears the in_progress
   251→    flag so the feature returns to "pending" status.
   252→
   253→    Args:
   254→        feature_id: The ID of the feature to skip
   255→
   256→    Returns:
   257→        JSON with skip details: id, name, old_priority, new_priority, message
   258→    """
   259→    session = get_session()
   260→    try:
   261→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   262→
   263→        if feature is None:
   264→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   265→
   266→        if feature.passes:
   267→            return json.dumps({"error": "Cannot skip a feature that is already passing"})
   268→
   269→        old_priority = feature.priority
   270→
   271→        # Get max priority and set this feature to max + 1
   272→        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
   273→        new_priority = (max_priority_result[0] + 1) if max_priority_result else 1
   274→
   275→        feature.priority = new_priority
   276→        feature.in_progress = False
   277→        session.commit()
   278→        session.refresh(feature)
   279→
   280→        return json.dumps({
   281→            "id": feature.id,
   282→            "name": feature.name,
   283→            "old_priority": old_priority,
   284→            "new_priority": new_priority,
   285→            "message": f"Feature '{feature.name}' moved to end of queue"
   286→        }, indent=2)
   287→    finally:
   288→        session.close()
   289→
   290→
   291→@mcp.tool()
   292→def feature_mark_in_progress(
   293→    feature_id: Annotated[int, Field(description="The ID of the feature to mark as in-progress", ge=1)]
   294→) -> str:
   295→    """Mark a feature as in-progress. Call immediately after feature_get_next().
   296→
   297→    This prevents other agent sessions from working on the same feature.
   298→    Use this as soon as you retrieve a feature to work on.
   299→
   300→    Args:
   301→        feature_id: The ID of the feature to mark as in-progress
   302→
   303→    Returns:
   304→        JSON with the updated feature details, or error if not found or already in-progress.
   305→    """
   306→    session = get_session()
   307→    try:
   308→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   309→
   310→        if feature is None:
   311→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   312→
   313→        if feature.passes:
   314→            return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})
   315→
   316→        if feature.in_progress:
   317→            return json.dumps({"error": f"Feature with ID {feature_id} is already in-progress"})
   318→
   319→        feature.in_progress = True
   320→        session.commit()
   321→        session.refresh(feature)
   322→
   323→        return json.dumps(feature.to_dict(), indent=2)
   324→    finally:
   325→        session.close()
   326→
   327→
   328→@mcp.tool()
   329→def feature_clear_in_progress(
   330→    feature_id: Annotated[int, Field(description="The ID of the feature to clear in-progress status", ge=1)]
   331→) -> str:
   332→    """Clear in-progress status from a feature.
   333→
   334→    Use this when abandoning a feature or manually unsticking a stuck feature.
   335→    The feature will return to the pending queue.
   336→
   337→    Args:
   338→        feature_id: The ID of the feature to clear in-progress status
   339→
   340→    Returns:
   341→        JSON with the updated feature details, or error if not found.
   342→    """
   343→    session = get_session()
   344→    try:
   345→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   346→
   347→        if feature is None:
   348→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   349→
   350→        feature.in_progress = False
   351→        session.commit()
   352→        session.refresh(feature)
   353→
   354→        return json.dumps(feature.to_dict(), indent=2)
   355→    finally:
   356→        session.close()
   357→
   358→
   359→@mcp.tool()
   360→def feature_create_bulk(
   361→    features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
   362→) -> str:
   363→    """Create multiple features in a single operation.
   364→
   365→    Features are assigned sequential priorities based on their order.
   366→    All features start with passes=false.
   367→
   368→    This is typically used by the initializer agent to set up the initial
   369→    feature list from the app specification.
   370→
   371→    Args:
   372→        features: List of features to create, each with:
   373→            - category (str): Feature category
   374→            - name (str): Feature name
   375→            - description (str): Detailed description
   376→            - steps (list[str]): Implementation/test steps
   377→
   378→    Returns:
   379→        JSON with: created (int) - number of features created
   380→    """
   381→    session = get_session()
   382→    try:
   383→        # Get the starting priority
   384→        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
   385→        start_priority = (max_priority_result[0] + 1) if max_priority_result else 1
   386→
   387→        created_count = 0
   388→        for i, feature_data in enumerate(features):
   389→            # Validate required fields
   390→            if not all(key in feature_data for key in ["category", "name", "description", "steps"]):
   391→                return json.dumps({
   392→                    "error": f"Feature at index {i} missing required fields (category, name, description, steps)"
   393→                })
   394→
   395→            db_feature = Feature(
   396→                priority=start_priority + i,
   397→                category=feature_data["category"],
   398→                name=feature_data["name"],
   399→                description=feature_data["description"],
   400→                steps=feature_data["steps"],
   401→                passes=False,
   402→            )
   403→            session.add(db_feature)
   404→            created_count += 1
   405→
   406→        session.commit()
   407→
   408→        return json.dumps({"created": created_count}, indent=2)
   409→    except Exception as e:
   410→        session.rollback()
   411→        return json.dumps({"error": str(e)})
   412→    finally:
   413→        session.close()
   414→
   415→
   416→if __name__ == "__main__":
   417→    mcp.run()
   418→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

#!/usr/bin/env python3
"""
MCP Server for Feature Management
==================================

Provides tools to manage features in the autonomous coding system,
replacing the previous FastAPI-based REST API.

Tools:
- feature_get_stats: Get progress statistics
- feature_get_next: Get next feature to implement
- feature_get_for_regression: Get random passing features for testing
- feature_mark_passing: Mark a feature as passing
- feature_skip: Skip a feature (move to end of queue)
- feature_mark_in_progress: Mark a feature as in-progress
- feature_clear_in_progress: Clear in-progress status
- feature_create_bulk: Create multiple features at once
"""

import json
import os
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Annotated

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from sqlalchemy.sql.expression import func

# Add parent directory to path so we can import from api module
sys.path.insert(0, str(Path(__file__).parent.parent))

from api.database import Feature, create_database
from api.migration import migrate_json_to_sqlite

# Configuration from environment
PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()

# Pydantic models for input validation
class MarkPassingInput(BaseModel):
    """Input for marking a feature as passing."""
    feature_id: int = Field(..., description="The ID of the feature to mark as passing", ge=1)

class SkipFeatureInput(BaseModel):
    """Input for skipping a feature."""
    feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)

class MarkInProgressInput(BaseModel):
    """Input for marking a feature as in-progress."""
    feature_id: int = Field(..., description="The ID of the feature to mark as in-progress", ge=1)

class ClearInProgressInput(BaseModel):
    """Input for clearing in-progress status."""
    feature_id: int = Field(..., description="The ID of the feature to clear in-progress status", ge=1)

class RegressionInput(BaseModel):
    """Input for getting regression features."""
    limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")

class FeatureCreateItem(BaseModel):
    """Schema for creating a single feature."""
    category: str = Field(..., min_length=1, max_length=100, description="Feature category")
    name: str = Field(..., min_length=1, max_length=255, description="Feature name")
    description: str = Field(..., min_length=1, description="Detailed description")
    steps: list[str] = Field(..., min_length=1, description="Implementation/test steps")

class BulkCreateInput(BaseModel):
    """Input for bulk creating features."""
    features: list[FeatureCreateItem] = Field(..., min_length=1, description="List of features to create")

# Global database session maker (initialized on startup)
_session_maker = None
_engine = None

@asynccontextmanager
async def server_lifespan(server: FastMCP):
    """Initialize database on startup, cleanup on shutdown."""
    global _session_maker, _engine

    # Create project directory if it doesn't exist
    PROJECT_DIR.mkdir(parents=True, exist_ok=True)

    # Initialize database
    _engine, _session_maker = create_database(PROJECT_DIR)

    # Run migration if needed (converts legacy JSON to SQLite)
    migrate_json_to_sqlite(PROJECT_DIR, _session_maker)

    yield

    # Cleanup
    if _engine:
        _engine.dispose()

# Initialize the MCP server
mcp = FastMCP("features", lifespan=server_lifespan)

def get_session():
    """Get a new database session."""
    if _session_maker is None:
        raise RuntimeError("Database not initialized")
    return _session_maker()

@mcp.tool()
def feature_get_stats() -> str:
    """Get statistics about feature completion progress.

    Returns the number of passing features, in-progress features, total features,
    and completion percentage. Use this to track overall progress of the implementation.

    Returns:
        JSON with: passing (int), in_progress (int), total (int), percentage (float)
    """
    session = get_session()
    try:
        total = session.query(Feature).count()
        passing = session.query(Feature).filter(Feature.passes == True).count()
        in_progress = session.query(Feature).filter(Feature.in_progress == True).count()
        percentage = round((passing / total) * 100, 1) if total > 0 else 0.0

        return json.dumps({
            "passing": passing,
            "in_progress": in_progress,
            "total": total,
            "percentage": percentage
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_next() -> str:
    """Get the highest-priority pending feature to work on.

    Returns the feature with the lowest priority number that has passes=false.
    Use this at the start of each coding session to determine what to implement next.

    Returns:
        JSON with feature details (id, priority, category, name, description, steps, passes, in_progress)
        or error message if all features are passing.
    """
    session = get_session()
    try:
        feature = (
            session.query(Feature)
            .filter(Feature.passes == False)
            .order_by(Feature.priority.asc(), Feature.id.asc())
            .first()
        )

        if feature is None:
            return json.dumps({"error": "All features are passing! No more work to do."})

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_for_regression(
    limit: Annotated[int, Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")] = 3
) -> str:
    """Get random passing features for regression testing.

    Returns a random selection of features that are currently passing.
    Use this to verify that previously implemented features still work
    after making changes.

    Args:
        limit: Maximum number of features to return (1-10, default 3)

    Returns:
        JSON with: features (list of feature objects), count (int)
    """
    session = get_session()
    try:
        features = (
            session.query(Feature)
            .filter(Feature.passes == True)
            .order_by(func.random())
            .limit(limit)
            .all()
        )

        return json.dumps({
            "features": [f.to_dict() for f in features],
            "count": len(features)
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_passing(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
) -> str:
    """Mark a feature as passing after successful implementation.

    Updates the feature's passes field to true and clears the in_progress flag.
    Use this after you have implemented the feature and verified it works correctly.

    Args:
        feature_id: The ID of the feature to mark as passing

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.passes = True
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_skip(
    feature_id: Annotated[int, Field(description="The ID of the feature to skip", ge=1)]
) -> str:
    """Skip a feature by moving it to the end of the priority queue.

    Use this when a feature cannot be implemented yet due to:
    - Dependencies on other features that aren't implemented yet
    - External blockers (missing assets, unclear requirements)
    - Technical prerequisites that need to be addressed first

    The feature's priority is set to max_priority + 1, so it will be
    worked on after all other pending features. Also clears the in_progress
    flag so the feature returns to "pending" status.

    Args:
        feature_id: The ID of the feature to skip

    Returns:
        JSON with skip details: id, name, old_priority, new_priority, message
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": "Cannot skip a feature that is already passing"})

        old_priority = feature.priority

        # Get max priority and set this feature to max + 1
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        new_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        feature.priority = new_priority
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps({
            "id": feature.id,
            "name": feature.name,
            "old_priority": old_priority,
            "new_priority": new_priority,
            "message": f"Feature '{feature.name}' moved to end of queue"
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as in-progress", ge=1)]
) -> str:
    """Mark a feature as in-progress. Call immediately after feature_get_next().

    This prevents other agent sessions from working on the same feature.
    Use this as soon as you retrieve a feature to work on.

    Args:
        feature_id: The ID of the feature to mark as in-progress

    Returns:
        JSON with the updated feature details, or error if not found or already in-progress.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})

        if feature.in_progress:
            return json.dumps({"error": f"Feature with ID {feature_id} is already in-progress"})

        feature.in_progress = True
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_clear_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to clear in-progress status", ge=1)]
) -> str:
    """Clear in-progress status from a feature.

    Use this when abandoning a feature or manually unsticking a stuck feature.
    The feature will return to the pending queue.

    Args:
        feature_id: The ID of the feature to clear in-progress status

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_create_bulk(
    features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
) -> str:
    """Create multiple features in a single operation.

    Features are assigned sequential priorities based on their order.
    All features start with passes=false.

    This is typically used by the initializer agent to set up the initial
    feature list from the app specification.

    Args:
        features: List of features to create, each with:
            - category (str): Feature category
            - name (str): Feature name
            - description (str): Detailed description
            - steps (list[str]): Implementation/test steps

    Returns:
        JSON with: created (int) - number of features created
    """
    session = get_session()
    try:
        # Get the starting priority
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        start_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        created_count = 0
        for i, feature_data in enumerate(features):
            # Validate required fields
            if not all(key in feature_data for key in ["category", "name", "description", "steps"]):
                return json.dumps({
                    "error": f"Feature at index {i} missing required fields (category, name, description, steps)"
                })

            db_feature = Feature(
                priority=start_priority + i,
                category=feature_data["category"],
                name=feature_data["name"],
                description=feature_data["description"],
                steps=feature_data["steps"],
                passes=False,
            )
            session.add(db_feature)
            created_count += 1

        session.commit()

        return json.dumps({"created": created_count}, indent=2)
    except Exception as e:
        session.rollback()
        return json.dumps({"error": str(e)})
    finally:
        session.close()

if __name__ == "__main__":
    mcp.run()