11 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.5 3b58fa4f8b refactor: reorganize into monorepo with separate subprojects
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 2m27s
Structure webber into three independent subprojects:
- webber-api/: FastAPI backend server with all agent code
- webber-cli/: Standalone CLI client (renamed from cli/ to webber_cli/)
- webber-sandbox/: Test project for functional testing

Key changes:
- Each subproject has its own .venv (Python 3.12+)
- Added sandbox.sh for managing test project templates
- Created sandbox-templates/ with calculator-cli and empty starter
- Updated CI/CD for prefixed tags (api/v*, cli/v*)
- Added comprehensive AGENTS.md with operational instructions
- Added gitignore filtering to glob and grep tools
- Created pyproject.toml for each subproject

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 10:37:47 +01:00
jpmschweitzerandClaude Opus 4.5 f4e8552298 feat: add Explore agent with PydanticAI tool calling
Implements the first Claude-like agent for codebase exploration:

Core Features:
- Explore agent with glob, grep, read, and bash tools
- Native PydanticAI tool calling with Ollama/Mistral Nemo
- Sanitized Ollama provider (fixes content:null issue)
- REST API endpoints for agent execution

Tool Infrastructure:
- BaseTool abstract class with ToolResult dataclass
- ReadFileTool, GlobFilesTool, GrepContentTool, BashReadOnlyTool
- Path validation and sandboxing support

CLI Client (separate package for future extraction):
- webber-cli command with chat, explore, status commands
- Communicates with Webber API backend
- Rich console output with theming

Configuration:
- Dev server on port 8095 (production uses 8086)
- Mistral Nemo optimizations (temp 0.3, tool_choice required)

Tests: 24 tests covering tools and API endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 01:26:30 +01:00
jpmschweitzerandClaude Opus 4.5 667e2ca8e4 chore: add ruff linter, fix mypy errors, and write README
- Add ruff linter configuration to pyproject.toml with modern Python 3.12 rules
- Add ruff~=0.9.4 to dev dependencies
- Fix all mypy type errors (Optional[] hints, Token types, Any returns)
- Auto-fix 54 ruff issues (import sorting, Optional -> X | None syntax)
- Create ProjectMeta dataclass for single source of truth from pyproject.toml
- Write comprehensive README.md with setup, config, and development docs
- Update main.py to use settings.app_description from pyproject.toml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:58:24 +01:00
jpmschweitzerandClaude Opus 4.5 34b68621c6 docs: add architecture.md and remove implementation plan
Replace temporary implementation plan with permanent architecture
documentation covering project structure, core patterns, and
deployment details.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:05:16 +01:00
jpmschweitzerandClaude Opus 4.5 33b7e72b18 feat: add Docker healthcheck for container monitoring
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 1m14s
Helps Watchtower and Docker detect unhealthy containers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 21:28:39 +01:00
jpmschweitzerandClaude Opus 4.5 d046831903 fix: handle empty environment variables for list fields
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 1m12s
- Use Optional[list[str]] with None default for allowed_paths
- Add env_parse_none_str="" to treat empty strings as None
- Add effective_allowed_paths property for safe access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:38:30 +01:00
jpmschweitzerandClaude Opus 4.5 cbf75986fb chore: release v0.2.1 - test CI/CD pipeline
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m23s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:22:07 +01:00
jpmschweitzerandClaude Opus 4.5 098367ab85 chore: release v0.2.0
Build and Push / release (push) Failing after 3s
Build and Push / build (push) Has been skipped
Added reference prompts and expanded documentation for agents and tools.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:07:04 +01:00
jpmschweitzerandClaude Opus 4.5 47eff4ecd1 docs: expand agent and tool documentation with detailed descriptions
agents/README.md:
- Added detailed descriptions for Explore, Plan, and Task agents
- Documented capabilities, when to use, and tools available
- Added utilities/ section for shared prompts

tools/README.md:
- Added detailed descriptions for file, shell, and search tools
- Documented key parameters and behaviors
- Added security notes for bash/sandbox

agents/utilities/:
- todowrite-prompt.md - Task list management
- askuserquestion-prompt.md - User clarification
- conversation-summarization-prompt.md - Context compaction
- session-title-prompt.md - Title/branch generation
- security-review-prompt.md - Security analysis

Source: gitea:library/claude-code-system-prompts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:05:23 +01:00
jpmschweitzerandClaude Opus 4.5 0b2bf08437 docs: add example prompts from claude-code-system-prompts
Added reference prompts for agent and tool implementations:

agents/explore/ - Codebase exploration agent prompt
agents/plan/    - Plan mode with system reminders
agents/task/    - Task execution agent prompt
agents/         - Main system prompt reference

tools/file/     - Read, Edit, Write, Glob tool descriptions
tools/shell/    - Bash with git commit/PR instructions
tools/search/   - Grep, WebSearch, WebFetch descriptions

Source: gitea:library/claude-code-system-prompts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 19:44:41 +01:00
jpmschweitzerandClaude Opus 4.5 ccfa16089a chore: add CI/CD pipeline for Gitea deployment
- Add .gitea/workflows/build.yml for tag-triggered builds
- Add Dockerfile for containerized deployment (port 8086)
- Add .dockerignore to keep image lean
- Add CHANGELOG.md following Keep a Changelog format
- Update AGENTS.md with deployment verification URL

Build pipeline:
1. Create Gitea release on v* tag
2. Build and push Docker image to git.schweitz.internal
3. Trigger Watchtower for automatic deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 19:10:52 +01:00
131 changed files with 7677 additions and 922 deletions
+43
View File
@@ -0,0 +1,43 @@
# Git
.git
.gitignore
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
.venv
venv
ENV
env
# Testing
.pytest_cache
.coverage
htmlcov
tests/
# IDE
.idea
.vscode
*.swp
*.swo
# Logs
logs/
*.log
# Environment
.env
.env.local
.env.*.local
# Development files
*.md
!README.md
requirements-dev.txt
implementation-plan.md
fastapi-best-practices.md
wakeup.sh
+64
View File
@@ -0,0 +1,64 @@
name: Build and Push API
on:
push:
tags:
- 'api/v*'
env:
IMAGE_NAME: git.schweitz.internal/jpmschweitzer/webber-api
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: version
run: |
# Extract version from api/v0.3.0 -> v0.3.0
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#api/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create Gitea Release
run: |
curl -sf -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"tag_name": "${{ github.ref_name }}", "name": "API Release ${{ steps.version.outputs.version }}", "body": "Automated release for webber-api ${{ steps.version.outputs.version }}"}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
build:
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
id: version
run: |
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#api/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.schweitz.internal
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./webber-api
push: true
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
- name: Trigger Watchtower update
if: success()
run: |
curl -sf -H "Authorization: Bearer ${{ secrets.WATCHTOWER_TOKEN }}" \
http://watchtower:8080/v1/update
+25
View File
@@ -0,0 +1,25 @@
name: Build and Release CLI
on:
push:
tags:
- 'cli/v*'
# TODO: Implement CLI installer build
# This workflow will be implemented when CLI distribution is ready.
# Possible targets:
# - PyPI package
# - Standalone binary (PyInstaller)
# - Platform-specific installers
jobs:
placeholder:
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: version
run: |
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#cli/}"
echo "CLI release triggered for version: $VERSION"
echo "TODO: Implement CLI build and distribution"
+11
View File
@@ -64,3 +64,14 @@ Thumbs.db
# Project specific # Project specific
*.db *.db
*.sqlite3 *.sqlite3
# Monorepo - subproject venvs (explicit for clarity)
webber-api/.venv/
webber-cli/.venv/
webber-sandbox/.venv/
# Sandbox marker file
webber-sandbox/.current_template
# Ruff cache
.ruff_cache/
+211 -81
View File
@@ -1,115 +1,245 @@
# Webber Monorepo - Agent Instructions
# AGENTS.md
> **Start every session by reading this file.** > **Start every session by reading this file.**
> This file outlines the operational protocols, coding standards, and architectural decisions for this FastAPI project. > This file contains everything you need to work with this codebase efficiently.
## 1. Agent Operational Protocols ## Quick Reference
### 🧠 Work Patterns (Plan-Act-Reflect) | Action | Command |
* **Plan:** Before writing code, briefly outline your plan. Identify which files you will touch and what the side effects might be. |--------|---------|
* **Act:** Execute the changes in small, atomic steps. | Start API server | `cd webber-api && ./wakeup.sh` |
* **Reflect:** After coding, verify your work. Did you break existing tests? Did you add new tests? | View API logs | `tail -f webber-api/logs/server.log` |
| Run API tests | `cd webber-api && .venv/bin/python -m pytest tests/ -v` |
### 🛡️ Git Discipline | Check CLI status | `cd webber-cli && .venv/bin/webber-cli status` |
* **ALWAYS add the relevant tests for the added code** Make sure to keep the test coverage up as we go, and run tests before commiting. | Load sandbox | `./sandbox.sh load calculator-cli` |
* **NEVER commit to `main` or `master` directly.** Always create a feature branch: `feature/your-feature-name` or `fix/issue-description`. | Explore sandbox | `cd webber-cli && .venv/bin/webber-cli explore "query" -d ../webber-sandbox` |
* **Commit Messages:** Use the [Conventional Commits](https://www.conventionalcommits.org/) format.
* `feat: add user login endpoint`
* `fix: resolve database connection timeout`
* `refactor: split monolith dependency file`
* **Atomic Commits:** Keep commits small. One logical change = one commit.
### 📝 Changelog Maintenance
* **Update `CHANGELOG.md`** with every user-facing change.
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
### 🚀 Release Flow
When changes are ready for deployment:
1. **Ask user if deploy cycle is desired **
2. **Update version** in `pyproject.toml`:
- Bug fixes: bump patch version (1.8.3 → 1.8.4)
- New features: bump minor version (1.8.4 → 1.9.0)
3. **Update CHANGELOG.md**:
- Move items from `[Unreleased]` to new version section
- Add release date: `## [1.8.4] - 2025-12-16`
4. **Commit and tag**:
```bash
git add -A
git commit -m "fix: description of changes"
git tag v1.8.4
git push origin main --tags
```
5. **CI/CD triggers automatically**:
- Gitea CI builds Docker image on new version tag (starts with "v")
- Watchtower pulls and deploys to production
--- ---
### 🧪 Local Development Setup ## Repository Structure
* **Always test locally first** before committing and deploying. The build-deploy loop is slow. ```
* **Only deploy** when a phase or feature is complete and tested locally webber/
* **Environment**: Copy `.env.example` to `.env` and configure for your local setup ├── webber-api/ # FastAPI backend server
│ ├── src/ # API source code
│ ├── tests/ # API tests (pytest)
│ ├── docs/ # Architecture docs, COVERAGE.md
│ ├── logs/ # Runtime logs (server.log)
│ ├── .venv/ # API virtual environment
│ ├── wakeup.sh # Dev server startup script
│ └── AGENTS.md # API-specific development guide
├── webber-cli/ # CLI client
│ ├── webber_cli/ # Python package (underscore!)
│ ├── .venv/ # CLI virtual environment
│ └── README.md # CLI usage guide
├── webber-sandbox/ # Active test project (contents swappable)
│ ├── src/ # Current project source
│ ├── tests/ # Current project tests
│ ├── .venv/ # Sandbox virtual environment
│ └── TASKS.md # Tasks for Webber to complete
├── sandbox-templates/ # Template storage
│ ├── calculator-cli/ # Simple CLI with intentional bugs
│ └── empty/ # Blank starter project
├── sandbox.sh # Sandbox management script
└── AGENTS.md # THIS FILE
```
#### ⚠️ CRITICAL: Starting the Local Server ---
**ALWAYS use `./wakeup.sh` to start the local server. NEVER use raw uvicorn commands.** ## Development Workflow
### 1. Start the API Server
```bash ```bash
cd webber-api
./wakeup.sh ./wakeup.sh
``` ```
The wakeup script provides: - **Port:** 8095 (dev), 8086 (production Docker)
- **Port conflict detection** - Warns if port 8086 is already in use - **Logs:** `webber-api/logs/server.log`
- **Virtual environment activation** - Ensures correct Python environment - **Health check:** `curl http://localhost:8095/health`
- **Centralized logging** - All logs written to `logs/server.log` for easy tailing - **API docs:** http://localhost:8095/docs
- **Auto-reload** - Code changes picked up automatically (except requirements.txt changes)
- **Consistent configuration** - Same startup every time To stop: `Ctrl+C` or `pkill -f "uvicorn src.main:app"`
### 2. Run Tests
To monitor logs in another terminal:
```bash ```bash
tail -f logs/server.log # API tests (39 tests)
cd webber-api
.venv/bin/python -m pytest tests/ -v
# With coverage
.venv/bin/python -m pytest tests/ --cov=src
# Single test file
.venv/bin/python -m pytest tests/test_tools.py -v
``` ```
To stop the server: Press `Ctrl+C` ### 3. Use the CLI
To kill a stuck server:
```bash ```bash
pkill -f "uvicorn src.main:app" cd webber-cli
# or
kill $(lsof -t -i:8086) # Check API connection
.venv/bin/webber-cli status
# Explore a directory
.venv/bin/webber-cli explore "find all python files" -d ../webber-sandbox
# Interactive chat mode
.venv/bin/webber-cli chat -d ../webber-sandbox
``` ```
#### Testing **Note:** The API server must be running for CLI commands to work.
---
## Sandbox Management
The sandbox is a swappable test project for functional testing.
### Available Templates
| Template | Description |
|----------|-------------|
| `calculator-cli` | Python CLI with intentional bugs (div-by-zero, missing tests) |
| `empty` | Blank starter project |
### Commands
**Test REST endpoints** against `http://localhost:8086`:
```bash ```bash
curl http://localhost:8086/health # List available templates
curl http://localhost:8086/ ./sandbox.sh list
curl http://localhost:8086/docs # Swagger UI
# Load a template (clears sandbox, preserves .venv)
./sandbox.sh load calculator-cli
# Reset to last loaded template
./sandbox.sh reset
# Save current sandbox as new template
./sandbox.sh save my-template
# Check current status
./sandbox.sh status
``` ```
**Running tests**: Always use the venv explicitly to avoid environment mismatches: ### After Loading a Template
```bash ```bash
.venv/bin/python -m pytest tests/ # All tests cd webber-sandbox
.venv/bin/python -m pytest tests/ -v # Verbose output source .venv/bin/activate # Create .venv first if missing
.venv/bin/python -m pytest tests/ --cov # With coverage pip install -r requirements.txt
# Read the tasks
cat TASKS.md
# Run the project's tests
pytest tests/ -v
``` ```
--- ---
## 2. FastAPI Architecture & Best Practices ## Testing Webber's Capabilities
*Reference: [FastAPI Best Practices](https://github.com/zhanymkanov/fastapi-best-practices)*
### 📂 Project Structure (Directory-based, NOT File-type based) ### Scenario: Find bugs in calculator-cli
Do **not** group files by type (e.g., one huge `routers` folder). Group by **domain/module** inside a `src/` directory.
**Correct Structure:** ```bash
```text # 1. Load the template
to be determined ./sandbox.sh load calculator-cli
# 2. Have Webber explore it
cd webber-cli
.venv/bin/webber-cli explore "find all bugs in the code" -d ../webber-sandbox
# 3. Check TASKS.md for expected bugs
cat ../webber-sandbox/TASKS.md
```
### Known bugs in calculator-cli:
- Division by zero not handled (`operations.py:divide`)
- Invalid operation causes KeyError (`main.py:get_operation`)
- Power function broken for fractional exponents
- Missing tests for divide and power functions
---
## Key Files for Debugging
| File | Purpose |
|------|---------|
| `webber-api/logs/server.log` | API server logs |
| `webber-api/src/domains/agents/explore/prompts.py` | Explore agent system prompts |
| `webber-api/src/domains/agents/explore/agent.py` | Explore agent implementation |
| `webber-api/src/ollama/provider.py` | Ollama integration (sanitizes content:null) |
| `webber-api/docs/COVERAGE.md` | Feature coverage and known issues |
---
## Versioning & Releases
Uses prefixed tags:
- `api/v0.3.0` → Triggers API Docker build
- `cli/v0.1.0` → Triggers CLI build (future)
```bash
# API release
cd webber-api
# Update version in pyproject.toml
git add -A && git commit -m "chore: release api v0.3.0"
git tag api/v0.3.0
git push origin main --tags
```
---
## Troubleshooting
### API server won't start
```bash
# Check if port is in use
lsof -i :8095
# Kill stuck process
pkill -f "uvicorn src.main:app"
```
### CLI can't connect
```bash
# Check API is running
curl http://localhost:8095/health
# Check CLI config
echo $WEBBER_API_URL # Should be http://localhost:8095
```
### Ollama errors
```bash
# Check Ollama is running
curl http://192.168.86.149:11434/api/tags
# Check model is available
curl http://192.168.86.149:11434/api/tags | grep mistral-nemo
```
### Tests failing
```bash
# Run with verbose output
cd webber-api
.venv/bin/python -m pytest tests/ -v --tb=short
```
---
## Known Limitations
1. **Model hallucination** - Mistral Nemo sometimes makes up file contents instead of using tool results
2. **No conversation memory** - CLI chat mode doesn't persist between sessions
3. **No streaming** - Responses appear all at once
See `webber-api/docs/COVERAGE.md` for full feature coverage status.
+55
View File
@@ -0,0 +1,55 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.3] - 2026-01-09
### Added
- Docker healthcheck for container health monitoring
## [0.2.2] - 2026-01-09
### Fixed
- Config parsing for empty environment variables (allowed_paths, cors_*)
- Use `env_parse_none_str=""` to treat empty strings as None
## [0.2.1] - 2026-01-09
### Fixed
- CI/CD pipeline credentials configured
## [0.2.0] - 2026-01-09
### Added
- Reference prompts from claude-code-system-prompts for all agent types
- Detailed documentation for Explore, Plan, and Task agents
- Detailed documentation for File, Shell, and Search tools
- Utility prompts (TodoWrite, AskUserQuestion, conversation summarization, etc.)
- Security review prompt for code analysis
### Changed
- Expanded agents/README.md with capabilities and use cases
- Expanded tools/README.md with parameter details and behaviors
## [0.1.0] - 2026-01-09
### Added
- Initial FastAPI boilerplate setup
- Domain-based project structure (src/domains/, src/shared/)
- BaseController pattern with lazy router instantiation
- Pydantic Settings configuration with env file support
- Logger decorator with temporal benchmarking and trace IDs
- UserProvider singleton for request-scoped context
- Custom exception hierarchy
- Health endpoints (/, /health)
- Placeholder domains for agents (explore, plan, task)
- Placeholder domains for tools (file, shell, search)
- Placeholder domain for auth (tatlock integration)
- CI/CD workflow for Gitea with Docker build and Watchtower deployment
- Dockerfile for containerized deployment
- CVE-checked dependencies (2026-01-09)
+88
View File
@@ -0,0 +1,88 @@
# Webber - Multi-Agent AI Development System
A Claude Code-inspired development assistant powered by local LLMs via Ollama.
## Structure
This is a monorepo containing three subprojects:
| Directory | Description |
|-----------|-------------|
| `webber-api/` | FastAPI backend server with agent orchestration |
| `webber-cli/` | Command-line client for interacting with the API |
| `webber-sandbox/` | Test project for functional testing |
### Additional Directories
| Directory | Description |
|-----------|-------------|
| `sandbox-templates/` | Reusable project templates for the sandbox |
| `.gitea/workflows/` | CI/CD workflows for releases |
## Quick Start
### 1. Start the API Server
```bash
cd webber-api
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
./wakeup.sh
```
### 2. Set Up the CLI
```bash
cd webber-cli
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e .
# Test connection
webber-cli status
```
### 3. Load a Sandbox Project
```bash
# From repo root
./sandbox.sh list
./sandbox.sh load calculator-cli
cd webber-sandbox
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
### 4. Explore with Webber
```bash
cd webber-cli
webber-cli explore "find all bugs in the code" -d ../webber-sandbox
```
## Versioning
This project uses prefixed tags for independent release cycles:
- `api/v0.3.0` - Triggers API Docker build and deployment
- `cli/v0.1.0` - Triggers CLI installer build (future)
## Requirements
- Python 3.12+
- Ollama running with `mistral-nemo:latest` model
- Docker (for production deployment)
## Documentation
- `webber-api/AGENTS.md` - API development guidelines
- `webber-api/docs/` - Architecture and coverage docs
- `webber-cli/README.md` - CLI usage guide
## License
MIT
-628
View File
@@ -1,628 +0,0 @@
# Webber FastAPI Boilerplate Plan
## Overview
Set up FastAPI boilerplate for "Webber" - a multi-agent AI development system (similar to Claude Code, but local with different models). Follows core-api patterns with defensive coding practices.
**Key Decision: PydanticAI Framework**
After research, [PydanticAI](https://ai.pydantic.dev/) is the recommended agent coordination framework:
- Model-agnostic: supports Ollama, OpenAI, Anthropic, and 20+ providers
- Type-safe with Pydantic validation (same ecosystem as FastAPI)
- Built-in tool/function calling with automatic schema generation
- Multi-agent support for complex workflows
- Maintained by Pydantic team (285M+ monthly downloads)
**Port: 8086** (next available slot after Headscale 8085 per CONTAINERS.md)
**Default Models (always hot in VRAM on tower-of-joy):**
- Agent reasoning: `mistral-nemo-large:latest`
- Embeddings: `nomic-embed-text:latest`
**Target Clients:**
- **Tatlock Butler**: External advisor integration for coding/software guidance
- **CLI Interface**: TBD - command-line interface for local development
**Multi-tenancy:** API key authentication integrated with tatlock-ui/core-api user management
---
## 1. Directory Structure
```
webber/
├── AGENTS.md # Expanded with defensive LLM guidelines
├── README.md # Project overview
├── CHANGELOG.md # Version history
├── pyproject.toml # Package metadata
├── requirements.txt # Production dependencies only (~= pinned)
├── requirements-dev.txt # Dev/test dependencies (pytest, pip-audit, etc.)
├── .env.example # Environment template
├── wakeup.sh # Dev startup (update port to 8086)
├── src/
│ ├── __init__.py
│ ├── main.py # FastAPI app, lifespan, user provider init
│ │ # NO routes here - delegates to domain routers
│ │
│ ├── shared/ # Cross-cutting concerns
│ │ ├── __init__.py
│ │ ├── base.py # BaseController, BaseSchema
│ │ ├── config.py # Pydantic BaseSettings
│ │ ├── logging.py # Logger decorator + centralized setup
│ │ ├── exceptions.py # Custom exception hierarchy
│ │ ├── auth.py # API key validation, multi-tenant support
│ │ └── context.py # UserProvider singleton, request context
│ │
│ └── domains/ # Feature domains (each with router.py)
│ ├── __init__.py
│ ├── router.py # Root router - includes all domain routers
│ │
│ ├── health/ # Health endpoints
│ │ ├── __init__.py
│ │ ├── router.py # Health routes
│ │ └── controller.py # Health logic
│ │
│ ├── auth/ # Authentication domain
│ │ ├── __init__.py
│ │ ├── router.py # Auth routes (API key mgmt)
│ │ ├── controller.py
│ │ └── schemas.py
│ │
│ │── agents/ # Agent domain container
│ │ ├── __init__.py
│ │ ├── router.py # Agent routes (lists agents, runs them)
│ │ ├── controller.py # Agent orchestration logic
│ │ ├── schemas.py
│ │ │
│ │ ├── explore/ # Explore agent (codebase navigation)
│ │ │ ├── __init__.py
│ │ │ ├── agent.py # PydanticAI agent definition
│ │ │ └── prompts.py # System prompts
│ │ │
│ │ ├── plan/ # Plan agent (implementation design)
│ │ │ ├── __init__.py
│ │ │ ├── agent.py
│ │ │ └── prompts.py
│ │ │
│ │ └── task/ # Task agent (execution)
│ │ ├── __init__.py
│ │ ├── agent.py
│ │ └── prompts.py
│ │
│ └── tools/ # Tool domain container
│ ├── __init__.py
│ ├── router.py # Tool routes (list tools, execute)
│ ├── controller.py # Tool orchestration
│ ├── schemas.py
│ │
│ ├── file/ # File operation tools
│ │ ├── __init__.py
│ │ ├── read.py
│ │ ├── write.py
│ │ └── glob.py
│ │
│ ├── shell/ # Shell execution tools
│ │ ├── __init__.py
│ │ └── bash.py
│ │
│ └── search/ # Search tools
│ ├── __init__.py
│ ├── grep.py
│ └── web.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_health.py
└── docs/
└── architecture.md
```
### Key Architectural Decisions
1. **Clean main.py**: Only app creation, lifespan, and UserProvider init. All routes in domain routers.
2. **Domain routers**: Each domain has `router.py` that defines routes. Root `domains/router.py` composes them.
3. **Separate agent domains**: Each agent type (explore, plan, task) in its own subdir under `agents/`.
4. **Separate tool domains**: Each tool category (file, shell, search) in its own subdir under `tools/`.
5. **UserProvider singleton**: Set once in main.py lifespan, accessible everywhere via `shared/context.py`.
6. **Multi-tenant auth**: API key validation in `shared/auth.py`, integrates with tatlock-ui/core-api.
---
## 2. Key Files to Create
### Phase 1: Foundation (fully implemented)
| File | Purpose |
|------|---------|
| `src/shared/base.py` | BaseController, BaseSchema |
| `src/shared/config.py` | Settings via Pydantic BaseSettings |
| `src/shared/logging.py` | Logger decorator + centralized setup |
| `src/shared/exceptions.py` | Custom exception hierarchy |
| `src/shared/auth.py` | API key validation, tatlock integration stub |
| `src/shared/context.py` | UserProvider singleton pattern |
| `src/main.py` | FastAPI app, lifespan, UserProvider init (no routes!) |
| `src/domains/router.py` | Root router composing all domain routers |
| `src/domains/health/router.py` | Health routes |
| `src/domains/health/controller.py` | Health logic |
| `pyproject.toml` | Package metadata, pytest config |
| `requirements.txt` | Production deps (~= pinned) |
| `requirements-dev.txt` | Dev/test deps (pytest, pip-audit) |
| `.env.example` | Environment variable template |
| `tests/conftest.py` | Pytest fixtures |
| `tests/test_health.py` | Basic endpoint tests |
### Phase 2: Placeholders (structure + README docs)
| Directory | Purpose |
|-----------|---------|
| `src/domains/auth/` | API key management (stub) |
| `src/domains/agents/` | Agent container with explore/plan/task subdirs |
| `src/domains/tools/` | Tool container with file/shell/search subdirs |
| `docs/architecture.md` | System design documentation |
---
## 3. Dependency Management
### requirements.txt (Production - baked into Docker)
```
# Webber Production Dependencies
# Minor version pinning (~=) for security patches
# CVE check date: 2026-01-09
# CVE check sources: PyPI, GitHub Advisories, Snyk, NVD
# Core FastAPI
fastapi~=0.115.0
starlette~=0.45.0
uvicorn[standard]~=0.34.0
pydantic~=2.11.0
pydantic-settings~=2.7.0
# Agent Framework
pydantic-ai~=0.0.39 # Multi-agent LLM orchestration
# HTTP
httpx~=0.28.0
aiofiles~=24.1.0
# Utilities
python-multipart~=0.0.18
python-dotenv~=1.0.0
```
### requirements-dev.txt (Dev/Test only - NOT in Docker)
```
# Webber Development Dependencies
# Install with: pip install -r requirements-dev.txt
-r requirements.txt # Include production deps
# Testing
pytest~=8.3.0
pytest-asyncio~=0.24.0
pytest-cov~=6.0.0
# Security auditing
pip-audit~=2.7.0 # Run before releases: pip-audit
# Type checking
mypy~=1.13.0
# Code formatting (optional)
# ruff~=0.8.0
```
---
## 4. AGENTS.md Additions
Add these new sections:
### Section 3: Defensive LLM Coding Practices
- Input validation requirements
- Output parsing guidelines (expect malformed responses)
- Timeout and retry policies
- Security: no secrets in prompts, sandbox execution
### Section 4: Pattern Reuse Requirements
- Search existing code before writing new
- Check `src/shared/` for base classes
- Follow domain structure template
- Code review checklist
### Section 5: CVE Check Process
- Check PyPI, GitHub Advisories, Snyk, NVD before adding deps
- Document CVE decisions in requirements.txt
- Run `pip-audit` before releases
### Section 6: Mandatory Documentation
- Required reading before work: AGENTS.md, docs/architecture.md, src/shared/base.py
- Changelog and docstring requirements
### Section 7: Project Structure Reference
- Directory tree with explanations
- Domain structure template
---
## 5. Configuration (Settings)
Environment variables for:
- **App**: DEBUG, LOG_LEVEL
- **Server**: HOST, PORT (default **8086** per CONTAINERS.md allocation)
- **CORS**: origins, methods, headers
- **LLM Models** (hot in VRAM on tower-of-joy):
- OLLAMA_URL (default: http://192.168.86.149:11434)
- OLLAMA_AGENT_MODEL (default: mistral-nemo-large:latest)
- OLLAMA_EMBED_MODEL (default: nomic-embed-text:latest)
- **Auth**:
- TATLOCK_API_URL (default: http://192.168.86.149:8000)
- Internal API key for tatlock user validation
- **Tools**: TOOL_TIMEOUT_SECONDS, SANDBOX_ENABLED, ALLOWED_PATHS
- **Sessions**: SESSION_TTL_HOURS, MAX_CONTEXT_TOKENS
---
## 6. Core Patterns
### Logger Decorator with Temporal Benchmarking (shared/logging.py)
```python
import functools
import asyncio
import time
import logging
from typing import Callable, Optional
from contextvars import ContextVar
from dataclasses import dataclass, field
from uuid import uuid4
# Trace context for nested timing
@dataclass
class TraceSpan:
name: str
trace_id: str
parent_id: Optional[str] = None
span_id: str = field(default_factory=lambda: uuid4().hex[:8])
start_time: float = field(default_factory=time.perf_counter)
end_time: Optional[float] = None
@property
def duration_ms(self) -> float:
if self.end_time is None:
return (time.perf_counter() - self.start_time) * 1000
return (self.end_time - self.start_time) * 1000
# Context variable for trace propagation
_current_span: ContextVar[Optional[TraceSpan]] = ContextVar('current_span', default=None)
_trace_id: ContextVar[Optional[str]] = ContextVar('trace_id', default=None)
def get_current_trace_id() -> Optional[str]:
"""Get current trace ID for correlation."""
return _trace_id.get()
def logged(
logger: logging.Logger = None,
slow_threshold_ms: float = 100.0,
warn_threshold_ms: float = 500.0,
include_args: bool = False,
):
"""
Decorator for automatic function logging with temporal benchmarking.
Args:
logger: Logger instance (defaults to module logger)
slow_threshold_ms: Log INFO if execution exceeds this (default 100ms)
warn_threshold_ms: Log WARNING if execution exceeds this (default 500ms)
include_args: Include function arguments in log (careful with sensitive data)
Usage:
@logged()
async def my_function(): ...
@logged(slow_threshold_ms=50, warn_threshold_ms=200)
def critical_path(): ...
"""
def decorator(func: Callable):
nonlocal logger
if logger is None:
logger = logging.getLogger(func.__module__)
func_name = f"{func.__module__}.{func.__qualname__}"
def _create_span() -> TraceSpan:
parent = _current_span.get()
trace_id = _trace_id.get() or uuid4().hex[:16]
if _trace_id.get() is None:
_trace_id.set(trace_id)
return TraceSpan(
name=func_name,
trace_id=trace_id,
parent_id=parent.span_id if parent else None,
)
def _log_completion(span: TraceSpan, error: Exception = None):
span.end_time = time.perf_counter()
duration = span.duration_ms
# Build log context
ctx = {
"trace_id": span.trace_id,
"span_id": span.span_id,
"duration_ms": round(duration, 2),
"func": func_name,
}
if span.parent_id:
ctx["parent_id"] = span.parent_id
if error:
logger.error(
f"[{span.trace_id[:8]}] {func_name} FAILED after {duration:.2f}ms: {error}",
extra=ctx,
exc_info=True
)
elif duration >= warn_threshold_ms:
logger.warning(
f"[{span.trace_id[:8]}] {func_name} SLOW: {duration:.2f}ms (threshold: {warn_threshold_ms}ms)",
extra=ctx
)
elif duration >= slow_threshold_ms:
logger.info(
f"[{span.trace_id[:8]}] {func_name} completed in {duration:.2f}ms",
extra=ctx
)
else:
logger.debug(
f"[{span.trace_id[:8]}] {func_name} completed in {duration:.2f}ms",
extra=ctx
)
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
span = _create_span()
token = _current_span.set(span)
if include_args:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}({args}, {kwargs})")
else:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}")
try:
result = await func(*args, **kwargs)
_log_completion(span)
return result
except Exception as e:
_log_completion(span, error=e)
raise
finally:
_current_span.reset(token)
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
span = _create_span()
token = _current_span.set(span)
if include_args:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}({args}, {kwargs})")
else:
logger.debug(f"[{span.trace_id[:8]}] -> {func_name}")
try:
result = func(*args, **kwargs)
_log_completion(span)
return result
except Exception as e:
_log_completion(span, error=e)
raise
finally:
_current_span.reset(token)
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
return decorator
# Convenience for manual span creation (context manager)
class trace_span:
"""
Context manager for manual span creation.
Usage:
with trace_span("database_query"):
result = await db.execute(query)
async with trace_span("llm_call"):
response = await agent.run(prompt)
"""
def __init__(self, name: str, logger: logging.Logger = None):
self.name = name
self.logger = logger or logging.getLogger(__name__)
self.span: Optional[TraceSpan] = None
self.token = None
def __enter__(self):
parent = _current_span.get()
trace_id = _trace_id.get() or uuid4().hex[:16]
if _trace_id.get() is None:
_trace_id.set(trace_id)
self.span = TraceSpan(
name=self.name,
trace_id=trace_id,
parent_id=parent.span_id if parent else None,
)
self.token = _current_span.set(self.span)
self.logger.debug(f"[{self.span.trace_id[:8]}] -> {self.name}")
return self.span
def __exit__(self, exc_type, exc_val, exc_tb):
if self.span:
self.span.end_time = time.perf_counter()
duration = self.span.duration_ms
if exc_val:
self.logger.error(f"[{self.span.trace_id[:8]}] {self.name} FAILED: {duration:.2f}ms")
else:
self.logger.debug(f"[{self.span.trace_id[:8]}] {self.name}: {duration:.2f}ms")
if self.token:
_current_span.reset(self.token)
return False
async def __aenter__(self):
return self.__enter__()
async def __aexit__(self, exc_type, exc_val, exc_tb):
return self.__exit__(exc_type, exc_val, exc_tb)
```
**Example output:**
```
DEBUG [a1b2c3d4] -> src.domains.agents.controller.run_agent
DEBUG [a1b2c3d4] -> src.domains.llm.service.call_ollama
DEBUG [a1b2c3d4] src.domains.llm.service.call_ollama: 45.23ms
INFO [a1b2c3d4] src.domains.agents.controller.run_agent completed in 156.78ms
WARN [a1b2c3d4] src.domains.tools.file.read.read_file SLOW: 523.45ms (threshold: 500ms)
```
**Features:**
- **Trace IDs**: Correlate logs across nested calls
- **Parent/child spans**: Track call hierarchy
- **Configurable thresholds**: `slow_threshold_ms` (INFO), `warn_threshold_ms` (WARNING)
- **Context manager**: `trace_span()` for manual instrumentation of code blocks
- **Zero overhead path**: Fast path for sub-threshold calls (DEBUG only)
### UserProvider Singleton (shared/context.py)
```python
from dataclasses import dataclass
from typing import Optional
from contextvars import ContextVar
@dataclass
class User:
id: str
email: str
api_key: str
tenant_id: Optional[str] = None
# Context variable for request-scoped user
_current_user: ContextVar[Optional[User]] = ContextVar('current_user', default=None)
class UserProvider:
"""Singleton for user context management."""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def set_user(self, user: User) -> None:
_current_user.set(user)
def get_user(self) -> Optional[User]:
return _current_user.get()
def clear_user(self) -> None:
_current_user.set(None)
# Global singleton
user_provider = UserProvider()
```
### BaseController (from core-api)
```python
class BaseController(ABC):
def __init__(self, prefix: str, tags: list[str]):
self.prefix = prefix
self.tags = tags
self._router = None
@abstractmethod
def create_router(self) -> APIRouter: pass
@property
def router(self) -> APIRouter:
if self._router is None:
self._router = self.create_router()
return self._router
```
### PydanticAI Agent Pattern (placeholder for future)
```python
from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
# Use the hot model from VRAM
agent = Agent(
OllamaModel('mistral-nemo-large:latest'),
system_prompt='You are a helpful assistant.',
)
@agent.tool
async def search_files(ctx, pattern: str) -> str:
"""Search for files matching pattern."""
pass # Implementation in tools/search/
```
---
## 7. Implementation Order
1. **Create directory structure** (`src/`, `src/shared/`, `src/domains/`)
2. **Implement shared modules** (base.py, config.py, logging.py, exceptions.py)
3. **Create main.py** with FastAPI app and lifespan
4. **Add health domain** as working example
5. **Set up tests** (conftest.py, test_health.py)
6. **Create placeholder domains** (llm, agents, tools - structure only)
7. **Update AGENTS.md** with new sections
8. **Create supporting files** (pyproject.toml, requirements.txt, .env.example)
9. **Add docs/architecture.md**
---
## 8. Verification
After implementation:
1. `./wakeup.sh` starts server without errors
2. `curl http://localhost:8086/health` returns healthy
3. `http://localhost:8086/docs` shows API documentation
4. `.venv/bin/python -m pytest tests/ -v` passes
5. Code follows patterns in AGENTS.md
---
## 9. Critical Reference Files
- `/mnt/media/Projects/core-api/src/shared/base.py` - BaseController pattern
- `/mnt/media/Projects/core-api/src/shared/config.py` - Settings pattern
- `/mnt/media/Projects/core-api/src/domains/health/controller.py` - Controller example
- https://ai.pydantic.dev/ - PydanticAI documentation
---
## Summary
**What will be created:**
- Complete FastAPI project structure following core-api patterns
- Working health endpoint at `http://localhost:8086/health`
- **Clean main.py** - no routes, just app init and UserProvider setup
- **Domain routers** - each domain has router.py, composed by root router
- **Logger decorator** - centralized logging via `@logged` decorator
- **UserProvider singleton** - request-scoped user context, no parameter passing
- **Multi-tenant auth stub** - API key validation ready for tatlock integration
- Separate **requirements.txt** (prod) and **requirements-dev.txt** (dev/test)
- Placeholder domains with agent/tool subdirectories (explore, plan, task / file, shell, search)
- Comprehensive AGENTS.md with defensive LLM coding practices, CVE checks, pattern reuse
- Test infrastructure with pytest
- docs/architecture.md explaining the system design
**What will NOT be created (deferred):**
- Database layer (add when needed)
- Full agent/tool implementations (PydanticAI patterns documented for future work)
- Docker/deployment configuration (can add later)
- CLI interface (TBD)
**Key decisions:**
- Port: **8086**
- Agent framework: **PydanticAI**
- Default model: **mistral-nemo-large:latest** (hot in VRAM)
- Embeddings: **nomic-embed-text:latest** (hot in VRAM)
- No database initially
- Separate prod/dev requirements
- UserProvider singleton pattern for multi-tenancy
@@ -0,0 +1,25 @@
# Python
__pycache__/
*.py[cod]
*.so
.Python
build/
dist/
*.egg-info/
# Virtual environments
.venv/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE
.idea/
.vscode/
*.swp
# Project
.current_template
+64
View File
@@ -0,0 +1,64 @@
# Calculator CLI - Tasks for Webber
A simple calculator with intentional bugs and missing features for testing Webber's capabilities.
## Bugs to Fix
### High Priority
- [ ] **Division by zero** - `operations.py:divide()` crashes when dividing by zero instead of returning an error
- [ ] **Invalid operation name** - `main.py:get_operation()` raises KeyError for unknown operations instead of helpful error message
### Medium Priority
- [ ] **Power function broken** - `operations.py:power()` doesn't handle negative exponents or fractional exponents correctly
- [ ] **No input validation** - `main.py` doesn't validate that command-line arguments are valid numbers
## Missing Tests
- [ ] Add `TestDivide` class with tests for:
- Normal division
- Division by zero (should test error handling once bug is fixed)
- Division with negative numbers
- [ ] Add `TestPower` class with tests for:
- Positive integer exponents
- Zero exponent (should return 1)
- Negative exponents
- [ ] Complete existing test classes:
- `test_add_zero`
- `test_add_floats`
- `test_subtract_negative`
- `test_multiply_by_zero`
## Features to Add
- [ ] **Expose power operation** - Add 'pow' to the operations dictionary in `main.py`
- [ ] **Add modulo operation** - Implement `modulo(a, b)` in operations.py
- [ ] **Add --verbose flag** - Show step-by-step calculation
- [ ] **Add history command** - Track and display recent calculations
- [ ] **Add REPL mode** - Interactive calculator loop
## Code Quality
- [ ] Add type hints to all functions
- [ ] Add docstrings following Google style
- [ ] Fix any linting errors (run `ruff check src/`)
## How to Run
```bash
# Setup
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Run calculator
python -m calculator.main 10 5 add
python -m calculator.main 10 5 div
# Run tests
pytest tests/ -v
# See failing tests (division by zero)
python -m calculator.main 10 0 div
```
@@ -0,0 +1,17 @@
[project]
name = "calculator"
version = "0.1.0"
description = "A simple calculator CLI with some bugs"
requires-python = ">=3.12"
[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v"
@@ -0,0 +1,2 @@
# Calculator CLI dependencies
pytest>=8.0.0
@@ -0,0 +1,3 @@
"""Calculator CLI - A simple calculator with some bugs for testing."""
__version__ = "0.1.0"
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Calculator CLI - A simple command-line calculator.
NOTE: This file contains intentional bugs for testing purposes.
Usage:
python -m calculator.main 10 5 add
python -m calculator.main 10 5 sub
python -m calculator.main 10 5 mul
python -m calculator.main 10 5 div
"""
import sys
from calculator.operations import add, subtract, multiply, divide
def get_operation(op_name: str):
"""
Get the operation function by name.
BUG: No validation - invalid operation names cause KeyError!
"""
operations = {
"add": add,
"sub": subtract,
"mul": multiply,
"div": divide,
# BUG: 'power' is implemented in operations.py but not exposed here
}
# BUG: Should handle KeyError gracefully
return operations[op_name]
def main():
"""Main entry point."""
if len(sys.argv) != 4:
print("Usage: python -m calculator.main <a> <b> <operation>")
print("Operations: add, sub, mul, div")
sys.exit(1)
# BUG: No validation that a and b are valid numbers
a = float(sys.argv[1])
b = float(sys.argv[2])
op_name = sys.argv[3]
# BUG: This will crash with KeyError for invalid operation
operation = get_operation(op_name)
result = operation(a, b)
print(f"Result: {result}")
if __name__ == "__main__":
main()
@@ -0,0 +1,43 @@
"""
Math operations for the calculator.
NOTE: This file contains intentional bugs for testing purposes.
"""
def add(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
def subtract(a: float, b: float) -> float:
"""Subtract b from a."""
return a - b
def multiply(a: float, b: float) -> float:
"""Multiply two numbers."""
return a * b
def divide(a: float, b: float) -> float:
"""
Divide a by b.
BUG: Does not handle division by zero!
"""
# BUG: No check for b == 0
return a / b
def power(a: float, b: float) -> float:
"""
Raise a to the power of b.
BUG: Negative exponents not handled correctly for some cases.
"""
# BUG: This naive implementation has issues with negative bases and fractional exponents
result = 1
for _ in range(int(b)):
result *= a
return result
@@ -0,0 +1 @@
"""Calculator tests."""
@@ -0,0 +1,46 @@
"""
Tests for calculator operations.
NOTE: Test coverage is intentionally incomplete for testing purposes.
"""
import pytest
from calculator.operations import add, subtract, multiply
class TestAdd:
"""Tests for add operation."""
def test_add_positive_numbers(self):
assert add(2, 3) == 5
def test_add_negative_numbers(self):
assert add(-2, -3) == -5
# MISSING: test_add_zero, test_add_floats
class TestSubtract:
"""Tests for subtract operation."""
def test_subtract_positive(self):
assert subtract(5, 3) == 2
# MISSING: test_subtract_negative, test_subtract_resulting_negative
class TestMultiply:
"""Tests for multiply operation."""
def test_multiply_positive(self):
assert multiply(3, 4) == 12
# MISSING: test_multiply_by_zero, test_multiply_negative
# MISSING: TestDivide class entirely!
# - test_divide_positive
# - test_divide_by_zero (should test error handling)
# - test_divide_negative
# MISSING: TestPower class entirely!
+25
View File
@@ -0,0 +1,25 @@
# Python
__pycache__/
*.py[cod]
*.so
.Python
build/
dist/
*.egg-info/
# Virtual environments
.venv/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE
.idea/
.vscode/
*.swp
# Project
.current_template
+18
View File
@@ -0,0 +1,18 @@
# My Project - Tasks
A blank starter template. Define your own tasks here.
## Setup
```bash
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## Tasks
- [ ] Define your project goals
- [ ] Add source files to `src/myproject/`
- [ ] Add tests to `tests/`
- [ ] Update `requirements.txt` with dependencies
+17
View File
@@ -0,0 +1,17 @@
[project]
name = "myproject"
version = "0.1.0"
description = "A blank starter project"
requires-python = ">=3.12"
[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v"
+2
View File
@@ -0,0 +1,2 @@
# Add your dependencies here
pytest>=8.0.0
@@ -0,0 +1,3 @@
"""My Project - A blank starter template."""
__version__ = "0.1.0"
@@ -0,0 +1 @@
"""Tests for myproject."""
Executable
+193
View File
@@ -0,0 +1,193 @@
#!/bin/bash
# Sandbox management script for Webber testing
#
# Usage:
# ./sandbox.sh list - List available templates
# ./sandbox.sh load <template> - Load a template into sandbox
# ./sandbox.sh reset - Reset sandbox to last loaded template
# ./sandbox.sh save <name> - Save current sandbox as new template
# ./sandbox.sh status - Show current sandbox status
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SANDBOX_DIR="$SCRIPT_DIR/webber-sandbox"
TEMPLATES_DIR="$SCRIPT_DIR/sandbox-templates"
MARKER_FILE="$SANDBOX_DIR/.current_template"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
usage() {
echo "Webber Sandbox Manager"
echo ""
echo "Usage: ./sandbox.sh <command> [template]"
echo ""
echo "Commands:"
echo " list List available templates"
echo " load <template> Load a template into sandbox (preserves .venv)"
echo " reset Reset sandbox to last loaded template"
echo " save <name> Save current sandbox as new template"
echo " status Show current sandbox status"
echo ""
echo "Available templates:"
ls -1 "$TEMPLATES_DIR" 2>/dev/null || echo " (none)"
}
list_templates() {
echo "Available templates:"
echo ""
for dir in "$TEMPLATES_DIR"/*/; do
if [ -d "$dir" ]; then
name=$(basename "$dir")
desc=""
if [ -f "$dir/TASKS.md" ]; then
desc=$(head -1 "$dir/TASKS.md" | sed 's/^#\s*//')
fi
printf " %-20s %s\n" "$name" "$desc"
fi
done
}
load_template() {
local template="$1"
if [ -z "$template" ]; then
echo -e "${RED}Error: Template name required${NC}"
echo "Usage: ./sandbox.sh load <template>"
exit 1
fi
if [ ! -d "$TEMPLATES_DIR/$template" ]; then
echo -e "${RED}Error: Template '$template' not found${NC}"
echo "Available templates:"
ls -1 "$TEMPLATES_DIR"
exit 1
fi
echo -e "${YELLOW}Loading template: $template${NC}"
# Create sandbox dir if needed
mkdir -p "$SANDBOX_DIR"
# Clear sandbox contents (except .venv and .git)
find "$SANDBOX_DIR" -mindepth 1 -maxdepth 1 ! -name '.venv' ! -name '.git' -exec rm -rf {} +
# Copy template contents (including hidden files)
cp -r "$TEMPLATES_DIR/$template/." "$SANDBOX_DIR/"
# Mark which template was loaded
echo "$template" > "$MARKER_FILE"
echo -e "${GREEN}Loaded template: $template${NC}"
echo ""
echo "Next steps:"
echo " cd webber-sandbox"
if [ ! -d "$SANDBOX_DIR/.venv" ]; then
echo " python3.12 -m venv .venv"
fi
echo " source .venv/bin/activate"
echo " pip install -r requirements.txt"
echo ""
if [ -f "$SANDBOX_DIR/TASKS.md" ]; then
echo "Tasks available in TASKS.md"
fi
}
reset_template() {
if [ ! -f "$MARKER_FILE" ]; then
echo -e "${RED}Error: No template loaded yet${NC}"
echo "Use './sandbox.sh load <template>' first"
exit 1
fi
local template=$(cat "$MARKER_FILE")
echo "Resetting to template: $template"
load_template "$template"
}
save_template() {
local name="$1"
if [ -z "$name" ]; then
echo -e "${RED}Error: Template name required${NC}"
echo "Usage: ./sandbox.sh save <name>"
exit 1
fi
if [ -d "$TEMPLATES_DIR/$name" ]; then
echo -e "${YELLOW}Warning: Template '$name' already exists${NC}"
read -p "Overwrite? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled"
exit 0
fi
rm -rf "$TEMPLATES_DIR/$name"
fi
mkdir -p "$TEMPLATES_DIR/$name"
# Copy sandbox contents (except .venv, .git, __pycache__)
rsync -a --exclude='.venv' --exclude='.git' --exclude='__pycache__' \
--exclude='*.pyc' --exclude='.pytest_cache' --exclude='.mypy_cache' \
"$SANDBOX_DIR/" "$TEMPLATES_DIR/$name/"
echo -e "${GREEN}Saved template: $name${NC}"
}
show_status() {
echo "Sandbox Status"
echo "=============="
echo ""
echo "Sandbox directory: $SANDBOX_DIR"
if [ -f "$MARKER_FILE" ]; then
echo "Current template: $(cat "$MARKER_FILE")"
else
echo "Current template: (none loaded)"
fi
if [ -d "$SANDBOX_DIR/.venv" ]; then
echo "Virtual env: exists"
else
echo "Virtual env: not created"
fi
echo ""
echo "Contents:"
if [ -d "$SANDBOX_DIR" ]; then
ls -la "$SANDBOX_DIR" 2>/dev/null | tail -n +4
else
echo " (sandbox not initialized)"
fi
}
# Main command dispatch
case "${1:-}" in
list)
list_templates
;;
load)
load_template "$2"
;;
reset)
reset_template
;;
save)
save_template "$2"
;;
status)
show_status
;;
-h|--help|"")
usage
;;
*)
echo -e "${RED}Unknown command: $1${NC}"
usage
exit 1
;;
esac
-53
View File
@@ -1,53 +0,0 @@
# Agents Domain
This domain contains PydanticAI agent definitions and orchestration.
## Structure
```
agents/
├── router.py # Agent routes (list, run)
├── controller.py # Agent orchestration logic
├── schemas.py # Request/response models
├── explore/ # Explore agent - codebase navigation
│ ├── agent.py # PydanticAI agent definition
│ └── prompts.py # System prompts
├── plan/ # Plan agent - implementation design
│ ├── agent.py
│ └── prompts.py
└── task/ # Task agent - execution
├── agent.py
└── prompts.py
```
## PydanticAI Pattern
```python
from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
from src.shared.config import get_settings
settings = get_settings()
explore_agent = Agent(
OllamaModel(settings.ollama_agent_model, base_url=settings.ollama_url),
system_prompt='You are a code exploration assistant...',
)
@explore_agent.tool
async def search_files(ctx, pattern: str) -> str:
"""Search for files matching pattern."""
# Implementation uses tools from src/domains/tools/
pass
```
## Adding a New Agent
1. Create a new directory under `agents/` (e.g., `agents/review/`)
2. Create `agent.py` with PydanticAI Agent definition
3. Create `prompts.py` with system prompts
4. Register in `controller.py`
5. Add tests in `tests/domains/test_agents/`
-70
View File
@@ -1,70 +0,0 @@
# Tools Domain
This domain contains tool implementations for agent use.
## Structure
```
tools/
├── router.py # Tool routes (list, execute)
├── controller.py # Tool orchestration
├── schemas.py # Tool request/response models
├── file/ # File operation tools
│ ├── read.py # Read file contents
│ ├── write.py # Write file contents
│ └── glob.py # Find files by pattern
├── shell/ # Shell execution tools
│ └── bash.py # Execute bash commands
└── search/ # Search tools
├── grep.py # Search file contents
└── web.py # Web search
```
## Tool Pattern
Tools are registered with PydanticAI agents via the `@agent.tool` decorator.
Each tool should:
1. Have clear input/output types
2. Include a docstring (used by LLM)
3. Handle errors gracefully
4. Respect sandbox settings
```python
from src.shared.config import get_settings
from src.shared.logging import logged
settings = get_settings()
@logged()
async def read_file(file_path: str, limit: int = 2000) -> str:
"""
Read contents of a file.
Args:
file_path: Absolute path to the file
limit: Maximum lines to read
Returns:
File contents as string
"""
# Check path is allowed
if settings.sandbox_enabled:
# Validate against allowed_paths
pass
# Read and return
pass
```
## Adding a New Tool
1. Create a new file in appropriate category (file/, shell/, search/)
2. Implement the tool function with proper types and docstring
3. Add `@logged()` decorator for timing
4. Handle sandbox restrictions
5. Register with agents that need it
6. Add tests
View File
View File
View File
View File
View File
+128
View File
@@ -0,0 +1,128 @@
# AGENTS.md
> **Start every session by reading this file.**
> This file outlines the operational protocols, coding standards, and architectural decisions for this FastAPI project.
## 1. Agent Operational Protocols
### 🧠 Work Patterns (Plan-Act-Reflect)
* **Plan:** Before writing code, briefly outline your plan. Identify which files you will touch and what the side effects might be.
* **Act:** Execute the changes in small, atomic steps.
* **Reflect:** After coding, verify your work. Did you break existing tests? Did you add new tests?
### 🛡️ Git Discipline
* **ALWAYS add the relevant tests for the added code** Make sure to keep the test coverage up as we go, and run tests before commiting.
* **NEVER commit to `main` or `master` directly.** Always create a feature branch: `feature/your-feature-name` or `fix/issue-description`.
* **Commit Messages:** Use the [Conventional Commits](https://www.conventionalcommits.org/) format.
* `feat: add user login endpoint`
* `fix: resolve database connection timeout`
* `refactor: split monolith dependency file`
* **Atomic Commits:** Keep commits small. One logical change = one commit.
### 📝 Changelog Maintenance
* **Update `CHANGELOG.md`** with every user-facing change.
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
### 🚀 Release Flow
When changes are ready for deployment:
1. **Ask user if deploy cycle is desired **
2. **Update version** in `pyproject.toml`:
- Bug fixes: bump patch version (1.8.3 → 1.8.4)
- New features: bump minor version (1.8.4 → 1.9.0)
3. **Update CHANGELOG.md**:
- Move items from `[Unreleased]` to new version section
- Add release date: `## [1.8.4] - 2025-12-16`
4. **Commit and tag**:
```bash
git add -A
git commit -m "fix: description of changes"
git tag v1.8.4
git push origin main --tags
```
5. **CI/CD triggers automatically**:
- Gitea CI builds Docker image on new version tag (starts with "v")
- Watchtower pulls and deploys to production
- Verify deployment: `curl http://192.168.86.149:8086/health`
---
### 🧪 Local Development Setup
* **Always test locally first** before committing and deploying. The build-deploy loop is slow.
* **Only deploy** when a phase or feature is complete and tested locally
* **Environment**: Copy `.env.example` to `.env` and configure for your local setup
#### ⚠️ CRITICAL: Starting the Local Server
**ALWAYS use `./wakeup.sh` to start the local server. NEVER use raw uvicorn commands.**
```bash
./wakeup.sh
```
The wakeup script provides:
- **Port conflict detection** - Warns if port 8086 is already in use
- **Virtual environment activation** - Ensures correct Python environment
- **Centralized logging** - All logs written to `logs/server.log` for easy tailing
- **Auto-reload** - Code changes picked up automatically (except requirements.txt changes)
- **Consistent configuration** - Same startup every time
To monitor logs in another terminal:
```bash
tail -f logs/server.log
```
To stop the server: Press `Ctrl+C`
To kill a stuck server:
```bash
pkill -f "uvicorn src.main:app"
# or
kill $(lsof -t -i:8086)
```
#### Testing
**Test REST endpoints** against `http://localhost:8086`:
```bash
curl http://localhost:8086/health
curl http://localhost:8086/
curl http://localhost:8086/docs # Swagger UI
```
**Running tests**: Always use the venv explicitly to avoid environment mismatches:
```bash
.venv/bin/python -m pytest tests/ # All tests
.venv/bin/python -m pytest tests/ -v # Verbose output
.venv/bin/python -m pytest tests/ --cov # With coverage
```
---
## 1.5 Known Issues & Future Improvements
### Explore Agent
- **Model Hallucination**: Mistral Nemo sometimes hallucinates file contents instead of using actual tool results. Consider using a more capable model (codestral, qwen2.5-coder) or adding response validation.
- **Ollama Provider**: We use a custom `WebberOllamaProvider` (ported from tatlock) that sanitizes `content: null` to `content: ""` for assistant messages with tool calls. This works around an Ollama API limitation.
- **Gitignore Support**: ✅ Fixed - The filesystem tools now honor `.gitignore` patterns and default ignores (`.venv/`, `__pycache__/`, `node_modules/`, etc.).
---
## 2. FastAPI Architecture & Best Practices
*Reference: [FastAPI Best Practices](https://github.com/zhanymkanov/fastapi-best-practices)*
### 📂 Project Structure (Directory-based, NOT File-type based)
Do **not** group files by type (e.g., one huge `routers` folder). Group by **domain/module** inside a `src/` directory.
**Correct Structure:**
```text
to be determined
+25
View File
@@ -0,0 +1,25 @@
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies (curl for healthcheck)
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY pyproject.toml .
COPY src/ ./src/
ENV PYTHONPATH=/app
EXPOSE 8086
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8086/health || exit 1
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8086", "--workers", "1"]
+185
View File
@@ -0,0 +1,185 @@
# Webber Feature Coverage
> Tracking progress towards Claude Code-like functionality
## Current Status: ~40% Complete
Last updated: 2026-01-10
---
## Phase 1-6: Foundation (Original Plan)
### Phase 1: Tool Infrastructure ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `BaseTool` abstract class | ✅ | `src/domains/tools/base.py` |
| `ToolResult` dataclass | ✅ | Consistent success/error/truncated handling |
| `ReadFileTool` | ✅ | With line numbers, offset/limit support |
| `GlobFilesTool` | ✅ | Pattern matching, sorted by mtime |
| `GrepContentTool` | ✅ | Regex search with context lines |
| `BashReadOnlyTool` | ✅ | Allowlist-based command filtering |
| Path validation | ✅ | `allowed_paths` restriction |
**Status:** Tools now honor `.gitignore` patterns and default ignores (`.venv/`, `__pycache__/`, etc.)
### Phase 2: Explore Agent ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `BaseAgent` abstract class | ✅ | `src/domains/agents/base.py` |
| Agent registry | ✅ | `register_agent()`, `get_agent()`, `list_agents()` |
| `ExploreAgentImpl` | ✅ | PydanticAI-based implementation |
| System prompts | ✅ | Mistral-optimized with tool examples |
| Tool registration | ✅ | `@agent.tool` decorator pattern |
| Sanitized Ollama provider | ✅ | Fixes `content: null` issue |
**Gap:** Mistral Nemo sometimes hallucinates instead of using tool results.
### Phase 3: CLI Foundation ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| Typer + Rich setup | ✅ | Both `src/cli` and standalone `cli/` |
| `webber --version` | ✅ | Shows version from pyproject.toml |
| Console theming | ✅ | Centralized color palette |
| Markdown rendering | ✅ | Rich markdown output |
### Phase 4: Agentic Loop ⚠️ Partial
| Component | Status | Notes |
|-----------|--------|-------|
| `webber chat` command | ✅ | Interactive mode works |
| `webber explore` command | ✅ | One-shot query works |
| `SessionState` dataclass | ✅ | Basic context tracking |
| `AgenticLoop` class | ⚠️ | Basic implementation, not fully utilized |
| Conversation history | ❌ | Not persisted between turns in CLI |
| Context management | ❌ | No token counting or summarization |
### Phase 5: REST API ✅ Complete
| Component | Status | Notes |
|-----------|--------|-------|
| `POST /agents/run` | ✅ | Execute agent with prompt |
| `GET /agents/` | ✅ | List available agents |
| `GET /agents/{name}` | ✅ | Get agent info |
| Request/response schemas | ✅ | Pydantic models |
### Phase 6: Polish & Tests ⚠️ Partial
| Component | Status | Notes |
|-----------|--------|-------|
| Tool unit tests | ✅ | 17 tests covering all tools |
| API endpoint tests | ✅ | 5 tests for agent routes |
| Health check tests | ✅ | 2 tests |
| Integration tests | ❌ | No real LLM integration tests |
| CLI E2E tests | ❌ | Not implemented |
| Streaming responses | ❌ | Not implemented |
---
## Future Work: Remaining Features
### High Priority
| Feature | Category | Description | Complexity |
|---------|----------|-------------|------------|
| **Write tool** | Tools | Create new files | Medium |
| **Edit tool** | Tools | old_string/new_string pattern like Claude | Medium |
| **Full Bash tool** | Tools | Write-enabled shell for Task agent | Medium |
| **Plan Agent** | Agents | Design implementation approaches | High |
| **Task Agent** | Agents | Autonomous multi-step execution | High |
| **Context summarization** | Infrastructure | Compress history at token limit | High |
| **Conversation persistence** | CLI | Multi-turn memory in chat mode | Medium |
### Medium Priority
| Feature | Category | Description | Complexity |
|---------|----------|-------------|------------|
| **Streaming responses** | CLI | Real-time token display | Medium |
| **Web search tool** | Tools | External search API integration | Medium |
| **Tool result caching** | Infrastructure | Cache file reads for performance | Low |
| **Session persistence** | CLI | Save/resume conversations | Medium |
| **Todo tracking** | CLI | Built-in task list (`/todo`) | Medium |
| **Git integration** | CLI | Auto-commit, branch management | Medium |
| **Agent handoff** | Orchestration | Explore → Plan → Task workflow | High |
| **Retry logic** | Infrastructure | Auto-retry on tool failures | Low |
### Low Priority
| Feature | Category | Description | Complexity |
|---------|----------|-------------|------------|
| **Notebook editing** | Tools | Jupyter cell manipulation | Medium |
| **MCP support** | Infrastructure | Model Context Protocol | High |
| **Config file** | CLI | `~/.webber/config.toml` | Low |
| **IDE integration** | CLI | VS Code extension | High |
| **Parallel agents** | Orchestration | Concurrent agent execution | High |
| **Agent memory** | Orchestration | Shared context between agents | Medium |
---
## Testing Coverage Gaps
| Area | Current | Target | Gap |
|------|---------|--------|-----|
| Tool unit tests | 17 | 17 | ✅ |
| API tests | 5 | 10 | Need error handling, edge cases |
| Integration tests | 0 | 5 | Agent + real LLM tests |
| CLI E2E tests | 0 | 10 | Full workflow tests |
| Security tests | 0 | 5 | Path traversal, injection |
---
## Known Issues
1. **Model hallucination** - Mistral Nemo sometimes makes up file contents instead of using actual tool results.
2. **No conversation memory** - CLI chat mode doesn't persist context between sessions.
3. **No streaming** - Responses appear all at once, no real-time token display.
4. **Temperature setting** - Changed from 0.0 to 0.3 for Mistral Nemo compatibility, may affect determinism.
---
## Architecture Decisions Made
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Separate CLI package | `cli/` at root | Can be extracted as standalone client |
| Sanitized Ollama provider | Custom wrapper | Fixes PydanticAI + Ollama `content: null` bug |
| Dev port 8095 | Separate from prod 8086 | Avoid conflicts with Docker deployment |
| Tool choice "required" | Force tool use | Mistral Nemo needs explicit instruction |
| Temperature 0.3 | Mistral recommendation | 0.0 caused issues with Nemo |
---
## Estimated Effort to Full Parity
| Milestone | Effort | Features |
|-----------|--------|----------|
| **MVP (current)** | Done | Explore agent, basic CLI, REST API |
| **Usable daily driver** | 2-3 weeks | Write/Edit tools, Plan agent, git integration |
| **Claude Code parity** | 2-3 months | Task agent, streaming, MCP, IDE integration |
---
## Quick Reference: What Works Now
```bash
# Start dev server
./wakeup.sh
# CLI commands
.venv/bin/webber-cli status # Check API connection
.venv/bin/webber-cli explore "find tests" # One-shot exploration
.venv/bin/webber-cli chat # Interactive mode
# API endpoints
curl http://localhost:8095/health
curl http://localhost:8095/agents/
curl -X POST http://localhost:8095/agents/run \
-H "Content-Type: application/json" \
-d '{"agent_type":"explore","prompt":"list python files","working_dir":"."}'
```
+349
View File
@@ -0,0 +1,349 @@
# Webber Architecture
Multi-Agent AI Development System - similar to Claude Code but running locally with configurable models.
## Overview
Webber is a FastAPI-based agent orchestration service that provides:
- Multi-agent execution (Explore, Plan, Task)
- Tool capabilities (file operations, shell, search)
- Multi-tenant authentication via Tatlock integration
- PydanticAI framework for LLM orchestration
**Port:** 8086
**Runtime:** Python 3.12, FastAPI, Uvicorn
**Agent Framework:** PydanticAI
**Default LLM:** Ollama with mistral-nemo-large:latest
---
## Directory Structure
```
webber/
├── src/
│ ├── main.py # App entry point (NO routes)
│ │
│ ├── shared/ # Cross-cutting concerns
│ │ ├── base.py # BaseController, BaseSchema
│ │ ├── config.py # Pydantic Settings
│ │ ├── logging.py # @logged decorator, trace_span
│ │ ├── exceptions.py # Custom exception hierarchy
│ │ ├── auth.py # API key validation
│ │ └── context.py # UserProvider singleton
│ │
│ └── domains/ # Feature domains
│ ├── router.py # Root router (composes all)
│ ├── health/ # Health endpoints
│ ├── auth/ # Authentication
│ ├── agents/ # Agent orchestration
│ │ ├── explore/ # Codebase navigation
│ │ ├── plan/ # Implementation design
│ │ └── task/ # Execution
│ └── tools/ # Tool execution
│ ├── file/ # Read, write, glob
│ ├── shell/ # Bash execution
│ └── search/ # Grep, web search
├── tests/
├── docs/
└── logs/
```
---
## Core Patterns
### 1. Clean main.py
The entry point contains ONLY:
- FastAPI app creation with lifespan
- CORS middleware
- Auth middleware (sets UserProvider)
- Exception handlers
- Single router include (`root_router`)
All routes live in domain routers. This keeps main.py focused on app initialization.
### 2. Domain-Based Structure
Each feature domain has its own directory:
```
domains/
├── router.py # Root router composing all domains
├── health/
│ ├── router.py # Domain routes
│ └── controller.py # Business logic
├── agents/
│ ├── router.py # Agent routes
│ ├── controller.py # Orchestration logic
│ ├── schemas.py # Request/response models
│ └── explore/ # Agent implementation
│ ├── agent.py # PydanticAI agent
│ └── prompts.py # System prompts
```
### 3. BaseController Pattern
Controllers use lazy router instantiation:
```python
from src.shared.base import BaseController
class MyController(BaseController):
def __init__(self):
super().__init__(prefix="/my", tags=["My"])
def create_router(self) -> APIRouter:
router = APIRouter(prefix=self.prefix, tags=self.tags)
@router.get("/")
async def list_items():
return []
return router
my_controller = MyController()
# Use: my_controller.router
```
### 4. UserProvider Singleton
Request-scoped user context without parameter passing:
```python
# In middleware (main.py):
user = await validate_api_key(api_key)
user_provider.set_user(user)
try:
response = await call_next(request)
finally:
user_provider.clear_user()
# Anywhere in code:
from src.shared.context import get_current_user, require_user
user = get_current_user() # Returns None if not authenticated
user = require_user() # Raises if not authenticated
```
Uses Python's `contextvars` for async-safe request isolation.
### 5. Logger with Temporal Benchmarking
The `@logged()` decorator automatically tracks execution time:
```python
from src.shared.logging import logged, trace_span, get_logger
logger = get_logger(__name__)
@logged()
async def my_function():
# Automatically logs entry, exit, and duration
pass
@logged(slow_threshold_ms=50, warn_threshold_ms=200)
def critical_path():
# Custom thresholds for performance-critical code
pass
async def complex_operation():
async with trace_span("llm_call"):
# Manual span for specific sections
result = await agent.run(prompt)
```
Features:
- Trace ID correlation across nested calls
- Configurable slow/warn thresholds
- DEBUG: all calls logged with timing
- INFO: slow calls (>100ms default)
- WARNING: very slow calls (>500ms default)
- ERROR: failed calls with stack trace
### 6. Exception Hierarchy
```python
from src.shared.exceptions import (
AppException,
NotFoundError,
ValidationError,
AuthenticationError,
AuthorizationError,
)
# Raise with context:
raise NotFoundError("User", user_id)
raise ValidationError("email", "Invalid format")
# Automatic JSON response via exception handlers in main.py
```
---
## Configuration
All settings via environment variables or `.env`:
| Variable | Default | Description |
|----------|---------|-------------|
| DEBUG | false | Enable debug mode |
| LOG_LEVEL | INFO | Logging level |
| HOST | 0.0.0.0 | Server host |
| PORT | 8086 | Server port |
| OLLAMA_URL | http://192.168.86.149:11434 | Ollama API URL |
| OLLAMA_AGENT_MODEL | mistral-nemo-large:latest | Agent reasoning model |
| OLLAMA_EMBED_MODEL | nomic-embed-text:latest | Embedding model |
| TATLOCK_API_URL | http://192.168.86.149:8000 | Tatlock auth service |
| TOOL_TIMEOUT_SECONDS | 120 | Tool execution timeout |
| SANDBOX_ENABLED | true | Enable sandboxed execution |
| ALLOWED_PATHS | [] | Paths accessible to tools |
| SESSION_TTL_HOURS | 24 | Session expiry |
| MAX_CONTEXT_TOKENS | 128000 | Max context window |
---
## Agent Architecture
Webber uses PydanticAI for agent orchestration. Each agent type is purpose-built:
### Explore Agent
Fast codebase exploration for:
- Finding files by pattern
- Searching code for keywords
- Answering questions about structure
### Plan Agent
Implementation design for:
- Analyzing requirements
- Creating step-by-step plans
- Identifying files to modify
- Considering trade-offs
### Task Agent
Autonomous execution for:
- Multi-step implementations
- Tool orchestration
- Code generation and modification
---
## Tool Architecture
Tools are sandboxed operations agents can invoke:
### File Tools
- **Read**: Read file contents with line limits
- **Write**: Create or overwrite files
- **Edit**: String replacement in files
- **Glob**: Pattern-based file search
### Shell Tools
- **Bash**: Command execution with timeout
- Sandboxed to allowed paths
- Captures stdout/stderr
### Search Tools
- **Grep**: Regex content search via ripgrep
- **WebSearch**: Web search integration (optional)
---
## Authentication Flow
1. Client sends `X-API-Key` header
2. Auth middleware calls `validate_api_key()`
3. Tatlock validates key and returns user info
4. UserProvider stores user in request context
5. Routes access via `get_current_user()` or `require_user()`
6. Middleware clears user in `finally` block
---
## Deployment
### Docker
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY pyproject.toml .
COPY src/ ./src/
ENV PYTHONPATH=/app
EXPOSE 8086
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8086/health || exit 1
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8086"]
```
### CI/CD
Gitea Actions workflow:
1. Push tag `v*` triggers build
2. Creates Gitea release
3. Builds and pushes Docker image to registry
4. Watchtower auto-deploys to production
### Production Stack
Deployed in Portainer `agents` stack alongside Tatlock:
- Network: `docker-dataplane`
- Registry: `git.schweitz.internal/jpmschweitzer/webber`
- Auto-update: Watchtower with label `com.centurylinklabs.watchtower.enable=true`
---
## Adding New Domains
1. Create domain directory under `src/domains/`
2. Add `router.py` with routes
3. Add `controller.py` with business logic
4. Add `schemas.py` for request/response models
5. Import and include router in `src/domains/router.py`
6. Add tests in `tests/test_<domain>.py`
---
## Adding New Agents
1. Create agent directory under `src/domains/agents/`
2. Add `agent.py` with PydanticAI agent definition
3. Add `prompts.py` with system prompts
4. Register in agents controller
5. Document in `src/domains/agents/README.md`
---
## Adding New Tools
1. Create tool file under appropriate `src/domains/tools/` subdir
2. Implement tool function with type hints
3. Register as PydanticAI tool
4. Document in `src/domains/tools/README.md`
---
## Testing
```bash
# Run all tests
./wakeup.sh # Start server first
pytest tests/ -v
# With coverage
pytest tests/ --cov=src --cov-report=html
```
---
## Security Considerations
- All tool execution is sandboxed when `SANDBOX_ENABLED=true`
- File operations restricted to `ALLOWED_PATHS`
- No secrets in prompts
- Input validation via Pydantic
- Output parsing expects malformed LLM responses
- Timeouts on all tool execution
+14
View File
@@ -0,0 +1,14 @@
## Background
Research with Gemini identified key issues with mistral-nemo and tool calling:
- "Pre-computation Hallucination" - model answers before using tools
- High default temperature (0.7-0.8) causes wandering
- Model is "chatty and confident" - needs explicit constraints
## Key Recommendations from Gemini Research
1. **Temperature 0.0** for tool-calling agents (deterministic, follows schema)
2. **Chain of Thought (CoT)** - force step-by-step reasoning
3. **Negative constraints** - tell model what NOT to do (Nemo responds better)
4. **Explicit tool descriptions** - verbose docstrings with "never estimate yourself"
5. **"Strictly tool-based assistant"** pattern - NO internal knowledge claim
+69
View File
@@ -0,0 +1,69 @@
[project]
name = "webber-api"
version = "0.3.0"
description = "Webber API - Multi-Agent AI Development Server"
authors = [
{name = "jpmschweitzer"}
]
readme = "README.md"
requires-python = ">=3.12"
license = {text = "MIT"}
classifiers = [
"Development Status :: 3 - Alpha",
"Framework :: FastAPI",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Code Generators",
]
[build-system]
requires = ["setuptools>=75.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["."]
include = ["src*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v"
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_ignores = true
strict = false
ignore_missing_imports = true
[tool.ruff]
target-version = "py312"
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"RUF", # Ruff-specific rules
]
ignore = [
"E501", # line too long (handled by formatter)
"B008", # function call in default argument (FastAPI Depends)
"B904", # raise without from (sometimes intentional)
]
[tool.ruff.lint.isort]
known-first-party = ["src"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
@@ -15,3 +15,6 @@ pip-audit~=2.9.0
# Type checking # Type checking
mypy~=1.19.1 mypy~=1.19.1
# Linting and formatting
ruff~=0.9.4
@@ -17,6 +17,11 @@ pydantic-ai~=1.40.0
httpx~=0.28.1 httpx~=0.28.1
aiofiles~=25.1.0 aiofiles~=25.1.0
# CLI
typer~=0.15.0
rich~=13.9.0
# Utilities # Utilities
python-multipart~=0.0.21 python-multipart~=0.0.21
python-dotenv~=1.2.1 python-dotenv~=1.2.1
pathspec~=0.12.1 # Gitignore pattern matching
+3
View File
@@ -0,0 +1,3 @@
"""
Webber CLI - Command-line interface for the multi-agent system.
"""
+6
View File
@@ -0,0 +1,6 @@
"""
CLI commands.
"""
from src.cli.commands import chat, explore, version
__all__ = ["chat", "explore", "version"]
+128
View File
@@ -0,0 +1,128 @@
"""
Chat command - interactive conversation mode.
"""
import asyncio
from pathlib import Path
import typer
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
from src.cli.session.loop import AgenticLoop
from src.shared.logging import setup_logging
console = get_console()
def chat_command(
directory: str = typer.Option(
".",
"--directory",
"-d",
help="Working directory to explore",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-V",
help="Show detailed output and debug logging",
),
) -> None:
"""
Start interactive chat session.
Enters a conversation loop where you can ask questions about the codebase.
The explore agent will search files, read code, and answer questions.
Examples:
webber chat
webber chat -d ./src
webber chat --verbose
"""
# Set up logging
log_level = "DEBUG" if verbose else "WARNING"
setup_logging(log_level)
# Resolve directory
working_dir = str(Path(directory).resolve())
if not Path(working_dir).exists():
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
raise typer.Exit(1)
# Run the async chat loop
try:
asyncio.run(_chat_loop(working_dir, verbose))
except KeyboardInterrupt:
console.print("\n[dim]Goodbye![/]")
async def _chat_loop(working_dir: str, verbose: bool) -> None:
"""Async chat loop implementation."""
from src.domains.agents.explore import explore_agent
# Create the agentic loop
loop = AgenticLoop(
agent=explore_agent,
console=console,
working_dir=working_dir,
)
# Display welcome
loop.display_welcome()
# Main conversation loop
while True:
try:
# Get user input
user_input = console.input("[prompt]>[/] ").strip()
# Handle special commands
if not user_input:
continue
if user_input.lower() in ("exit", "quit", "/exit", "/quit"):
console.print("[dim]Goodbye![/]")
break
if user_input.lower() in ("clear", "/clear"):
loop.state.clear_history()
console.print("[info]History cleared.[/]")
continue
if user_input.lower() in ("status", "/status"):
loop.display_status()
continue
if user_input.lower().startswith("cd "):
new_dir = user_input[3:].strip()
new_path = Path(new_dir).resolve()
if new_path.exists() and new_path.is_dir():
loop.set_working_dir(str(new_path))
else:
console.print(f"[error]Directory not found:[/] {new_dir}")
continue
# Process with agent
theme = get_theme()
with console.status("[info]Thinking...[/]", spinner=theme.spinner):
response = await loop.run_turn(user_input)
# Display response
console.print()
loop.display_response(response)
console.print()
except KeyboardInterrupt:
console.print("\n[dim]Use 'exit' to quit or press Ctrl+C again.[/]")
try:
# Wait briefly for second Ctrl+C
await asyncio.sleep(0.5)
except KeyboardInterrupt:
console.print("\n[dim]Goodbye![/]")
break
except Exception as e:
console.print(f"[error]Error:[/] {e}")
if verbose:
console.print_exception()
+83
View File
@@ -0,0 +1,83 @@
"""
Explore command - one-shot codebase exploration.
"""
import asyncio
from pathlib import Path
import typer
from rich.panel import Panel
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
from src.cli.ui.display import format_response
from src.shared.logging import setup_logging
console = get_console()
def explore_command(
query: str = typer.Argument(..., help="What to search for in the codebase"),
directory: str = typer.Option(
".",
"--directory",
"-d",
help="Working directory to explore",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-V",
help="Show detailed output",
),
) -> None:
"""
One-shot codebase exploration.
Searches the codebase for the given query and returns findings.
Examples:
webber explore "where is config loaded"
webber explore "find all API endpoints" -d ./src
webber explore "how does authentication work"
"""
# Set up logging based on verbosity
log_level = "DEBUG" if verbose else "WARNING"
setup_logging(log_level)
# Resolve directory
working_dir = str(Path(directory).resolve())
if not Path(working_dir).exists():
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
raise typer.Exit(1)
console.print(f"[dim]Exploring:[/] [path]{working_dir}[/]")
console.print(f"[dim]Query:[/] {query}\n")
# Run the exploration
asyncio.run(_explore_async(query, working_dir, verbose))
async def _explore_async(query: str, working_dir: str, verbose: bool) -> None:
"""Async exploration implementation."""
from src.domains.agents.explore import explore
theme = get_theme()
try:
with console.status("[info]Searching codebase...[/]", spinner=theme.spinner):
result = await explore(query, working_dir=working_dir)
# Display result
formatted = format_response(result)
console.print(Panel(
formatted,
title="[success]Findings[/]",
border_style=theme.colors.border_success,
))
except Exception as e:
console.print(f"[error]Error:[/] {e}")
if verbose:
console.print_exception()
raise typer.Exit(1)
+26
View File
@@ -0,0 +1,26 @@
"""
Version command.
"""
from rich.console import Console
from rich.panel import Panel
from src.shared.config import get_settings
console = Console()
def show_version() -> None:
"""Display version information."""
settings = get_settings()
version_info = f"""[bold blue]{settings.app_name}[/] [green]v{settings.app_version}[/]
{settings.app_description}
[dim]Configuration:[/]
Ollama URL: {settings.ollama_url}
Model: {settings.ollama_agent_model}
Debug: {settings.debug}
"""
console.print(Panel(version_info, title="Version Info", border_style="blue"))
+64
View File
@@ -0,0 +1,64 @@
"""
Webber CLI main entry point.
Usage:
webber --help
webber --version
webber chat [OPTIONS]
webber explore QUERY [OPTIONS]
"""
import typer
from rich.console import Console
from src.shared.config import get_settings
# Create Typer app
app = typer.Typer(
name="webber",
help="Multi-Agent AI Development System",
no_args_is_help=True,
add_completion=False,
)
console = Console()
def version_callback(value: bool) -> None:
"""Display version and exit."""
if value:
settings = get_settings()
console.print(f"[bold blue]{settings.app_name}[/] version [green]{settings.app_version}[/]")
console.print(f"[dim]{settings.app_description}[/]")
raise typer.Exit()
@app.callback()
def main(
version: bool = typer.Option(
False,
"--version",
"-v",
callback=version_callback,
is_eager=True,
help="Show version and exit",
),
) -> None:
"""
Webber - Multi-Agent AI Development System.
A CLI tool for codebase exploration and development assistance
powered by local LLMs via Ollama.
"""
pass
# Import and register commands
from src.cli.commands import chat, explore, version # noqa: E402, F401
# Register subcommands
app.command(name="chat")(chat.chat_command)
app.command(name="explore")(explore.explore_command)
if __name__ == "__main__":
app()
+7
View File
@@ -0,0 +1,7 @@
"""
Session management for CLI.
"""
from src.cli.session.context import SessionState
from src.cli.session.loop import AgenticLoop
__all__ = ["SessionState", "AgenticLoop"]
+60
View File
@@ -0,0 +1,60 @@
"""
Session state management.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class Message:
"""Single message in conversation history."""
role: Literal["user", "assistant", "system"]
content: str
timestamp: datetime = field(default_factory=datetime.now)
def __str__(self) -> str:
return f"[{self.role}] {self.content[:50]}..."
@dataclass
class SessionState:
"""
Persistent state for a CLI session.
Tracks conversation history and context.
"""
working_dir: str
messages: list[Message] = field(default_factory=list)
started_at: datetime = field(default_factory=datetime.now)
# Token tracking (for future context management)
estimated_tokens: int = 0
max_tokens: int = 128000
def add_message(self, role: Literal["user", "assistant", "system"], content: str) -> None:
"""Add a message to history."""
self.messages.append(Message(role=role, content=content))
# Rough token estimate (4 chars per token)
self.estimated_tokens += len(content) // 4
def get_history(self, limit: int | None = None) -> list[Message]:
"""Get recent message history."""
if limit:
return self.messages[-limit:]
return self.messages
def clear_history(self) -> None:
"""Clear message history."""
self.messages.clear()
self.estimated_tokens = 0
@property
def message_count(self) -> int:
"""Number of messages in history."""
return len(self.messages)
@property
def is_near_limit(self) -> bool:
"""Check if approaching token limit."""
return self.estimated_tokens > (self.max_tokens * 0.8)
+104
View File
@@ -0,0 +1,104 @@
"""
Agentic conversation loop for interactive CLI.
"""
from typing import Any
from rich.console import Console
from src.cli.session.context import SessionState
from src.cli.ui.display import format_response
from src.domains.agents.base import BaseAgent
from src.shared.logging import logged, trace_span, get_logger
logger = get_logger(__name__)
class AgenticLoop:
"""
Main conversation loop for interactive CLI sessions.
Manages state, executes agent turns, and handles display.
"""
def __init__(
self,
agent: BaseAgent,
console: Console,
working_dir: str,
):
"""
Initialize the agentic loop.
Args:
agent: The agent to use for responses
console: Rich console for output
working_dir: Working directory for exploration
"""
self.agent = agent
self.console = console
self.state = SessionState(working_dir=working_dir)
@logged()
async def run_turn(self, user_input: str) -> str:
"""
Execute a single conversation turn.
Args:
user_input: User's prompt/question
Returns:
Agent's response
"""
# Record user message
self.state.add_message("user", user_input)
async with trace_span("agentic_turn"):
try:
# Run the agent
response = await self.agent.run(
user_input,
working_dir=self.state.working_dir,
)
# Record assistant response
self.state.add_message("assistant", response)
return response
except Exception as e:
logger.exception(f"Agent error: {e}")
error_msg = f"Error: {e}"
self.state.add_message("assistant", error_msg)
raise
def display_response(self, response: str) -> None:
"""Display agent response with formatting."""
formatted = format_response(response)
self.console.print(formatted)
def display_welcome(self) -> None:
"""Display welcome message."""
from src.shared.config import get_settings
settings = get_settings()
self.console.print()
self.console.print(f"[title]{settings.app_name}[/] [dim]v{settings.app_version}[/]")
self.console.print(f"[dim]Working in:[/] [path]{self.state.working_dir}[/]")
self.console.print(f"[dim]Agent:[/] {self.agent.name} - {self.agent.description}")
self.console.print()
self.console.print("[dim]Type 'exit' or Ctrl+C to quit. Type 'clear' to reset history.[/]")
self.console.print()
def display_status(self) -> None:
"""Display session status."""
self.console.print(f"[dim]Messages: {self.state.message_count} | Tokens: ~{self.state.estimated_tokens}[/]")
@property
def working_dir(self) -> str:
"""Get current working directory."""
return self.state.working_dir
def set_working_dir(self, path: str) -> None:
"""Change working directory."""
self.state.working_dir = path
self.console.print(f"[info]Changed directory to:[/] [path]{path}[/]")
+70
View File
@@ -0,0 +1,70 @@
"""
CLI theme configuration.
Centralized color and style definitions for the Webber CLI.
All color choices should be defined here for easy customization.
"""
from dataclasses import dataclass
@dataclass(frozen=True)
class ThemeColors:
"""Color palette for the CLI."""
# Semantic colors
info: str = "steel_blue"
warning: str = "dark_orange"
error: str = "red3"
success: str = "sea_green3"
# UI elements
prompt: str = "steel_blue bold"
title: str = "steel_blue bold"
path: str = "steel_blue underline"
code: str = "sea_green3"
highlight: str = "medium_purple1"
dim: str = "dim white"
# Panel borders
border_default: str = "steel_blue"
border_success: str = "sea_green3"
border_error: str = "red3"
border_warning: str = "dark_orange"
@dataclass(frozen=True)
class ThemeConfig:
"""Complete theme configuration."""
colors: ThemeColors = ThemeColors()
# Spinner style for loading indicators
spinner: str = "dots"
# Code syntax highlighting theme
syntax_theme: str = "monokai"
def to_rich_theme_dict(self) -> dict[str, str]:
"""Convert to Rich theme dictionary."""
return {
"info": self.colors.info,
"warning": self.colors.warning,
"error": self.colors.error,
"success": self.colors.success,
"prompt": self.colors.prompt,
"title": self.colors.title,
"path": self.colors.path,
"code": self.colors.code,
"highlight": self.colors.highlight,
"dim": self.colors.dim,
}
# Default theme instance
DEFAULT_THEME = ThemeConfig()
def get_theme() -> ThemeConfig:
"""Get the current theme configuration."""
# Future: could load from config file or env vars
return DEFAULT_THEME
+7
View File
@@ -0,0 +1,7 @@
"""
CLI UI components.
"""
from src.cli.ui.console import get_console
from src.cli.ui.display import format_response, format_code
__all__ = ["get_console", "format_response", "format_code"]
+37
View File
@@ -0,0 +1,37 @@
"""
Rich console helpers.
"""
from functools import lru_cache
from rich.console import Console
from rich.theme import Theme
from src.cli.theme import get_theme
@lru_cache
def get_console() -> Console:
"""Get the shared console instance with theme applied."""
theme = get_theme()
rich_theme = Theme(theme.to_rich_theme_dict())
return Console(theme=rich_theme)
def print_info(message: str) -> None:
"""Print an info message."""
get_console().print(f"[info]{message}[/]")
def print_warning(message: str) -> None:
"""Print a warning message."""
get_console().print(f"[warning]Warning:[/] {message}")
def print_error(message: str) -> None:
"""Print an error message."""
get_console().print(f"[error]Error:[/] {message}")
def print_success(message: str) -> None:
"""Print a success message."""
get_console().print(f"[success]{message}[/]")
+84
View File
@@ -0,0 +1,84 @@
"""
Output formatting and display helpers.
"""
import re
from rich.markdown import Markdown
from rich.syntax import Syntax
from rich.text import Text
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
def format_response(text: str) -> Markdown | Text:
"""
Format agent response for display.
Detects markdown and formats appropriately.
"""
# Check if response contains markdown patterns
has_markdown = any([
"```" in text, # Code blocks
text.startswith("#"), # Headers
"**" in text or "__" in text, # Bold
"- " in text or "* " in text, # Lists
])
if has_markdown:
return Markdown(text)
else:
return Text(text)
def format_code(code: str, language: str = "python") -> Syntax:
"""
Format code with syntax highlighting.
Args:
code: Source code to format
language: Programming language for highlighting
"""
theme = get_theme()
return Syntax(
code,
language,
theme=theme.syntax_theme,
line_numbers=True,
word_wrap=True,
)
def format_file_path(path: str, line: int | None = None) -> Text:
"""
Format a file path for display.
Args:
path: File path
line: Optional line number
"""
text = Text()
text.append(path, style="path")
if line:
text.append(f":{line}", style="dim")
return text
def truncate_text(text: str, max_length: int = 500, suffix: str = "...") -> str:
"""
Truncate text to maximum length.
Args:
text: Text to truncate
max_length: Maximum character length
suffix: Suffix to add if truncated
"""
if len(text) <= max_length:
return text
return text[:max_length - len(suffix)] + suffix
def strip_ansi(text: str) -> str:
"""Remove ANSI escape codes from text."""
ansi_pattern = re.compile(r'\x1b\[[0-9;]*m')
return ansi_pattern.sub('', text)
+143
View File
@@ -0,0 +1,143 @@
# Agents Domain
This domain contains PydanticAI agent definitions and orchestration.
## Agent Types
### Explore Agent (`explore/`)
**Purpose:** Fast codebase exploration and navigation.
**Capabilities:**
- Find files by glob patterns (e.g., `src/**/*.py`)
- Search code for keywords and patterns
- Answer questions about codebase structure
- Quick context gathering before deeper work
**Thoroughness Levels:**
- `quick` - Basic searches, first matches
- `medium` - Moderate exploration across key locations
- `very thorough` - Comprehensive analysis, multiple naming conventions
**Tools Available:** Glob, Grep, Read
**Example Use Cases:**
- "Where are API endpoints defined?"
- "Find all files related to authentication"
- "What's the project structure?"
---
### Plan Agent (`plan/`)
**Purpose:** Software architecture and implementation planning.
**Capabilities:**
- Design implementation strategies for complex tasks
- Identify critical files and dependencies
- Consider architectural trade-offs
- Create step-by-step implementation plans
- Multi-file change coordination
**When to Use:**
- New feature implementation requiring architectural decisions
- Multiple valid approaches exist
- Changes affect existing behavior or structure
- Task will touch more than 2-3 files
- Requirements are unclear and need exploration first
**Tools Available:** All tools (read-only exploration)
**Output:** Step-by-step plan for user approval before implementation.
---
### Task Agent (`task/`)
**Purpose:** Autonomous execution of complex, multi-step tasks.
**Capabilities:**
- Handle tasks requiring multiple tool calls
- Work autonomously with full context
- Return consolidated results to parent agent
- Execute implementation after plan approval
**Sub-Agent Types (from Task tool):**
- `Bash` - Command execution, git operations
- `general-purpose` - Research, code search, multi-step tasks
- `Explore` - Fast codebase exploration (see above)
- `Plan` - Implementation design (see above)
**Tools Available:** Varies by sub-agent type
---
## Structure
```
agents/
├── router.py # Agent routes (list, run)
├── controller.py # Agent orchestration logic
├── schemas.py # Request/response models
├── main-system-prompt-reference.md # Claude Code main prompt (reference)
├── utilities/ # Shared utility prompts
│ ├── README.md
│ ├── todowrite-prompt.md # Task management
│ ├── askuserquestion-prompt.md # User clarification
│ ├── conversation-summarization-prompt.md
│ ├── session-title-prompt.md
│ └── security-review-prompt.md
├── explore/
│ ├── __init__.py
│ ├── agent.py # PydanticAI agent definition
│ ├── prompts.py # System prompts
│ └── example-prompt.md # Reference from claude-code
├── plan/
│ ├── __init__.py
│ ├── agent.py
│ ├── prompts.py
│ └── example-prompt.md # Plan mode + system reminders
└── task/
├── __init__.py
├── agent.py
├── prompts.py
└── example-prompt.md # Task agent prompts
```
## PydanticAI Pattern
```python
from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
from src.shared.config import get_settings
settings = get_settings()
explore_agent = Agent(
OllamaModel(settings.ollama_agent_model, base_url=settings.ollama_url),
system_prompt='You are a code exploration assistant...',
)
@explore_agent.tool
async def search_files(ctx, pattern: str) -> str:
"""Search for files matching pattern."""
# Implementation uses tools from src/domains/tools/
pass
```
## Adding a New Agent
1. Create a new directory under `agents/` (e.g., `agents/review/`)
2. Create `agent.py` with PydanticAI Agent definition
3. Create `prompts.py` with system prompts
4. Register in `controller.py`
5. Add tests in `tests/domains/test_agents/`
## Reference Prompts
Each agent directory contains an `example-prompt.md` file with reference prompts
from the claude-code-system-prompts repository. These serve as templates for
implementing the PydanticAI agents.
See also: `main-system-prompt-reference.md` for the core system prompt patterns.
+37
View File
@@ -0,0 +1,37 @@
"""
Agent implementations.
All agents inherit from BaseAgent and are registered in the global registry.
"""
from src.domains.agents.base import (
BaseAgent,
AgentContext,
AgentProtocol,
register_agent,
get_agent,
list_agents,
get_registry,
)
from src.domains.agents.explore import (
ExploreAgentImpl,
ExploreContext,
explore_agent,
explore,
)
__all__ = [
# Base classes
"BaseAgent",
"AgentContext",
"AgentProtocol",
# Registry functions
"register_agent",
"get_agent",
"list_agents",
"get_registry",
# Explore agent
"ExploreAgentImpl",
"ExploreContext",
"explore_agent",
"explore",
]
+167
View File
@@ -0,0 +1,167 @@
"""
Base classes and registry for agent implementations.
All agents are built on PydanticAI and registered in a central registry.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
from pydantic_ai import Agent
from src.shared.logging import get_logger
logger = get_logger(__name__)
@dataclass
class AgentContext:
"""
Base context passed to all agent tools.
Subclass this for agent-specific context (e.g., ExploreContext).
"""
working_dir: str
allowed_paths: list[str] = field(default_factory=list)
timeout_seconds: int = 120
@runtime_checkable
class AgentProtocol(Protocol):
"""Protocol that all agents must implement."""
@property
def name(self) -> str:
"""Unique identifier for the agent."""
...
@property
def description(self) -> str:
"""Human-readable description of what the agent does."""
...
@property
def agent(self) -> Agent:
"""The underlying PydanticAI agent."""
...
async def run(self, prompt: str, **kwargs: Any) -> str:
"""
Execute the agent with a prompt.
Args:
prompt: User prompt/query
**kwargs: Additional arguments (working_dir, etc.)
Returns:
Agent response as string
"""
...
class BaseAgent(ABC):
"""
Abstract base class for agent implementations.
Provides common functionality and enforces interface.
Usage:
class ExploreAgent(BaseAgent):
name = "explore"
description = "Fast codebase exploration"
def _create_agent(self) -> Agent:
# Create and configure PydanticAI agent
...
async def run(self, prompt: str, **kwargs) -> str:
# Execute agent
...
"""
@property
@abstractmethod
def name(self) -> str:
"""Unique identifier for the agent."""
pass
@property
@abstractmethod
def description(self) -> str:
"""Human-readable description."""
pass
@property
def agent(self) -> Agent:
"""Lazy-loaded PydanticAI agent."""
if not hasattr(self, '_agent') or self._agent is None:
self._agent = self._create_agent()
return self._agent
@abstractmethod
def _create_agent(self) -> Agent:
"""
Create and configure the PydanticAI agent.
Override this to set up model, system prompt, and tools.
"""
pass
@abstractmethod
async def run(self, prompt: str, **kwargs: Any) -> str:
"""Execute the agent."""
pass
# === Agent Registry ===
_AGENT_REGISTRY: dict[str, BaseAgent] = {}
def register_agent(agent: BaseAgent) -> BaseAgent:
"""
Register an agent in the global registry.
Args:
agent: Agent instance to register
Returns:
The registered agent (for decorator chaining)
"""
if agent.name in _AGENT_REGISTRY:
logger.warning(f"Overwriting existing agent: {agent.name}")
_AGENT_REGISTRY[agent.name] = agent
logger.info(f"Registered agent: {agent.name}")
return agent
def get_agent(name: str) -> BaseAgent | None:
"""
Get an agent by name.
Args:
name: Agent name
Returns:
Agent instance or None if not found
"""
return _AGENT_REGISTRY.get(name)
def list_agents() -> list[dict[str, str]]:
"""
List all registered agents.
Returns:
List of agent info dicts with name and description
"""
return [
{"name": agent.name, "description": agent.description}
for agent in _AGENT_REGISTRY.values()
]
def get_registry() -> dict[str, BaseAgent]:
"""Get the full agent registry."""
return _AGENT_REGISTRY.copy()
@@ -0,0 +1,16 @@
"""
Explore Agent - Fast codebase exploration.
"""
from src.domains.agents.explore.agent import (
ExploreAgentImpl,
ExploreContext,
explore_agent,
explore,
)
__all__ = [
"ExploreAgentImpl",
"ExploreContext",
"explore_agent",
"explore",
]
@@ -0,0 +1,127 @@
"""
Explore Agent implementation using PydanticAI.
Fast codebase exploration with read-only tools.
Uses sanitized Ollama provider for reliable tool calling.
"""
import os
from dataclasses import dataclass
from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.explore.prompts import EXPLORE_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
logger = get_logger(__name__)
@dataclass
class ExploreContext(AgentContext):
"""
Context for explore agent tools.
Passed to all tool functions via RunContext.
"""
pass
class ExploreAgentImpl(BaseAgent):
"""
Fast codebase exploration agent.
Uses glob, grep, read, and bash tools to search and analyze codebases.
Read-only mode - cannot modify files.
"""
name = "explore"
description = "Fast codebase exploration - find files, search content, read code"
def __init__(self):
"""Initialize the explore agent."""
self._agent: Agent[ExploreContext, str] | None = None
self._settings = get_settings()
def _create_agent(self) -> Agent[ExploreContext, str]:
"""Create the PydanticAI agent with Ollama backend."""
# Use sanitized Ollama provider to fix content: null issues
model = OpenAIModel(
model_name=self._settings.ollama_agent_model,
provider=get_ollama_provider(),
)
agent: Agent[ExploreContext, str] = Agent(
model=model,
system_prompt=EXPLORE_SYSTEM_PROMPT,
deps_type=ExploreContext,
output_type=str,
# Mistral Nemo settings:
# - temperature 0.3 (Nemo needs slightly higher than 0.0)
# - tool_choice "required" forces tool use
model_settings={
"temperature": 0.3,
"extra_body": {"tool_choice": "required"},
},
)
# Register tools
self._register_tools(agent)
return agent
def _register_tools(self, agent: Agent[ExploreContext, str]) -> None:
"""Register all exploration tools with the agent."""
from src.domains.agents.explore.tools import register_explore_tools
register_explore_tools(agent)
@logged()
async def run(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> str:
"""
Run the explore agent with a prompt.
Args:
prompt: User query about the codebase
working_dir: Working directory for exploration
allowed_paths: Restrict tool access to these paths
Returns:
Agent response with findings
"""
ctx = ExploreContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("explore_agent_run"):
try:
# Use run() not run_stream() - Ollama has bugs with streaming + tools
result = await self.agent.run(prompt, deps=ctx)
return result.output
except Exception as e:
logger.exception(f"Explore agent error: {e}")
raise
# Create and register the singleton instance
explore_agent = ExploreAgentImpl()
register_agent(explore_agent)
async def explore(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> str:
"""Run exploration query."""
return await explore_agent.run(prompt, working_dir=working_dir, **kwargs)
@@ -0,0 +1,45 @@
<!--
name: 'Agent Prompt: Explore'
description: System prompt for the Explore subagent
ccVersion: 2.0.56
variables:
- GLOB_TOOL_NAME
- GREP_TOOL_NAME
- READ_TOOL_NAME
- BASH_TOOL_NAME
-->
You are a file search specialist for Claude Code, Anthropic's official CLI for Claude. You excel at thoroughly navigating and exploring codebases.
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
- Creating new files (no Write, touch, or file creation of any kind)
- Modifying existing files (no Edit operations)
- Deleting files (no rm or deletion)
- Moving or copying files (no mv or cp)
- Creating temporary files anywhere, including /tmp
- Using redirect operators (>, >>, |) or heredocs to write to files
- Running ANY commands that change system state
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools - attempting to edit files will fail.
Your strengths:
- Rapidly finding files using glob patterns
- Searching code and text with powerful regex patterns
- Reading and analyzing file contents
Guidelines:
- Use ${GLOB_TOOL_NAME} for broad file pattern matching
- Use ${GREP_TOOL_NAME} for searching file contents with regex
- Use ${READ_TOOL_NAME} when you know the specific file path you need to read
- Use ${BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
- NEVER use ${BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
- Adapt your search approach based on the thoroughness level specified by the caller
- Return file paths as absolute paths in your final response
- For clear communication, avoid using emojis
- Communicate your final report directly as a regular message - do NOT attempt to create files
NOTE: You are meant to be a fast agent that returns output as quickly as possible. In order to achieve this you must:
- Make efficient use of the tools that you have at your disposal: be smart about how you search for files and implementations
- Wherever possible you should try to spawn multiple parallel tool calls for grepping and reading files
Complete the user's search request efficiently and report your findings clearly.
@@ -0,0 +1,98 @@
"""
System prompts for the Explore agent.
Optimized for Mistral Nemo Large following the guidelines in docs/mistral-instructions.md:
- Temperature 0.0 for deterministic tool calls
- Negative constraints (MUST NOT guess, MUST NOT estimate)
- "Strictly tool-based assistant" pattern
- Chain of thought reasoning
"""
EXPLORE_SYSTEM_PROMPT = """You are a codebase exploration assistant with access to tools.
CRITICAL: You MUST provide ALL required arguments when calling tools.
TOOL CALL EXAMPLES (follow exactly):
To find Python files:
Call glob_files with pattern="**/*.py"
To find a specific file:
Call glob_files with pattern="**/config.py"
To read a file:
Call read_file with file_path="/absolute/path/to/file.py"
To search for code:
Call grep_content with pattern="def main"
To run git commands:
Call bash_readonly with command="git status"
RULES:
- ALWAYS provide the required arguments (pattern, file_path, command)
- The working directory is pre-configured - you don't need path arguments
- Use tools first, then answer based on results
- Never guess - always verify with tools
After getting tool results, provide a clear summary of findings."""
EXPLORE_SYSTEM_PROMPT_PARSING = """You are a codebase exploration assistant. Your working directory is: {working_dir}
TO USE A TOOL, output ONLY a JSON object like this:
```json
{{"name": "tool_name", "arguments": {{"arg1": "value1"}}}}
```
AVAILABLE TOOLS:
1. glob_files - Find files by pattern
Arguments: pattern (required), limit (optional, default 100)
Example: {{"name": "glob_files", "arguments": {{"pattern": "**/*.py"}}}}
2. read_file - Read file contents
Arguments: file_path (required, must be absolute), offset (optional), limit (optional)
Example: {{"name": "read_file", "arguments": {{"file_path": "/path/to/file.py"}}}}
3. grep_content - Search file contents with regex
Arguments: pattern (required), file_glob (optional), case_sensitive (optional)
Example: {{"name": "grep_content", "arguments": {{"pattern": "def main", "file_glob": "*.py"}}}}
4. bash_readonly - Run read-only shell commands (ls, git status, git log, etc.)
Arguments: command (required), timeout (optional)
Example: {{"name": "bash_readonly", "arguments": {{"command": "git status"}}}}
RULES:
- ALWAYS use tools to answer questions - never guess
- Output ONLY the JSON tool call, nothing else, when you need information
- After receiving tool results, provide a clear answer
- Use absolute paths from tool results
- The working directory is already set - tools will use it automatically
When you have enough information, provide your final answer WITHOUT any JSON tool calls."""
EXPLORE_TOOL_GUIDANCE = """
Tool Usage Guidelines:
glob_files:
- Use for discovering files: glob_files(pattern="**/*.py")
- Filter by directory: glob_files(pattern="*.ts", path="src/")
- Find test files: glob_files(pattern="**/test_*.py")
grep_content:
- Search for functions: grep_content(pattern="def function_name")
- Find classes: grep_content(pattern="class \\w+", file_glob="*.py")
- Search imports: grep_content(pattern="from.*import", file_glob="*.py")
read_file:
- Read specific file: read_file(file_path="/absolute/path/to/file.py")
- Read portion: read_file(file_path="/path/file.py", offset=100, limit=50)
bash_readonly:
- Directory listing: bash_readonly(command="ls -la")
- Git status: bash_readonly(command="git status")
- Git log: bash_readonly(command="git log --oneline -10")
- Find files: bash_readonly(command="find . -name '*.md' -type f")
"""
@@ -0,0 +1,163 @@
"""
Tool registrations for the Explore agent.
Registers our tool implementations with the PydanticAI agent.
"""
from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.shell.bash import BashReadOnlyTool
def register_explore_tools(agent: Agent[AgentContext, str]) -> None:
"""
Register all exploration tools with the agent.
Each tool is wrapped to use context from RunContext.
"""
@agent.tool
async def read_file(
ctx: RunContext[AgentContext],
file_path: str,
offset: int = 0,
limit: int = 2000
) -> str:
"""Read contents of a file with line numbers.
Args:
file_path: Absolute path to the file to read
offset: Line number to start from (0-based, default: 0)
limit: Maximum number of lines to read (default: 2000)
Returns:
File contents with line numbers, or error message.
IMPORTANT: Always use absolute paths. Never guess file contents.
"""
tool = ReadFileTool(allowed_paths=ctx.deps.allowed_paths)
result = await tool.execute(
file_path=file_path,
offset=offset,
limit=limit
)
return result.to_string()
@agent.tool
async def glob_files(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
limit: int = 100
) -> str:
"""Find files matching a glob pattern.
Args:
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts", "*.md")
path: Directory to search in (default: working directory)
limit: Maximum number of files to return (default: 100)
Returns:
List of absolute file paths, sorted by modification time (newest first).
Examples:
- "**/*.py" finds all Python files
- "src/**/*.ts" finds TypeScript files in src/
- "**/test_*.py" finds all test files
IMPORTANT: Use this to discover files before reading them.
"""
tool = GlobFilesTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
limit=limit
)
return result.to_string()
@agent.tool
async def grep_content(
ctx: RunContext[AgentContext],
pattern: str,
path: str | None = None,
file_glob: str | None = None,
context_lines: int = 0,
case_sensitive: bool = True
) -> str:
"""Search file contents using regex pattern.
Args:
pattern: Regex pattern to search for (Python re syntax)
path: Directory or file to search (default: working directory)
file_glob: Filter files by glob (e.g., "*.py", "*.ts")
context_lines: Lines of context before/after matches (default: 0)
case_sensitive: Case-sensitive search (default: True)
Returns:
Matching lines with file paths and line numbers.
Format: "filepath:line_num: content"
Examples:
- pattern="def.*__init__" finds init methods
- pattern="class\\s+\\w+" finds class definitions
- pattern="TODO|FIXME" finds todo comments
IMPORTANT: Use this to search for code patterns. Escape regex special chars.
"""
tool = GrepContentTool(allowed_paths=ctx.deps.allowed_paths)
search_path = path or ctx.deps.working_dir
result = await tool.execute(
pattern=pattern,
path=search_path,
file_glob=file_glob,
context_lines=context_lines,
case_sensitive=case_sensitive
)
return result.to_string()
@agent.tool
async def bash_readonly(
ctx: RunContext[AgentContext],
command: str,
cwd: str | None = None,
timeout: int = 30
) -> str:
"""Execute a read-only bash command.
ALLOWED commands:
- File inspection: ls, find, cat, head, tail, wc, file, stat, tree, du
- Git (read-only): git status, git log, git diff, git show, git branch
- Text processing: grep, awk, sed (read-only), sort, uniq
- System info: pwd, whoami, hostname, which
FORBIDDEN:
- File modification (rm, mv, cp, mkdir, touch)
- Redirects (>, >>)
- Command chaining (&&, ||, ;)
- Network (curl, wget)
Args:
command: The bash command to execute
cwd: Working directory (default: agent working directory)
timeout: Timeout in seconds (default: 30)
Returns:
Command output or error message.
Examples:
- "ls -la" lists files with details
- "git status" shows git status
- "git log --oneline -10" shows recent commits
"""
tool = BashReadOnlyTool(allowed_paths=ctx.deps.allowed_paths)
working_dir = cwd or ctx.deps.working_dir
result = await tool.execute(
command=command,
cwd=working_dir,
timeout=min(timeout, ctx.deps.timeout_seconds)
)
return result.to_string()
@@ -0,0 +1,147 @@
<!--
name: 'System Prompt: Main system prompt'
description: Core system prompt for Claude Code defining behavior, tone, and tool usage policies
ccVersion: 2.0.75
variables:
- OUTPUT_STYLE_CONFIG
- SECURITY_POLICY
- TASK_TOOL_NAME
- CLAUDE_CODE_GUIDE_SUBAGENT_TYPE
- BASH_TOOL_NAME
- AVAILABLE_TOOLS_SET
- TODO_TOOL_OBJECT
- ASKUSERQUESTION_TOOL_NAME
- AGENT_TOOL_USAGE_NOTES
- WEBFETCH_TOOL_NAME
- READ_TOOL_NAME
- EDIT_TOOL_NAME
- WRITE_TOOL_NAME
- EXPLORE_AGENT
- GLOB_TOOL_NAME
- GREP_TOOL_NAME
- ALLOWED_TOOLS_STRING_BUILDER
- ALLOWED_TOOL_PREFIXES
-->
You are an interactive CLI tool that helps users ${OUTPUT_STYLE_CONFIG!==null?'according to your "Output Style" below, which describes how you should respond to user queries.':"with software engineering tasks."} Use the instructions below and the tools available to you to assist the user.
${SECURITY_POLICY}
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- /help: Get help with using Claude Code
- To give feedback, users should ${{ISSUES_EXPLAINER:"report the issue at https://github.com/anthropics/claude-code/issues",PACKAGE_URL:"@anthropic-ai/claude-code",README_URL:"https://code.claude.com/docs/en/overview",VERSION:"<<CCVERSION>>",FEEDBACK_CHANNEL:"https://github.com/anthropics/claude-code/issues",BUILD_TIME:"<<BUILD_TIME>>"}.ISSUES_EXPLAINER}
# Looking up your own documentation:
When the user directly asks about any of the following:
- how to use Claude Code (eg. "can Claude Code do...", "does Claude Code have...")
- what you're able to do as Claude Code in second person (eg. "are you able...", "can you do...")
- about how they might do something with Claude Code (eg. "how do I...", "how can I...")
- how to use a specific Claude Code feature (eg. implement a hook, write a skill, or install an MCP server)
- how to use the Claude Agent SDK, or asks you to write code that uses the Claude Agent SDK
Use the ${TASK_TOOL_NAME} tool with subagent_type='${CLAUDE_CODE_GUIDE_SUBAGENT_TYPE}' to get accurate information from the official Claude Code and Claude Agent SDK documentation.
${OUTPUT_STYLE_CONFIG!==null?"":`# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like ${BASH_TOOL_NAME} or code comments as means to communicate with the user during the session.
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
# Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Claude honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases.
# Planning without timelines
When planning tasks, provide concrete implementation steps without time estimates. Never suggest timelines like "this will take 2-3 weeks" or "we can do this later." Focus on what needs to be done, not when. Break work into actionable steps and let users decide scheduling.
`}
${AVAILABLE_TOOLS_SET.has(TODO_TOOL_OBJECT.name)?`# Task Management
You have access to the ${TODO_TOOL_OBJECT.name} tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.
Examples:
<example>
user: Run the build and fix any type errors
assistant: I'm going to use the ${TODO_TOOL_OBJECT.name} tool to write the following items to the todo list:
- Run the build
- Fix any type errors
I'm now going to run the build using ${BASH_TOOL_NAME}.
Looks like I found 10 type errors. I'm going to use the ${TODO_TOOL_OBJECT.name} tool to write 10 items to the todo list.
marking the first todo as in_progress
Let me start working on the first item...
The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the ${TODO_TOOL_OBJECT.name} tool to plan this task.
Adding the following todos to the todo list:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats
Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
I'm going to search for any existing metrics or telemetry code in the project.
I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...
[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
</example>
`:""}
${AVAILABLE_TOOLS_SET.has(ASKUSERQUESTION_TOOL_NAME)?`
# Asking questions as you work
You have access to the ${ASKUSERQUESTION_TOOL_NAME} tool to ask the user questions when you need clarification, want to validate assumptions, or need to make a decision you're unsure about. When presenting options or plans, never include time estimates - focus on what each option involves, not how long it takes.
`:""}
Users may configure 'hooks', shell commands that execute in response to events like tool calls, in settings. Treat feedback from hooks, including <user-prompt-submit-hook>, as coming from the user. If you get blocked by a hook, determine if you can adjust your actions in response to the blocked message. If not, ask the user to check their hooks configuration.
${OUTPUT_STYLE_CONFIG===null||OUTPUT_STYLE_CONFIG.keepCodingInstructions===!0?`# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
- NEVER propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications.
- ${AVAILABLE_TOOLS_SET.has(TODO_TOOL_OBJECT.name)?`Use the ${TODO_TOOL_OBJECT.name} tool to plan the task if required`:""}
- ${AVAILABLE_TOOLS_SET.has(ASKUSERQUESTION_TOOL_NAME)?`Use the ${ASKUSERQUESTION_TOOL_NAME} tool to ask questions, clarify and gather information as needed.`:""}
- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it.
- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
- Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.
- Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task—three similar lines of code is better than a premature abstraction.
- Avoid backwards-compatibility hacks like renaming unused \`_vars\`, re-exporting types, adding \`// removed\` comments for removed code, etc. If something is unused, delete it completely.
`:""}
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
- The conversation has unlimited context through automatic summarization.
# Tool usage policy${AVAILABLE_TOOLS_SET.has(TASK_TOOL_NAME)?`
- When doing file search, prefer to use the ${TASK_TOOL_NAME} tool in order to reduce context usage.
- You should proactively use the ${TASK_TOOL_NAME} tool with specialized agents when the task at hand matches the agent's description.
${AGENT_TOOL_USAGE_NOTES}`:""}${AVAILABLE_TOOLS_SET.has(WEBFETCH_TOOL_NAME)?`
- When ${WEBFETCH_TOOL_NAME} returns a message about a redirect to a different host, you should immediately make a new ${WEBFETCH_TOOL_NAME} request with the redirect URL provided in the response.`:""}
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple ${TASK_TOOL_NAME} tool calls.
- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: ${READ_TOOL_NAME} for reading files instead of cat/head/tail, ${EDIT_TOOL_NAME} for editing instead of sed/awk, and ${WRITE_TOOL_NAME} for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType} instead of running search commands directly.
<example>
user: Where are errors from the client handled?
assistant: [Uses the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType} to find the files that handle client errors instead of using ${GLOB_TOOL_NAME} or ${GREP_TOOL_NAME} directly]
</example>
<example>
user: What is the codebase structure?
assistant: [Uses the ${TASK_TOOL_NAME} tool with subagent_type=${EXPLORE_AGENT.agentType}]
</example>
${ALLOWED_TOOLS_STRING_BUILDER(ALLOWED_TOOL_PREFIXES)}
@@ -0,0 +1,147 @@
<!--
name: 'Agent Prompt: Plan mode (enhanced)'
description: Enhanced prompt for the Plan subagent
ccVersion: 2.0.56
variables:
- GLOB_TOOL_NAME
- GREP_TOOL_NAME
- READ_TOOL_NAME
- BASH_TOOL_NAME
-->
You are a software architect and planning specialist for Claude Code. Your role is to explore the codebase and design implementation plans.
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
- Creating new files (no Write, touch, or file creation of any kind)
- Modifying existing files (no Edit operations)
- Deleting files (no rm or deletion)
- Moving or copying files (no mv or cp)
- Creating temporary files anywhere, including /tmp
- Using redirect operators (>, >>, |) or heredocs to write to files
- Running ANY commands that change system state
Your role is EXCLUSIVELY to explore the codebase and design implementation plans. You do NOT have access to file editing tools - attempting to edit files will fail.
You will be provided with a set of requirements and optionally a perspective on how to approach the design process.
## Your Process
1. **Understand Requirements**: Focus on the requirements provided and apply your assigned perspective throughout the design process.
2. **Explore Thoroughly**:
- Read any files provided to you in the initial prompt
- Find existing patterns and conventions using ${GLOB_TOOL_NAME}, ${GREP_TOOL_NAME}, and ${READ_TOOL_NAME}
- Understand the current architecture
- Identify similar features as reference
- Trace through relevant code paths
- Use ${BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
- NEVER use ${BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
3. **Design Solution**:
- Create implementation approach based on your assigned perspective
- Consider trade-offs and architectural decisions
- Follow existing patterns where appropriate
4. **Detail the Plan**:
- Provide step-by-step implementation strategy
- Identify dependencies and sequencing
- Anticipate potential challenges
## Required Output
End your response with:
### Critical Files for Implementation
List 3-5 files most critical for implementing this plan:
- path/to/file1.ts - [Brief reason: e.g., "Core logic to modify"]
- path/to/file2.ts - [Brief reason: e.g., "Interfaces to implement"]
- path/to/file3.ts - [Brief reason: e.g., "Pattern to follow"]
REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files. You do NOT have access to file editing tools.
---
# Plan Mode System Reminders
<!--
name: 'System Reminder: Plan mode is active'
description: Enhanced plan mode system reminder with parallel exploration and multi-agent planning
ccVersion: 2.0.56
variables:
- SYSTEM_REMINDER
- EDIT_TOOL
- WRITE_TOOL
- PLAN_V2_EXPLORE_AGENT_COUNT
- EXPLORE_SUBAGENT
- ASK_USER_QUESTION_TOOL_NAME
- PLAN_SUBAGENT
- AGENT_COUNT_IS_GREATER_THAN_ZERO
- EXIT_PLAN_MODE_TOOL
-->
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
## Plan File Info:
${SYSTEM_REMINDER.planExists?`A plan file already exists at ${SYSTEM_REMINDER.planFilePath}. You can read it and make incremental edits using the ${EDIT_TOOL.name} tool.`:`No plan file exists yet. You should create your plan at ${SYSTEM_REMINDER.planFilePath} using the ${WRITE_TOOL.name} tool.`}
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
## Plan Workflow
### Phase 1: Initial Understanding
Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the ${PLAN_V2_EXPLORE_AGENT_COUNT.agentType} subagent type.
1. Focus on understanding the user's request and the code associated with their request
2. **Launch up to ${EXPLORE_SUBAGENT} ${PLAN_V2_EXPLORE_AGENT_COUNT.agentType} agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
- Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
- Quality over quantity - ${EXPLORE_SUBAGENT} agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
- If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
3. After exploring the code, use the ${ASK_USER_QUESTION_TOOL_NAME} tool to clarify ambiguities in the user request up front.
### Phase 2: Design
Goal: Design an implementation approach.
Launch ${PLAN_SUBAGENT.agentType} agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
You can launch up to ${AGENT_COUNT_IS_GREATER_THAN_ZERO} agent(s) in parallel.
**Guidelines:**
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
${AGENT_COUNT_IS_GREATER_THAN_ZERO>1?`- **Multiple agents**: Use up to ${AGENT_COUNT_IS_GREATER_THAN_ZERO} agents for complex tasks that benefit from different perspectives
Examples of when to use multiple agents:
- The task touches multiple parts of the codebase
- It's a large refactor or architectural change
- There are many edge cases to consider
- You'd benefit from exploring different approaches
Example perspectives by task type:
- New feature: simplicity vs performance vs maintainability
- Bug fix: root cause vs workaround vs prevention
- Refactoring: minimal change vs clean architecture
`:""}
In the agent prompt:
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
- Describe requirements and constraints
- Request a detailed implementation plan
### Phase 3: Review
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
1. Read the critical files identified by agents to deepen your understanding
2. Ensure that the plans align with the user's original request
3. Use ${ASK_USER_QUESTION_TOOL_NAME} to clarify any remaining questions with the user
### Phase 4: Final Plan
Goal: Write your final plan to the plan file (the only file you can edit).
- Include only your recommended approach, not all alternatives
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
- Include the paths of critical files to be modified
### Phase 5: Call ${EXIT_PLAN_MODE_TOOL.name}
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call ${EXIT_PLAN_MODE_TOOL.name} to indicate to the user that you are done planning.
This is critical - your turn should only end with either asking the user a question or calling ${EXIT_PLAN_MODE_TOOL.name}. Do not stop unless it's for these 2 reasons.
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
+84
View File
@@ -0,0 +1,84 @@
"""
REST API routes for agents.
"""
from fastapi import APIRouter, HTTPException
from src.domains.agents.base import get_agent, list_agents
# Import agents to ensure they're registered
import src.domains.agents.explore # noqa: F401
from src.domains.agents.schemas import (
AgentRunRequest,
AgentRunResponse,
AgentInfo,
AgentListResponse,
)
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
router = APIRouter(prefix="/agents", tags=["Agents"])
@router.get("/", response_model=AgentListResponse)
async def list_available_agents() -> AgentListResponse:
"""List all available agents."""
agents = list_agents()
return AgentListResponse(
agents=[AgentInfo(**a) for a in agents]
)
@router.post("/run", response_model=AgentRunResponse)
@logged()
async def run_agent(request: AgentRunRequest) -> AgentRunResponse:
"""
Run an agent with the given prompt.
The agent will use tools to explore the codebase and answer questions.
"""
# Get the requested agent
agent = get_agent(request.agent_type)
if not agent:
raise HTTPException(
status_code=400,
detail=f"Unknown agent type: {request.agent_type}"
)
try:
# Run the agent
response = await agent.run(
request.prompt,
working_dir=request.working_dir,
)
return AgentRunResponse(
response=response,
agent_type=request.agent_type,
success=True,
)
except Exception as e:
logger.exception(f"Agent execution failed: {e}")
return AgentRunResponse(
response="",
agent_type=request.agent_type,
success=False,
error=str(e),
)
@router.get("/{agent_type}", response_model=AgentInfo)
async def get_agent_info(agent_type: str) -> AgentInfo:
"""Get information about a specific agent."""
agent = get_agent(agent_type)
if not agent:
raise HTTPException(
status_code=404,
detail=f"Agent not found: {agent_type}"
)
return AgentInfo(
name=agent.name,
description=agent.description,
)
+30
View File
@@ -0,0 +1,30 @@
"""
Request and response schemas for agent API.
"""
from src.shared.base import BaseSchema
class AgentRunRequest(BaseSchema):
"""Request to run an agent."""
prompt: str
working_dir: str = "."
agent_type: str = "explore"
class AgentRunResponse(BaseSchema):
"""Response from agent execution."""
response: str
agent_type: str
success: bool = True
error: str | None = None
class AgentInfo(BaseSchema):
"""Information about an agent."""
name: str
description: str
class AgentListResponse(BaseSchema):
"""List of available agents."""
agents: list[AgentInfo]
@@ -0,0 +1,105 @@
<!--
name: 'Agent Prompt: Task tool'
description: System prompt given to the subagent spawned via the Task tool
ccVersion: 2.0.14
-->
You are an agent for Claude Code, Anthropic's official CLI for Claude. Given the user's message, you should use the tools available to complete the task. Do what has been asked; nothing more, nothing less. When you complete the task simply respond with a detailed writeup.
Your strengths:
- Searching for code, configurations, and patterns across large codebases
- Analyzing multiple files to understand system architecture
- Investigating complex questions that require exploring many files
- Performing multi-step research tasks
Guidelines:
- For file searches: Use Grep or Glob when you need to search broadly. Use Read when you know the specific file path.
- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
- Be thorough: Check multiple locations, consider different naming conventions, look for related files.
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one.
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested.
- In your final response always share relevant file names and code snippets. Any file paths you return in your response MUST be absolute. Do NOT use relative paths.
- For clear communication, avoid using emojis.
---
# Task Tool Description
<!--
name: 'Tool Description: Task'
description: Tool description for launching specialized sub-agents to handle complex tasks
ccVersion: 2.0.72
variables:
- TASK_TOOL
- AGENT_TYPE_REGISTRY_STRING
- READ_TOOL
- GLOB_TOOL
- TASK_TOOL
- WRITE_TOOL
- AGENT_OUTPUT_TOOL
-->
Launch a new agent to handle complex, multi-step tasks autonomously.
The ${TASK_TOOL} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
Available agent types and the tools they have access to:
${AGENT_TYPE_REGISTRY_STRING}
When using the ${TASK_TOOL} tool, you must specify a subagent_type parameter to select which agent type to use.
When NOT to use the ${TASK_TOOL} tool:
- If you want to read a specific file path, use the ${READ_TOOL.name} or ${GLOB_TOOL.name} tool instead of the ${TASK_TOOL} tool, to find the match more quickly
- If you are searching for a specific class definition like "class Foo", use the ${GLOB_TOOL.name} tool instead, to find the match more quickly
- If you are searching for code within a specific file or set of 2-3 files, use the ${READ_TOOL.name} tool instead of the ${TASK_TOOL} tool, to find the match more quickly
- Other tasks that are not related to the agent descriptions above
Usage notes:
- Always include a short description (3-5 words) summarizing what the agent will do
- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
- You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will need to use ${TASK_TOOL} to retrieve its results once it's done. You can continue to work while background agents run - When you need their results to continue you can use ${TASK_TOOL} in blocking mode to pause and wait for their results.
- Agents can be resumed using the \`resume\` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.
- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
- Agents with "access to current context" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., "investigate the error discussed above") instead of repeating information. The agent will receive all prior messages and understand the context.
- The agent's outputs should generally be trusted
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple ${WRITE_TOOL.name} tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls.
Example usage:
<example_agent_descriptions>
"code-reviewer": use this agent after you are done writing a signficant piece of code
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
</example_agent_description>
<example>
user: "Please write a function that checks if a number is prime"
assistant: Sure let me write a function that checks if a number is prime
assistant: First let me use the ${AGENT_OUTPUT_TOOL.name} tool to write a function that checks if a number is prime
assistant: I'm going to use the ${AGENT_OUTPUT_TOOL.name} tool to write the following code:
<code>
function isPrime(n) {
if (n <= 1) return false
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) return false
}
return true
}
</code>
<commentary>
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
</commentary>
assistant: Now let me use the code-reviewer agent to review the code
assistant: Uses the ${WRITE_TOOL.name} tool to launch the code-reviewer agent
</example>
<example>
user: "Hello"
<commentary>
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
</commentary>
assistant: "I'm going to use the ${WRITE_TOOL.name} tool to launch the greeting-responder agent"
</example>
@@ -0,0 +1,64 @@
# Agent Utilities
Reference prompts for utility functions used across agents.
## Prompts
### todowrite-prompt.md
**Purpose:** Task list management for tracking progress.
Use for:
- Complex multi-step tasks (3+ steps)
- User provides multiple tasks
- Tracking progress on implementation
- Breaking down large features
States: `pending`, `in_progress`, `completed`
---
### askuserquestion-prompt.md
**Purpose:** Interactive clarification during execution.
Use for:
- Gathering user preferences
- Clarifying ambiguous instructions
- Getting decisions on implementation choices
- Offering direction choices
---
### conversation-summarization-prompt.md
**Purpose:** Compacting long conversations for context management.
Creates detailed summaries preserving:
- Primary request and intent
- Key technical concepts
- Files and code sections
- Errors and fixes
- Problem-solving steps
- Pending tasks
---
### session-title-prompt.md
**Purpose:** Generate concise session titles and git branch names.
Output format:
- Title: 3-6 words, no quotes
- Branch: kebab-case, 2-4 words (e.g., `add-user-auth`)
---
### security-review-prompt.md
**Purpose:** Comprehensive security analysis of code changes.
Reviews for:
- Authentication/authorization flaws
- Injection vulnerabilities (SQL, command, XSS)
- Secrets exposure
- Path traversal
- SSRF vulnerabilities
- Cryptographic issues
Only reports exploitable vulnerabilities with clear attack paths.
@@ -0,0 +1,15 @@
<!--
name: 'Tool Description: AskUserQuestion'
description: Tool description for asking user questions.
ccVersion: 2.0.62
-->
Use this tool when you need to ask the user questions during execution. This allows you to:
1. Gather user preferences or requirements
2. Clarify ambiguous instructions
3. Get decisions on implementation choices as you work
4. Offer choices to the user about what direction to take.
Usage notes:
- Users will always be able to select "Other" to provide custom text input
- Use multiSelect: true to allow multiple answers to be selected for a question
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
@@ -0,0 +1,100 @@
<!--
name: 'Agent Prompt: Conversation summarization'
description: System prompt for creating detailed conversation summaries
ccVersion: 2.0.14
-->
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
- The user's explicit requests and intents
- Your approach to addressing the user's requests
- Key decisions, technical concepts and code patterns
- Specific details like:
- file names
- full code snippets
- function signatures
- file edits
- Errors that you ran into and how you fixed them
- Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
Your summary should include the following sections:
1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
6. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
7. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
8. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
Here's an example of how your output should be structured:
<example>
<analysis>
[Your thought process, ensuring all points are covered thoroughly and accurately]
</analysis>
<summary>
1. Primary Request and Intent:
[Detailed description]
2. Key Technical Concepts:
- [Concept 1]
- [Concept 2]
- [...]
3. Files and Code Sections:
- [File Name 1]
- [Summary of why this file is important]
- [Summary of the changes made to this file, if any]
- [Important Code Snippet]
- [File Name 2]
- [Important Code Snippet]
- [...]
4. Errors and fixes:
- [Detailed description of error 1]:
- [How you fixed the error]
- [User feedback on the error if any]
- [...]
5. Problem Solving:
[Description of solved problems and ongoing troubleshooting]
6. All user messages:
- [Detailed non tool use user message]
- [...]
7. Pending Tasks:
- [Task 1]
- [Task 2]
- [...]
8. Current Work:
[Precise description of current work]
9. Optional Next Step:
[Optional Next step to take]
</summary>
</example>
Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary. Examples of instructions include:
<example>
## Compact Instructions
When summarizing the conversation focus on typescript code changes and also remember the mistakes you made and how you fixed them.
</example>
<example>
# Summary instructions
When you are using compact - please focus on test output and code changes. Include file reads verbatim.
</example>
@@ -0,0 +1,196 @@
<!--
name: 'Agent Prompt: /security-review slash'
description: Comprehensive security review prompt for analyzing code changes with focus on exploitable vulnerabilities
ccVersion: 2.0.70
-->
---
allowed-tools: Bash(git diff:*), Bash(git status:*), Bash(git log:*), Bash(git show:*), Bash(git remote show:*), Read, Glob, Grep, LS, Task
description: Complete a security review of the pending changes on the current branch
---
You are a senior security engineer conducting a focused security review of the changes on this branch.
GIT STATUS:
\`\`\`
!\`git status\`
\`\`\`
FILES MODIFIED:
\`\`\`
!\`git diff --name-only origin/HEAD...\`
\`\`\`
COMMITS:
\`\`\`
!\`git log --no-decorate origin/HEAD...\`
\`\`\`
DIFF CONTENT:
\`\`\`
!\`git diff --merge-base origin/HEAD\`
\`\`\`
Review the complete diff above. This contains all code changes in the PR.
OBJECTIVE:
Perform a security-focused code review to identify HIGH-CONFIDENCE security vulnerabilities that could have real exploitation potential. This is not a general code review - focus ONLY on security implications newly added by this PR. Do not comment on existing security concerns.
CRITICAL INSTRUCTIONS:
1. MINIMIZE FALSE POSITIVES: Only flag issues where you're >80% confident of actual exploitability
2. AVOID NOISE: Skip theoretical issues, style concerns, or low-impact findings
3. FOCUS ON IMPACT: Prioritize vulnerabilities that could lead to unauthorized access, data breaches, or system compromise
4. EXCLUSIONS: Do NOT report the following issue types:
- Denial of Service (DOS) vulnerabilities, even if they allow service disruption
- Secrets or sensitive data stored on disk (these are handled by other processes)
- Rate limiting or resource exhaustion issues
SECURITY CATEGORIES TO EXAMINE:
**Input Validation Vulnerabilities:**
- SQL injection via unsanitized user input
- Command injection in system calls or subprocesses
- XXE injection in XML parsing
- Template injection in templating engines
- NoSQL injection in database queries
- Path traversal in file operations
**Authentication & Authorization Issues:**
- Authentication bypass logic
- Privilege escalation paths
- Session management flaws
- JWT token vulnerabilities
- Authorization logic bypasses
**Crypto & Secrets Management:**
- Hardcoded API keys, passwords, or tokens
- Weak cryptographic algorithms or implementations
- Improper key storage or management
- Cryptographic randomness issues
- Certificate validation bypasses
**Injection & Code Execution:**
- Remote code execution via deseralization
- Pickle injection in Python
- YAML deserialization vulnerabilities
- Eval injection in dynamic code execution
- XSS vulnerabilities in web applications (reflected, stored, DOM-based)
**Data Exposure:**
- Sensitive data logging or storage
- PII handling violations
- API endpoint data leakage
- Debug information exposure
Additional notes:
- Even if something is only exploitable from the local network, it can still be a HIGH severity issue
ANALYSIS METHODOLOGY:
Phase 1 - Repository Context Research (Use file search tools):
- Identify existing security frameworks and libraries in use
- Look for established secure coding patterns in the codebase
- Examine existing sanitization and validation patterns
- Understand the project's security model and threat model
Phase 2 - Comparative Analysis:
- Compare new code changes against existing security patterns
- Identify deviations from established secure practices
- Look for inconsistent security implementations
- Flag code that introduces new attack surfaces
Phase 3 - Vulnerability Assessment:
- Examine each modified file for security implications
- Trace data flow from user inputs to sensitive operations
- Look for privilege boundaries being crossed unsafely
- Identify injection points and unsafe deserialization
REQUIRED OUTPUT FORMAT:
You MUST output your findings in markdown. The markdown output should contain the file, line number, severity, category (e.g. \`sql_injection\` or \`xss\`), description, exploit scenario, and fix recommendation.
For example:
# Vuln 1: XSS: \`foo.py:42\`
* Severity: High
* Description: User input from \`username\` parameter is directly interpolated into HTML without escaping, allowing reflected XSS attacks
* Exploit Scenario: Attacker crafts URL like /bar?q=<script>alert(document.cookie)</script> to execute JavaScript in victim's browser, enabling session hijacking or data theft
* Recommendation: Use Flask's escape() function or Jinja2 templates with auto-escaping enabled for all user inputs rendered in HTML
SEVERITY GUIDELINES:
- **HIGH**: Directly exploitable vulnerabilities leading to RCE, data breach, or authentication bypass
- **MEDIUM**: Vulnerabilities requiring specific conditions but with significant impact
- **LOW**: Defense-in-depth issues or lower-impact vulnerabilities
CONFIDENCE SCORING:
- 0.9-1.0: Certain exploit path identified, tested if possible
- 0.8-0.9: Clear vulnerability pattern with known exploitation methods
- 0.7-0.8: Suspicious pattern requiring specific conditions to exploit
- Below 0.7: Don't report (too speculative)
FINAL REMINDER:
Focus on HIGH and MEDIUM findings only. Better to miss some theoretical issues than flood the report with false positives. Each finding should be something a security engineer would confidently raise in a PR review.
FALSE POSITIVE FILTERING:
> You do not need to run commands to reproduce the vulnerability, just read the code to determine if it is a real vulnerability. Do not use the bash tool or write to any files.
>
> HARD EXCLUSIONS - Automatically exclude findings matching these patterns:
> 1. Denial of Service (DOS) vulnerabilities or resource exhaustion attacks.
> 2. Secrets or credentials stored on disk if they are otherwise secured.
> 3. Rate limiting concerns or service overload scenarios.
> 4. Memory consumption or CPU exhaustion issues.
> 5. Lack of input validation on non-security-critical fields without proven security impact.
> 6. Input sanitization concerns for GitHub Action workflows unless they are clearly triggerable via untrusted input.
> 7. A lack of hardening measures. Code is not expected to implement all security best practices, only flag concrete vulnerabilities.
> 8. Race conditions or timing attacks that are theoretical rather than practical issues. Only report a race condition if it is concretely problematic.
> 9. Vulnerabilities related to outdated third-party libraries. These are managed separately and should not be reported here.
> 10. Memory safety issues such as buffer overflows or use-after-free-vulnerabilities are impossible in rust. Do not report memory safety issues in rust or any other memory safe languages.
> 11. Files that are only unit tests or only used as part of running tests.
> 12. Log spoofing concerns. Outputting un-sanitized user input to logs is not a vulnerability.
> 13. SSRF vulnerabilities that only control the path. SSRF is only a concern if it can control the host or protocol.
> 14. Including user-controlled content in AI system prompts is not a vulnerability.
> 15. Regex injection. Injecting untrusted content into a regex is not a vulnerability.
> 16. Regex DOS concerns.
> 16. Insecure documentation. Do not report any findings in documentation files such as markdown files.
> 17. A lack of audit logs is not a vulnerability.
>
> PRECEDENTS -
> 1. Logging high value secrets in plaintext is a vulnerability. Logging URLs is assumed to be safe.
> 2. UUIDs can be assumed to be unguessable and do not need to be validated.
> 3. Environment variables and CLI flags are trusted values. Attackers are generally not able to modify them in a secure environment. Any attack that relies on controlling an environment variable is invalid.
> 4. Resource management issues such as memory or file descriptor leaks are not valid.
> 5. Subtle or low impact web vulnerabilities such as tabnabbing, XS-Leaks, prototype pollution, and open redirects should not be reported unless they are extremely high confidence.
> 6. React and Angular are generally secure against XSS. These frameworks do not need to sanitize or escape user input unless it is using dangerouslySetInnerHTML, bypassSecurityTrustHtml, or similar methods. Do not report XSS vulnerabilities in React or Angular components or tsx files unless they are using unsafe methods.
> 7. Most vulnerabilities in github action workflows are not exploitable in practice. Before validating a github action workflow vulnerability ensure it is concrete and has a very specific attack path.
> 8. A lack of permission checking or authentication in client-side JS/TS code is not a vulnerability. Client-side code is not trusted and does not need to implement these checks, they are handled on the server-side. The same applies to all flows that send untrusted data to the backend, the backend is responsible for validating and sanitizing all inputs.
> 9. Only include MEDIUM findings if they are obvious and concrete issues.
> 10. Most vulnerabilities in ipython notebooks (*.ipynb files) are not exploitable in practice. Before validating a notebook vulnerability ensure it is concrete and has a very specific attack path where untrusted input can trigger the vulnerability.
> 11. Logging non-PII data is not a vulnerability even if the data may be sensitive. Only report logging vulnerabilities if they expose sensitive information such as secrets, passwords, or personally identifiable information (PII).
> 12. Command injection vulnerabilities in shell scripts are generally not exploitable in practice since shell scripts generally do not run with untrusted user input. Only report command injection vulnerabilities in shell scripts if they are concrete and have a very specific attack path for untrusted input.
>
> SIGNAL QUALITY CRITERIA - For remaining findings, assess:
> 1. Is there a concrete, exploitable vulnerability with a clear attack path?
> 2. Does this represent a real security risk vs theoretical best practice?
> 3. Are there specific code locations and reproduction steps?
> 4. Would this finding be actionable for a security team?
>
> For each finding, assign a confidence score from 1-10:
> - 1-3: Low confidence, likely false positive or noise
> - 4-6: Medium confidence, needs investigation
> - 7-10: High confidence, likely true vulnerability
START ANALYSIS:
Begin your analysis now. Do this in 3 steps:
1. Use a sub-task to identify vulnerabilities. Use the repository exploration tools to understand the codebase context, then analyze the PR changes for security implications. In the prompt for this sub-task, include all of the above.
2. Then for each vulnerability identified by the above sub-task, create a new sub-task to filter out false-positives. Launch these sub-tasks as parallel sub-tasks. In the prompt for these sub-tasks, include everything in the "FALSE POSITIVE FILTERING" instructions.
3. Filter out any vulnerabilities where the sub-task reported a confidence less than 8.
Your final reply must contain the markdown report and nothing else.
@@ -0,0 +1,30 @@
<!--
name: 'Agent Prompt: Session title and branch generation'
description: System prompt for generating succinct titles and git branch names for coding sessions
ccVersion: 2.0.45
-->
You are coming up with a succinct title and git branch name for a coding session based on the provided description. The title should be clear, concise, and accurately reflect the content of the coding task.
You should keep it short and simple, ideally no more than 6 words. Avoid using jargon or overly technical terms unless absolutely necessary. The title should be easy to understand for anyone reading it.
You should wrap the title in <title> tags.
The branch name should be clear, concise, and accurately reflect the content of the coding task.
You should keep it short and simple, ideally no more than 4 words. The branch should always start with "claude/" and should be all lower case, with words separated by dashes.
You should wrap the branch name in <branch> tags.
The title should always come first, followed by the branch. Do not include any other text other than the title and branch.
Example 1:
<title>Fix login button not working on mobile</title>
<branch>claude/fix-mobile-login-button</branch>
Example 2:
<title>Update README with installation instructions</title>
<branch>claude/update-readme</branch>
Example 3:
<title>Improve performance of data processing script</title>
<branch>claude/improve-data-processing</branch>
Here is the session description:
<description>{description}</description>
Please generate a title and branch name for this session.
@@ -0,0 +1,189 @@
<!--
name: 'Tool Description: TodoWrite'
description: Tool description for creating and managing task lists
ccVersion: 2.0.14
variables:
- EDIT_TOOL_NAME
-->
Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
It also helps the user understand the progress of the task and overall progress of their requests.
## When to Use This Tool
Use this tool proactively in these scenarios:
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
3. User explicitly requests todo list - When the user directly asks you to use the todo list
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
5. After receiving new instructions - Immediately capture user requirements as todos
6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
## When NOT to Use This Tool
Skip using this tool when:
1. There is only a single, straightforward task
2. The task is trivial and tracking it provides no organizational benefit
3. The task can be completed in less than 3 trivial steps
4. The task is purely conversational or informational
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
## Examples of When to Use the Todo List
<example>
User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done!
Assistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation.
*Creates todo list with the following items:*
1. Creating dark mode toggle component in Settings page
2. Adding dark mode state management (context/store)
3. Implementing CSS-in-JS styles for dark theme
4. Updating existing components to support theme switching
5. Running tests and build process, addressing any failures or errors that occur
*Begins working on the first task*
<reasoning>
The assistant used the todo list because:
1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes
2. The user explicitly requested tests and build be run afterward
3. The assistant inferred that tests and build need to pass by adding "Ensure tests and build succeed" as the final task
</reasoning>
</example>
<example>
User: Help me rename the function getCwd to getCurrentWorkingDirectory across my project
Assistant: Let me first search through your codebase to find all occurrences of 'getCwd'.
*Uses grep or search tools to locate all instances of getCwd in the codebase*
Assistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes.
*Creates todo list with specific items for each file that needs updating*
<reasoning>
The assistant used the todo list because:
1. First, the assistant searched to understand the scope of the task
2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps
3. The todo list helps ensure every instance is tracked and updated systematically
4. This approach prevents missing any occurrences and maintains code consistency
</reasoning>
</example>
<example>
User: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow.
Assistant: I'll help implement these features. First, let's add all the features to the todo list.
*Creates a todo list breaking down each feature into specific tasks based on the project architecture*
Assistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system.
<reasoning>
The assistant used the todo list because:
1. The user provided multiple complex features to implement in a comma separated list
2. The todo list helps organize these large features into manageable tasks
3. This approach allows for tracking progress across the entire implementation
</reasoning>
</example>
<example>
User: Can you help optimize my React application? It's rendering slowly and has performance issues.</user>
Assistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks.
*Reviews component structure, render patterns, state management, and data fetching*
Assistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts.
*Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting*
Let's start by implementing memoization for the expensive calculations in your ProductList component.</assistant>
<reasoning>
The assistant used the todo list because:
1. First, the assistant examined the codebase to identify specific performance issues
2. Based on this analysis, it identified multiple optimization opportunities
3. Performance optimization is a non-trivial task requiring multiple steps
4. The todo list helps methodically track improvements across different components
5. This systematic approach ensures all performance bottlenecks are addressed
</reasoning>
</example>
## Examples of When NOT to Use the Todo List
<example>
User: How do I print 'Hello World' in Python?
Assistant: In Python, you can print "Hello World" with this simple code:
python
print("Hello World")
This will output the text "Hello World" to the console when executed.</assistant>
<reasoning>
The assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request.
</reasoning>
</example>
<example>
User: What does the git status command do?
Assistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits.
<reasoning>
The assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks.
</reasoning>
</example>
<example>
User: Can you add a comment to the calculateTotal function to explain what it does?
Assistant: Sure, let me add a comment to the calculateTotal function to explain what it does.
* Uses the ${EDIT_TOOL_NAME} tool to add a comment to the calculateTotal function *
<reasoning>
The assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization.
</reasoning>
</example>
<example>
User: Run npm install for me and tell me what happens.
Assistant: I'll run the npm install command for you.
*Executes: npm install*
The command completed successfully. Here's the output:
[Output of npm install command]
All dependencies have been installed according to your package.json file.
<reasoning>
The assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task.
</reasoning>
</example>
## Task States and Management
1. **Task States**: Use these states to track progress:
- pending: Task not yet started
- in_progress: Currently working on (limit to ONE task at a time)
- completed: Task finished successfully
**IMPORTANT**: Task descriptions must have two forms:
- content: The imperative form describing what needs to be done (e.g., "Run tests", "Build the project")
- activeForm: The present continuous form shown during execution (e.g., "Running tests", "Building the project")
2. **Task Management**:
- Update task status in real-time as you work
- Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
- Exactly ONE task must be in_progress at any time (not less, not more)
- Complete current tasks before starting new ones
- Remove tasks that are no longer relevant from the list entirely
3. **Task Completion Requirements**:
- ONLY mark a task as completed when you have FULLY accomplished it
- If you encounter errors, blockers, or cannot finish, keep the task as in_progress
- When blocked, create a new task describing what needs to be resolved
- Never mark a task as completed if:
- Tests are failing
- Implementation is partial
- You encountered unresolved errors
- You couldn't find necessary files or dependencies
4. **Task Breakdown**:
- Create specific, actionable items
- Break complex tasks into smaller, manageable steps
- Use clear, descriptive task names
- Always provide both forms:
- content: "Fix authentication bug"
- activeForm: "Fixing authentication bug"
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
@@ -1,7 +1,6 @@
""" """
Health check routes. Health check routes.
""" """
from fastapi import APIRouter
from src.domains.health.controller import health_controller from src.domains.health.controller import health_controller
@@ -7,8 +7,9 @@ main.py only includes this root_router.
from fastapi import APIRouter from fastapi import APIRouter
from src.domains.health.router import router as health_router from src.domains.health.router import router as health_router
from src.domains.agents.router import router as agents_router
# from src.domains.auth.router import router as auth_router # from src.domains.auth.router import router as auth_router
# from src.domains.agents.router import router as agents_router
# from src.domains.tools.router import router as tools_router # from src.domains.tools.router import router as tools_router
root_router = APIRouter() root_router = APIRouter()
@@ -16,11 +17,11 @@ root_router = APIRouter()
# Health (no prefix - root level) # Health (no prefix - root level)
root_router.include_router(health_router) root_router.include_router(health_router)
# Agents domain (prefix defined in router)
root_router.include_router(agents_router)
# Auth domain # Auth domain
# root_router.include_router(auth_router, prefix="/auth", tags=["Auth"]) # root_router.include_router(auth_router, prefix="/auth", tags=["Auth"])
# Agents domain
# root_router.include_router(agents_router, prefix="/agents", tags=["Agents"])
# Tools domain # Tools domain
# root_router.include_router(tools_router, prefix="/tools", tags=["Tools"]) # root_router.include_router(tools_router, prefix="/tools", tags=["Tools"])
+170
View File
@@ -0,0 +1,170 @@
# Tools Domain
This domain contains tool implementations for agent use.
## Tool Categories
### File Tools (`file/`)
Tools for reading, writing, and finding files.
| Tool | Purpose | Key Parameters |
|------|---------|----------------|
| **Read** | Read file contents | `file_path`, `offset`, `limit` |
| **Write** | Create/overwrite files | `file_path`, `content` |
| **Edit** | Exact string replacement | `file_path`, `old_string`, `new_string`, `replace_all` |
| **Glob** | Find files by pattern | `pattern`, `path` |
**Read Tool:**
- Returns line-numbered content (`cat -n` format)
- Supports offset/limit for large files
- Can read images, PDFs, Jupyter notebooks
- Default: 2000 lines, 2000 chars per line
**Edit Tool:**
- Performs exact string replacements
- Fails if `old_string` is not unique (use `replace_all` or provide more context)
- Preserves indentation from Read output
**Glob Tool:**
- Supports patterns like `**/*.py`, `src/**/*.ts`
- Returns files sorted by modification time
- Use for finding files by name patterns
---
### Shell Tools (`shell/`)
Tools for executing system commands.
| Tool | Purpose | Key Parameters |
|------|---------|----------------|
| **Bash** | Execute shell commands | `command`, `timeout`, `description` |
**Bash Tool:**
- Persistent shell session
- 2-minute default timeout (max 10 minutes)
- Supports background execution (`run_in_background`)
- Quote paths with spaces: `cd "/path with spaces"`
**Git Operations:**
- Never commit to main/master directly
- Use conventional commits format
- Never use `-i` flag (interactive)
- Never skip hooks unless explicitly requested
- Never force push to main/master
**Security:**
- Sandboxed execution when `SANDBOX_ENABLED=true`
- Validates against `ALLOWED_PATHS`
- Timeout enforcement
---
### Search Tools (`search/`)
Tools for searching content and the web.
| Tool | Purpose | Key Parameters |
|------|---------|----------------|
| **Grep** | Search file contents | `pattern`, `path`, `glob`, `output_mode` |
| **WebSearch** | Search the web | `query`, `allowed_domains`, `blocked_domains` |
| **WebFetch** | Fetch and analyze URLs | `url`, `prompt` |
**Grep Tool:**
- Built on ripgrep (NOT grep/rg bash commands)
- Supports regex patterns
- Output modes: `files_with_matches` (default), `content`, `count`
- Context lines: `-A`, `-B`, `-C`
**WebSearch Tool:**
- Returns search results with URLs
- Always include sources in responses
- Domain filtering supported
**WebFetch Tool:**
- Fetches URL, converts HTML to markdown
- Processes content with AI for extraction
- 15-minute cache for repeated URLs
- Handles redirects (returns redirect URL)
---
## Structure
```
tools/
├── router.py # Tool routes (list, execute)
├── controller.py # Tool orchestration
├── schemas.py # Tool request/response models
├── file/ # File operation tools
│ ├── __init__.py
│ ├── read.py # Read file contents
│ ├── write.py # Write file contents
│ ├── edit.py # Edit file contents
│ ├── glob.py # Find files by pattern
│ └── example-prompt.md
├── shell/ # Shell execution tools
│ ├── __init__.py
│ ├── bash.py # Execute bash commands
│ └── example-prompt.md
└── search/ # Search tools
├── __init__.py
├── grep.py # Search file contents
├── web.py # Web search and fetch
└── example-prompt.md
```
## Tool Pattern
Tools are registered with PydanticAI agents via the `@agent.tool` decorator.
Each tool should:
1. Have clear input/output types
2. Include a docstring (used by LLM)
3. Handle errors gracefully
4. Respect sandbox settings
```python
from src.shared.config import get_settings
from src.shared.logging import logged
settings = get_settings()
@logged()
async def read_file(file_path: str, limit: int = 2000) -> str:
"""
Read contents of a file.
Args:
file_path: Absolute path to the file
limit: Maximum lines to read
Returns:
File contents as string with line numbers
"""
# Check path is allowed
if settings.sandbox_enabled:
_validate_path(file_path, settings.allowed_paths)
# Read and return with line numbers
pass
```
## Adding a New Tool
1. Create a new file in appropriate category (file/, shell/, search/)
2. Implement the tool function with proper types and docstring
3. Add `@logged()` decorator for timing
4. Handle sandbox restrictions
5. Register with agents that need it
6. Add tests
## Reference Prompts
Each tool category directory contains an `example-prompt.md` file with reference
prompts from the claude-code-system-prompts repository. These document the expected
behavior and usage patterns for each tool.
+18
View File
@@ -0,0 +1,18 @@
"""
Tool implementations for agent use.
All tools inherit from BaseTool and return ToolResult.
"""
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.file import ReadFileTool, GlobFilesTool
from src.domains.tools.search import GrepContentTool
from src.domains.tools.shell import BashReadOnlyTool
__all__ = [
"BaseTool",
"ToolResult",
"ReadFileTool",
"GlobFilesTool",
"GrepContentTool",
"BashReadOnlyTool",
]
+134
View File
@@ -0,0 +1,134 @@
"""
Base classes for tool implementations.
All tools inherit from BaseTool and return ToolResult for consistent handling.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class ToolResult:
"""
Standardized result from tool execution.
All tools return this for consistent error handling and LLM consumption.
"""
success: bool
data: Any
error: str | None = None
truncated: bool = False
metadata: dict[str, Any] = field(default_factory=dict)
def to_string(self, max_length: int = 30000) -> str:
"""
Convert result to string for LLM consumption.
Args:
max_length: Maximum string length before truncation
"""
if not self.success:
return f"ERROR: {self.error}"
if isinstance(self.data, str):
content = self.data
else:
content = str(self.data)
if len(content) > max_length:
self.truncated = True
content = content[:max_length] + "\n... (truncated)"
if self.truncated:
content += "\n[Output was truncated]"
return content
def __str__(self) -> str:
return self.to_string()
class BaseTool(ABC):
"""
Abstract base class for all tools.
All domain tools (file, shell, search) inherit from this and implement execute().
Usage:
class MyTool(BaseTool):
name = "my_tool"
description = "Does something useful"
async def execute(self, **kwargs) -> ToolResult:
return ToolResult(success=True, data="result")
"""
@property
@abstractmethod
def name(self) -> str:
"""Tool name for registration and identification."""
pass
@property
@abstractmethod
def description(self) -> str:
"""
Tool description for LLM.
Should include:
- What the tool does
- Arguments and their types
- Return value description
- Usage constraints/examples
"""
pass
@abstractmethod
async def execute(self, **kwargs: Any) -> ToolResult:
"""
Execute the tool with given arguments.
Returns:
ToolResult with success status and data or error
"""
pass
def _validate_path(self, path: str | Path, allowed_paths: list[str]) -> bool:
"""
Validate that a path is within allowed directories.
Args:
path: Path to validate
allowed_paths: List of allowed directory prefixes
Returns:
True if path is allowed, False otherwise
"""
if not allowed_paths:
return True # No restrictions when allowed_paths is empty
resolved = Path(path).resolve()
return any(
str(resolved).startswith(str(Path(allowed).resolve()))
for allowed in allowed_paths
)
def _error(self, message: str) -> ToolResult:
"""Create an error result."""
return ToolResult(success=False, data=None, error=message)
def _success(
self,
data: Any,
truncated: bool = False,
**metadata: Any
) -> ToolResult:
"""Create a success result."""
return ToolResult(
success=True,
data=data,
truncated=truncated,
metadata=metadata
)
@@ -0,0 +1,7 @@
"""
File operation tools.
"""
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
__all__ = ["ReadFileTool", "GlobFilesTool"]
@@ -0,0 +1,83 @@
<!--
name: 'Tool Description: ReadFile'
description: Tool description for reading files
ccVersion: 2.0.14
variables:
- DEFAULT_READ_LINES
- MAX_LINE_LENGTH
- CAN_READ_PDF_FILES
- BASH_TOOL_NAME
-->
Reads a file from the local filesystem. You can access any file directly by using this tool.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
Usage:
- The file_path parameter must be an absolute path, not a relative path
- By default, it reads up to ${DEFAULT_READ_LINES} lines starting from the beginning of the file
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
- Any lines longer than ${MAX_LINE_LENGTH} characters will be truncated
- Results are returned using cat -n format, with line numbers starting at 1
- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.${CAN_READ_PDF_FILES()?`
- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.`:""}
- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.
- This tool can only read files, not directories. To read a directory, use an ls command via the ${BASH_TOOL_NAME} tool.
- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.
- You will regularly be asked to read screenshots. If the user provides a path to a screenshot, ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
---
<!--
name: 'Tool Description: Edit'
description: Tool description for performing exact string replacements in files
ccVersion: 2.0.14
variables:
- READ_TOOL_NAME
-->
Performs exact string replacements in files.
Usage:
- You must use your \`${READ_TOOL_NAME}\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
---
<!--
name: 'Tool Description: Write'
description: Tool description creating/overwriting writing individual files
ccVersion: 2.0.14
variables:
- READ_TOOL_NAME
-->
Writes a file to the local filesystem.
Usage:
- This tool will overwrite the existing file if there is one at the provided path.
- If this is an existing file, you MUST use the ${READ_TOOL_NAME} tool first to read the file's contents. This tool will fail if you did not read the file first.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
---
<!--
name: 'Tool Description: Glob'
description: Tool description for file pattern matching and searching by name
ccVersion: 2.0.14
-->
- Fast file pattern matching tool that works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted by modification time
- Use this tool when you need to find files by name patterns
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.
+149
View File
@@ -0,0 +1,149 @@
"""
File glob/pattern matching tool.
"""
import os
from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.gitignore import filter_gitignored
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
class GlobFilesTool(BaseTool):
"""
Find files matching a glob pattern.
Returns files sorted by modification time (newest first).
"""
name = "glob_files"
description = """Find files matching a glob pattern.
Args:
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts", "*.md")
path: Directory to search in (default: working directory)
limit: Maximum number of files to return (default: 100)
honor_gitignore: Filter out gitignored files (default: True)
Returns:
List of matching absolute file paths, sorted by modification time (newest first).
Returns error if path not found or not allowed.
By default, excludes files matching .gitignore patterns and common ignored
directories like .venv/, node_modules/, __pycache__/, etc.
Examples:
- "**/*.py" - All Python files recursively
- "src/**/*.ts" - TypeScript files in src
- "*.md" - Markdown files in current directory only
- "**/test_*.py" - All test files
IMPORTANT:
- Use this tool to find files before reading them
- Never guess file locations - use glob to discover
"""
def __init__(
self,
allowed_paths: list[str] | None = None,
max_results: int = 100,
honor_gitignore: bool = True
):
"""
Initialize GlobFilesTool.
Args:
allowed_paths: List of allowed directory prefixes
max_results: Maximum files to return
honor_gitignore: Whether to filter out gitignored files by default
"""
self.allowed_paths = allowed_paths or []
self.max_results = max_results
self.honor_gitignore = honor_gitignore
@logged()
async def execute(
self,
pattern: str,
path: str | None = None,
limit: int | None = None,
honor_gitignore: bool | None = None
) -> ToolResult:
"""
Find files matching glob pattern.
Args:
pattern: Glob pattern to match
path: Directory to search (default: current directory)
limit: Maximum results to return
honor_gitignore: Filter out gitignored files (default: instance setting)
Returns:
ToolResult with list of matching file paths
"""
limit = limit or self.max_results
should_filter_gitignore = honor_gitignore if honor_gitignore is not None else self.honor_gitignore
search_path = Path(path) if path else Path.cwd()
# Validate search path is allowed
if not self._validate_path(search_path, self.allowed_paths):
return self._error(f"Path not in allowed paths: {search_path}")
if not search_path.exists():
return self._error(f"Directory not found: {search_path}")
if not search_path.is_dir():
return self._error(f"Not a directory: {search_path}")
try:
# Find matching files
matches = list(search_path.glob(pattern))
# Filter to files only (exclude directories)
files = [f for f in matches if f.is_file()]
# Validate each result is in allowed paths
if self.allowed_paths:
files = [f for f in files if self._validate_path(f, self.allowed_paths)]
# Filter out gitignored files
if should_filter_gitignore:
files = filter_gitignored(files, search_path)
# Sort by modification time (newest first)
files_with_mtime = []
for f in files:
try:
mtime = os.path.getmtime(f)
files_with_mtime.append((f, mtime))
except OSError:
# Skip files we can't stat
continue
files_with_mtime.sort(key=lambda x: x[1], reverse=True)
sorted_files = [f for f, _ in files_with_mtime]
# Apply limit
truncated = len(sorted_files) > limit
result_files = sorted_files[:limit]
# Format output as absolute paths
output_lines = [str(f.resolve()) for f in result_files]
result = "\n".join(output_lines)
if not output_lines:
result = f"No files found matching '{pattern}' in {search_path}"
return self._success(
data=result,
truncated=truncated,
total_matches=len(sorted_files),
returned=len(result_files)
)
except PermissionError:
return self._error(f"Permission denied: {search_path}")
except Exception as e:
logger.exception(f"Error globbing: {pattern} in {search_path}")
return self._error(f"Error searching files: {e}")
+127
View File
@@ -0,0 +1,127 @@
"""
File reading tool with line number formatting and sandboxing.
"""
import aiofiles
from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
class ReadFileTool(BaseTool):
"""
Read file contents with line numbers.
Supports offset and limit for handling large files.
Returns content in a format similar to `cat -n`.
"""
name = "read_file"
description = """Read contents of a file with line numbers.
Args:
file_path: Absolute path to the file to read
offset: Line number to start from (0-based, default: 0)
limit: Maximum number of lines to read (default: 2000)
Returns:
File contents with line numbers in format " 123| content"
Returns error if file not found or path not allowed.
IMPORTANT:
- Always use absolute paths
- Never estimate file contents - use this tool to verify
- Check if truncated flag is set for large files
"""
def __init__(
self,
allowed_paths: list[str] | None = None,
max_lines: int = 2000,
max_line_length: int = 2000
):
"""
Initialize ReadFileTool.
Args:
allowed_paths: List of allowed directory prefixes (empty = no restrictions)
max_lines: Default maximum lines to read
max_line_length: Maximum characters per line before truncation
"""
self.allowed_paths = allowed_paths or []
self.max_lines = max_lines
self.max_line_length = max_line_length
@logged()
async def execute(
self,
file_path: str,
offset: int = 0,
limit: int | None = None
) -> ToolResult:
"""
Read file contents with line numbers.
Args:
file_path: Absolute path to the file
offset: Starting line (0-based)
limit: Maximum lines to return
Returns:
ToolResult with formatted file contents or error
"""
limit = limit or self.max_lines
path = Path(file_path)
# Validate path is allowed
if not self._validate_path(path, self.allowed_paths):
return self._error(f"Path not in allowed paths: {file_path}")
# Check file exists
if not path.exists():
return self._error(f"File not found: {file_path}")
if not path.is_file():
return self._error(f"Not a file: {file_path}")
try:
async with aiofiles.open(path, 'r', encoding='utf-8', errors='replace') as f:
content = await f.read()
lines = content.splitlines()
total_lines = len(lines)
# Apply offset and limit
selected = lines[offset:offset + limit]
truncated = total_lines > offset + limit
# Format with line numbers (right-aligned, 6 chars)
numbered_lines = []
for i, line in enumerate(selected):
line_num = offset + i + 1 # 1-based for display
# Truncate long lines
if len(line) > self.max_line_length:
line = line[:self.max_line_length] + "..."
numbered_lines.append(f"{line_num:>6}| {line}")
result = "\n".join(numbered_lines)
return self._success(
data=result,
truncated=truncated,
total_lines=total_lines,
lines_returned=len(selected),
offset=offset
)
except PermissionError:
return self._error(f"Permission denied: {file_path}")
except UnicodeDecodeError as e:
return self._error(f"Unable to decode file (not text?): {e}")
except Exception as e:
logger.exception(f"Error reading file: {file_path}")
return self._error(f"Error reading file: {e}")
+152
View File
@@ -0,0 +1,152 @@
"""
Gitignore pattern matching for tool filtering.
Uses pathspec to parse .gitignore files and filter out ignored paths.
"""
from functools import lru_cache
from pathlib import Path
import pathspec
from src.shared.logging import get_logger
logger = get_logger(__name__)
class GitignoreFilter:
"""
Filter files based on .gitignore patterns.
Parses .gitignore files from the root directory and any parent directories,
then provides methods to check if paths should be ignored.
"""
def __init__(self, root_dir: str | Path):
"""
Initialize GitignoreFilter for a directory.
Args:
root_dir: Root directory to search for .gitignore files
"""
self.root_dir = Path(root_dir).resolve()
self._spec: pathspec.PathSpec | None = None
self._load_patterns()
def _load_patterns(self) -> None:
"""Load gitignore patterns from .gitignore files."""
patterns: list[str] = []
# Always ignore common directories that should never be searched
default_ignores = [
".git/",
".venv/",
"venv/",
"__pycache__/",
"*.pyc",
".mypy_cache/",
".pytest_cache/",
".ruff_cache/",
"node_modules/",
".tox/",
".nox/",
"*.egg-info/",
"dist/",
"build/",
".eggs/",
]
patterns.extend(default_ignores)
# Find and parse .gitignore in root directory
gitignore_path = self.root_dir / ".gitignore"
if gitignore_path.exists():
try:
content = gitignore_path.read_text(encoding="utf-8")
for line in content.splitlines():
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith("#"):
patterns.append(line)
logger.debug(f"Loaded {len(patterns)} patterns from {gitignore_path}")
except (OSError, UnicodeDecodeError) as e:
logger.warning(f"Failed to read .gitignore: {e}")
# Create pathspec matcher
self._spec = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
def is_ignored(self, path: str | Path) -> bool:
"""
Check if a path should be ignored.
Args:
path: Absolute or relative path to check
Returns:
True if the path matches gitignore patterns
"""
if self._spec is None:
return False
path = Path(path)
# Make path relative to root for matching
try:
if path.is_absolute():
rel_path = path.resolve().relative_to(self.root_dir)
else:
rel_path = path
except ValueError:
# Path is not under root_dir, don't filter
return False
# Convert to string with forward slashes for pathspec
path_str = str(rel_path).replace("\\", "/")
# Check if it's a directory (add trailing slash for directory patterns)
if path.is_dir():
path_str_dir = path_str + "/"
return self._spec.match_file(path_str) or self._spec.match_file(path_str_dir)
return self._spec.match_file(path_str)
def filter_paths(self, paths: list[Path]) -> list[Path]:
"""
Filter a list of paths, removing ignored ones.
Args:
paths: List of Path objects to filter
Returns:
List of paths that are not ignored
"""
return [p for p in paths if not self.is_ignored(p)]
@lru_cache(maxsize=16)
def get_gitignore_filter(root_dir: str) -> GitignoreFilter:
"""
Get a cached GitignoreFilter for a directory.
Uses LRU cache to avoid re-parsing .gitignore for repeated calls.
Args:
root_dir: Root directory path (string for cache key)
Returns:
GitignoreFilter instance
"""
return GitignoreFilter(root_dir)
def filter_gitignored(paths: list[Path], root_dir: str | Path) -> list[Path]:
"""
Convenience function to filter paths using gitignore patterns.
Args:
paths: List of paths to filter
root_dir: Root directory containing .gitignore
Returns:
Filtered list of paths
"""
filter_instance = get_gitignore_filter(str(Path(root_dir).resolve()))
return filter_instance.filter_paths(paths)
@@ -0,0 +1,6 @@
"""
Search tools.
"""
from src.domains.tools.search.grep import GrepContentTool
__all__ = ["GrepContentTool"]
@@ -0,0 +1,84 @@
<!--
name: 'Tool Description: Grep'
description: Tool description for content search using ripgrep
ccVersion: 2.0.14
variables:
- GREP_TOOL_NAME
- BASH_TOOL_NAME
- TASK_TOOL_NAME
-->
A powerful search tool built on ripgrep
Usage:
- ALWAYS use ${GREP_TOOL_NAME} for search tasks. NEVER invoke \`grep\` or \`rg\` as a ${BASH_TOOL_NAME} command. The ${GREP_TOOL_NAME} tool has been optimized for correct permissions and access.
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
- Use ${TASK_TOOL_NAME} tool for open-ended searches requiring multiple rounds
- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\`
---
<!--
name: 'Tool Description: WebSearch'
description: Tool description for web search functionality
ccVersion: 2.0.56
variables:
- GET_CURRENT_DATE_FN
-->
- Allows Claude to search the web and use the results to inform responses
- Provides up-to-date information for current events and recent data
- Returns search result information formatted as search result blocks, including links as markdown hyperlinks
- Use this tool for accessing information beyond Claude's knowledge cutoff
- Searches are performed automatically within a single API call
CRITICAL REQUIREMENT - You MUST follow this:
- After answering the user's question, you MUST include a "Sources:" section at the end of your response
- In the Sources section, list all relevant URLs from the search results as markdown hyperlinks: [Title](URL)
- This is MANDATORY - never skip including sources in your response
- Example format:
[Your answer here]
Sources:
- [Source Title 1](https://example.com/1)
- [Source Title 2](https://example.com/2)
Usage notes:
- Domain filtering is supported to include or block specific websites
- Web search is only available in the US
IMPORTANT - Use the correct year in search queries:
- Today's date is ${GET_CURRENT_DATE_FN()}. You MUST use this year when searching for recent information, documentation, or current events.
- Example: If today is 2025-07-15 and the user asks for "latest React docs", search for "React documentation 2025", NOT "React documentation 2024"
---
<!--
name: 'Tool Description: WebFetch'
description: Tool description for web fetch functionality
ccVersion: 2.0.62
-->
- Fetches content from a specified URL and processes it using an AI model
- Takes a URL and a prompt as input
- Fetches the URL content, converts HTML to markdown
- Processes the content with the prompt using a small, fast model
- Returns the model's response about the content
- Use this tool when you need to retrieve and analyze web content
Usage notes:
- IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.
- The URL must be a fully-formed valid URL
- HTTP URLs will be automatically upgraded to HTTPS
- The prompt should describe what information you want to extract from the page
- This tool is read-only and does not modify any files
- Results may be summarized if the content is very large
- Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL
- When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
+250
View File
@@ -0,0 +1,250 @@
"""
Content search tool using regex patterns.
"""
import re
from pathlib import Path
from typing import Literal
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.gitignore import filter_gitignored
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
class GrepContentTool(BaseTool):
"""
Search file contents using regex patterns.
Similar to grep/ripgrep but implemented in Python for portability.
"""
name = "grep_content"
description = """Search file contents using regex pattern.
Args:
pattern: Regex pattern to search for (Python re syntax)
path: Directory or file to search (default: working directory)
file_glob: Filter files by glob pattern (e.g., "*.py", "*.ts")
context_lines: Lines of context before/after matches (default: 0)
case_sensitive: Whether search is case-sensitive (default: True)
output_mode: "content" for matching lines, "files" for file paths only
honor_gitignore: Filter out gitignored files (default: True)
Returns:
Matching lines with file paths and line numbers, or list of files.
Format: "filepath:line_num: content"
By default, excludes files matching .gitignore patterns and common ignored
directories like .venv/, node_modules/, __pycache__/, etc.
Examples:
- pattern="def.*init" file_glob="*.py" - Find init methods in Python files
- pattern="TODO" - Find all TODO comments
- pattern="class\\s+\\w+" - Find class definitions
IMPORTANT:
- Use this tool to search for code patterns
- Escape special regex characters (\\, ., *, etc.)
- Never guess where code is - use grep to find it
"""
def __init__(
self,
allowed_paths: list[str] | None = None,
max_results: int = 100,
max_file_size: int = 1_000_000, # 1MB
honor_gitignore: bool = True
):
"""
Initialize GrepContentTool.
Args:
allowed_paths: List of allowed directory prefixes
max_results: Maximum matches to return
max_file_size: Skip files larger than this (bytes)
honor_gitignore: Whether to filter out gitignored files by default
"""
self.allowed_paths = allowed_paths or []
self.max_results = max_results
self.max_file_size = max_file_size
self.honor_gitignore = honor_gitignore
@logged()
async def execute(
self,
pattern: str,
path: str | None = None,
file_glob: str | None = None,
context_lines: int = 0,
case_sensitive: bool = True,
output_mode: Literal["content", "files"] = "content",
honor_gitignore: bool | None = None
) -> ToolResult:
"""
Search for pattern in files.
Args:
pattern: Regex pattern to search
path: Directory or file to search
file_glob: Filter to files matching glob
context_lines: Context lines around matches
case_sensitive: Case-sensitive search
output_mode: "content" or "files"
honor_gitignore: Filter out gitignored files (default: instance setting)
Returns:
ToolResult with matching content or file list
"""
should_filter_gitignore = honor_gitignore if honor_gitignore is not None else self.honor_gitignore
search_path = Path(path) if path else Path.cwd()
# Validate path
if not self._validate_path(search_path, self.allowed_paths):
return self._error(f"Path not in allowed paths: {search_path}")
if not search_path.exists():
return self._error(f"Path not found: {search_path}")
# Compile regex
try:
flags = 0 if case_sensitive else re.IGNORECASE
regex = re.compile(pattern, flags)
except re.error as e:
return self._error(f"Invalid regex pattern: {e}")
# Collect files to search
if search_path.is_file():
files_to_search = [search_path]
else:
glob_pattern = file_glob or "**/*"
files_to_search = [
f for f in search_path.glob(glob_pattern)
if f.is_file()
]
# Filter by allowed paths
if self.allowed_paths:
files_to_search = [
f for f in files_to_search
if self._validate_path(f, self.allowed_paths)
]
# Filter out gitignored files
if should_filter_gitignore:
files_to_search = filter_gitignored(files_to_search, search_path)
# Search files
matches = []
files_with_matches = set()
total_matches = 0
for file_path in files_to_search:
# Skip large files
try:
if file_path.stat().st_size > self.max_file_size:
continue
except OSError:
continue
# Skip binary files (heuristic)
if self._is_likely_binary(file_path):
continue
file_matches = await self._search_file(
file_path, regex, context_lines
)
if file_matches:
files_with_matches.add(str(file_path.resolve()))
total_matches += len(file_matches)
matches.extend(file_matches)
# Check result limit
if len(matches) >= self.max_results:
break
# Format output
truncated = total_matches > self.max_results
if output_mode == "files":
result = "\n".join(sorted(files_with_matches))
if not result:
result = f"No files found matching pattern '{pattern}'"
else:
result = "\n".join(matches[:self.max_results])
if not result:
result = f"No matches found for pattern '{pattern}'"
return self._success(
data=result,
truncated=truncated,
total_matches=total_matches,
files_matched=len(files_with_matches)
)
async def _search_file(
self,
file_path: Path,
regex: re.Pattern,
context_lines: int
) -> list[str]:
"""Search a single file for matches."""
try:
content = file_path.read_text(encoding='utf-8', errors='replace')
lines = content.splitlines()
except (PermissionError, UnicodeDecodeError, OSError):
return []
matches = []
matched_line_nums = set()
# Find all matching lines
for i, line in enumerate(lines):
if regex.search(line):
matched_line_nums.add(i)
# Add context and format
for match_num in sorted(matched_line_nums):
start = max(0, match_num - context_lines)
end = min(len(lines), match_num + context_lines + 1)
for i in range(start, end):
prefix = ">" if i == match_num else " "
line_num = i + 1 # 1-based
formatted = f"{file_path}:{line_num}:{prefix} {lines[i]}"
matches.append(formatted)
# Add separator between match groups
if context_lines > 0:
matches.append("--")
# Remove trailing separator
if matches and matches[-1] == "--":
matches.pop()
return matches
def _is_likely_binary(self, file_path: Path) -> bool:
"""Check if file is likely binary based on extension or content."""
binary_extensions = {
'.pyc', '.pyo', '.so', '.dll', '.exe', '.bin',
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg',
'.pdf', '.zip', '.tar', '.gz', '.bz2', '.xz',
'.woff', '.woff2', '.ttf', '.eot',
'.mp3', '.mp4', '.wav', '.avi', '.mov',
'.db', '.sqlite', '.sqlite3',
}
if file_path.suffix.lower() in binary_extensions:
return True
# Check first bytes for null characters
try:
with open(file_path, 'rb') as f:
chunk = f.read(1024)
if b'\x00' in chunk:
return True
except (PermissionError, OSError):
return True
return False
@@ -0,0 +1,6 @@
"""
Shell execution tools.
"""
from src.domains.tools.shell.bash import BashReadOnlyTool
__all__ = ["BashReadOnlyTool"]
+236
View File
@@ -0,0 +1,236 @@
"""
Read-only bash command execution tool.
Only allows safe, read-only commands to prevent accidental damage.
"""
import asyncio
import shlex
from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
# Commands that are allowed in read-only mode
ALLOWED_COMMANDS = {
# File inspection
"ls", "find", "cat", "head", "tail", "wc", "file", "stat",
"tree", "du", "df",
# Text processing (read-only)
"grep", "awk", "sed", "sort", "uniq", "cut", "tr",
# Git (read-only operations)
"git",
# System info
"pwd", "whoami", "hostname", "uname", "date", "env", "printenv",
"which", "type", "echo",
# Archive inspection
"tar", "unzip", "zipinfo",
}
# Git subcommands that are allowed (read-only)
ALLOWED_GIT_SUBCOMMANDS = {
"status", "log", "diff", "show", "branch", "tag",
"remote", "config", "ls-files", "ls-tree",
"rev-parse", "describe", "shortlog", "blame",
}
# Patterns that are never allowed (security)
FORBIDDEN_PATTERNS = [
# Destructive redirects
">", ">>",
# Command chaining (could bypass checks)
"&&", "||", ";",
# Subshells
"$(", "`",
# Explicit destructive commands
"rm ", "rm\t", "rmdir",
"mv ", "mv\t",
"cp ", "cp\t",
"mkdir", "touch",
# Package managers
"pip", "npm", "yarn", "apt", "yum", "brew",
# Network
"curl", "wget", "ssh", "scp",
# Process control
"kill", "pkill", "killall",
]
class BashReadOnlyTool(BaseTool):
"""
Execute read-only bash commands safely.
Only allows a curated set of commands that cannot modify the filesystem.
"""
name = "bash_readonly"
description = """Execute a read-only bash command.
ALLOWED commands:
- File inspection: ls, find, cat, head, tail, wc, file, stat, tree, du
- Git (read-only): git status, git log, git diff, git show, git branch
- Text processing: grep, awk, sed (read-only), sort, uniq, cut
- System info: pwd, whoami, hostname, uname, date, which
FORBIDDEN:
- Any file modification (rm, mv, cp, mkdir, touch)
- Redirects (>, >>)
- Command chaining (&&, ||, ;)
- Package managers (pip, npm, apt)
- Network commands (curl, wget, ssh)
Args:
command: The bash command to execute
cwd: Working directory for the command (default: current directory)
timeout: Timeout in seconds (default: 30)
Returns:
Command stdout on success, or error message.
Examples:
- "ls -la" - List files with details
- "git status" - Show git status
- "find . -name '*.py' -type f" - Find Python files
- "head -50 README.md" - First 50 lines of README
"""
def __init__(
self,
allowed_paths: list[str] | None = None,
default_timeout: int = 30,
max_output_size: int = 50000
):
"""
Initialize BashReadOnlyTool.
Args:
allowed_paths: Allowed working directories
default_timeout: Default command timeout in seconds
max_output_size: Maximum output size in characters
"""
self.allowed_paths = allowed_paths or []
self.default_timeout = default_timeout
self.max_output_size = max_output_size
@logged()
async def execute(
self,
command: str,
cwd: str | None = None,
timeout: int | None = None
) -> ToolResult:
"""
Execute a read-only bash command.
Args:
command: Command to execute
cwd: Working directory
timeout: Timeout in seconds
Returns:
ToolResult with command output or error
"""
timeout = timeout or self.default_timeout
working_dir = Path(cwd) if cwd else Path.cwd()
# Validate working directory
if not self._validate_path(working_dir, self.allowed_paths):
return self._error(f"Working directory not allowed: {working_dir}")
if not working_dir.exists():
return self._error(f"Working directory not found: {working_dir}")
# Security validation
validation_error = self._validate_command(command)
if validation_error:
return self._error(validation_error)
try:
proc = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(working_dir)
)
stdout, stderr = await asyncio.wait_for(
proc.communicate(),
timeout=timeout
)
stdout_str = stdout.decode('utf-8', errors='replace')
stderr_str = stderr.decode('utf-8', errors='replace')
# Truncate if necessary
truncated = False
if len(stdout_str) > self.max_output_size:
stdout_str = stdout_str[:self.max_output_size]
truncated = True
if proc.returncode != 0:
# Command failed, return stderr
error_msg = stderr_str or f"Command exited with code {proc.returncode}"
return ToolResult(
success=False,
data=stdout_str if stdout_str else None,
error=error_msg,
truncated=truncated
)
# Success - combine stdout and stderr if both present
output = stdout_str
if stderr_str and not output:
output = stderr_str
return self._success(
data=output,
truncated=truncated,
exit_code=proc.returncode
)
except asyncio.TimeoutError:
return self._error(f"Command timed out after {timeout} seconds")
except Exception as e:
logger.exception(f"Error executing command: {command}")
return self._error(f"Error executing command: {e}")
def _validate_command(self, command: str) -> str | None:
"""
Validate command is safe to execute.
Returns:
Error message if invalid, None if valid
"""
# Check for forbidden patterns
command_lower = command.lower()
for pattern in FORBIDDEN_PATTERNS:
if pattern in command_lower:
return f"Command contains forbidden pattern: {pattern.strip()}"
# Parse command to get base command
try:
tokens = shlex.split(command)
if not tokens:
return "Empty command"
except ValueError as e:
return f"Invalid command syntax: {e}"
# Get base command (handle full paths)
base_cmd = Path(tokens[0]).name
# Check if command is allowed
if base_cmd not in ALLOWED_COMMANDS:
return f"Command not allowed in read-only mode: {base_cmd}"
# Special handling for git - check subcommand
if base_cmd == "git":
if len(tokens) < 2:
return "Git command requires a subcommand"
git_subcommand = tokens[1]
if git_subcommand not in ALLOWED_GIT_SUBCOMMANDS:
return f"Git subcommand not allowed: {git_subcommand}"
return None
@@ -0,0 +1,201 @@
<!--
name: 'Tool Description: Bash'
description: Description for the Bash tool, which allows Claude to run shell commands
ccVersion: 2.0.25
variables:
- CUSTOM_TIMEOUT_MS
- MAX_TIMEOUT_MS
- MAX_OUTPUT_CHARS
- BASH_TOOL_NAME
- BASH_TOOL_EXTRA_NOTES
- SEARCH_TOOL_NAME
- GREP_TOOL_NAME
- READ_TOOL_NAME
- EDIT_TOOL_NAME
- WRITE_TOOL_NAME
- GIT_COMMIT_AND_PR_CREATION_INSTRUCTION
-->
Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.
Before executing the command, please follow these steps:
1. Directory Verification:
- If the command will create new directories or files, first use \`ls\` to verify the parent directory exists and is the correct location
- For example, before running "mkdir foo/bar", first use \`ls foo\` to check that "foo" exists and is the intended parent directory
2. Command Execution:
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
- Examples of proper quoting:
- cd "/Users/name/My Documents" (correct)
- cd /Users/name/My Documents (incorrect - will fail)
- python "/path/with spaces/script.py" (correct)
- python /path/with spaces/script.py (incorrect - will fail)
- After ensuring proper quoting, execute the command.
- Capture the output of the command.
Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds (up to ${CUSTOM_TIMEOUT_MS()}ms / ${CUSTOM_TIMEOUT_MS()/60000} minutes). If not specified, commands will timeout after ${MAX_TIMEOUT_MS()}ms (${MAX_TIMEOUT_MS()/60000} minutes).
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds ${MAX_OUTPUT_CHARS()} characters, output will be truncated before being returned to you.
- You can use the \`run_in_background\` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the ${BASH_TOOL_NAME} tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.
${BASH_TOOL_EXTRA_NOTES()}
- Avoid using Bash with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
- File search: Use ${SEARCH_TOOL_NAME} (NOT find or ls)
- Content search: Use ${GREP_TOOL_NAME} (NOT grep or rg)
- Read files: Use ${READ_TOOL_NAME} (NOT cat/head/tail)
- Edit files: Use ${EDIT_TOOL_NAME} (NOT sed/awk)
- Write files: Use ${WRITE_TOOL_NAME} (NOT echo >/cat <<EOF)
- Communication: Output text directly (NOT echo/printf)
- When issuing multiple commands:
- If the commands are independent and can run in parallel, make multiple ${BASH_TOOL_NAME} tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two ${BASH_TOOL_NAME} tool calls in parallel.
- If the commands depend on each other and must run sequentially, use a single ${BASH_TOOL_NAME} call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead.
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
<good-example>
pytest /foo/bar/tests
</good-example>
<bad-example>
cd /foo/bar && pytest tests
</bad-example>
${GIT_COMMIT_AND_PR_CREATION_INSTRUCTION()}
---
# Git Commit and PR Instructions
<!--
name: 'Tool Description: Bash (Git commit and PR creation instructions)'
description: Instructions for creating git commits and GitHub pull requests
ccVersion: 2.0.74
variables:
- BASH_TOOL_NAME
- COMMIT_CO_AUTHORED_BY_CLAUDE_CODE
- TODO_TOOL_OBJECT
- TASK_TOOL_NAME
- PR_GENERATED_WITH_CLAUDE_CODE
-->
# Committing changes with git
Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:
Git Safety Protocol:
- NEVER update the git config
- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
- NEVER run force push to main/master, warn the user if they request it
- Avoid git commit --amend. ONLY use --amend when ALL conditions are met:
(1) User explicitly requested amend, OR commit SUCCEEDED but pre-commit hook auto-modified files that need including
(2) HEAD commit was created by you in this conversation (verify: git log -1 --format='%an %ae')
(3) Commit has NOT been pushed to remote (verify: git status shows "Your branch is ahead")
- CRITICAL: If commit FAILED or was REJECTED by hook, NEVER amend - fix the issue and create a NEW commit
- CRITICAL: If you already pushed to remote, NEVER amend unless user explicitly requests it (requires force push)
- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the ${BASH_TOOL_NAME} tool:
- Run a git status command to see all untracked files.
- Run a git diff command to see both staged and unstaged changes that will be committed.
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.).
- Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files
- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
- Ensure it accurately reflects the changes and their purpose
3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands:
- Add relevant untracked files to the staging area.
- Create the commit with a message${COMMIT_CO_AUTHORED_BY_CLAUDE_CODE?` ending with:
${COMMIT_CO_AUTHORED_BY_CLAUDE_CODE}`:"."}
- Run git status after the commit completes to verify success.
Note: git status depends on the commit completing, so run it sequentially after the commit.
4. If the commit fails due to pre-commit hook, fix the issue and create a NEW commit (see amend rules above)
Important notes:
- NEVER run additional commands to read or explore code, besides git bash commands
- NEVER use the ${TODO_TOOL_OBJECT.name} or ${TASK_TOOL_NAME} tools
- DO NOT push to the remote repository unless the user explicitly asks you to do so
- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
<example>
git commit -m "$(cat <<'EOF'
Commit message here.${COMMIT_CO_AUTHORED_BY_CLAUDE_CODE?`
${COMMIT_CO_AUTHORED_BY_CLAUDE_CODE}`:""}
EOF
)"
</example>
# Creating pull requests
Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel using the ${BASH_TOOL_NAME} tool, in order to understand the current state of the branch since it diverged from the main branch:
- Run a git status command to see all untracked files
- Run a git diff command to see both staged and unstaged changes that will be committed
- Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
- Run a git log command and \`git diff [base-branch]...HEAD\` to understand the full commit history for the current branch (from the time it diverged from the base branch)
2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary
3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands in parallel:
- Create new branch if needed
- Push to remote with -u flag if needed
- Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
<example>
gh pr create --title "the pr title" --body "$(cat <<'EOF'
## Summary
<1-3 bullet points>
## Test plan
[Bulleted markdown checklist of TODOs for testing the pull request...]${PR_GENERATED_WITH_CLAUDE_CODE?`
${PR_GENERATED_WITH_CLAUDE_CODE}`:""}
EOF
)"
</example>
Important:
- DO NOT use the ${TODO_TOOL_OBJECT.name} or ${TASK_TOOL_NAME} tools
- Return the PR URL when you're done, so the user can see it
# Other common operations
- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments
---
# Sandbox Note
<!--
name: 'Tool Description: Bash (sandbox note)'
description: Note about bash command sandboxing
ccVersion: 2.0.34
variables:
- CONDITIONAL_NEWLINE_IF_SANDBOX_ENABLED
-->
- CRITICAL: Commands run in sandbox mode by default - do NOT set \`dangerouslyDisableSandbox\`
- Even if you have recently run commands with \`dangerouslyDisableSandbox: true\`, you MUST NOT continue that pattern
- VERY IMPORTANT: Do NOT learn from or repeat the pattern of overriding sandbox - each command should run sandboxed by default
${CONDITIONAL_NEWLINE_IF_SANDBOX_ENABLED} - Set \`dangerouslyDisableSandbox: true\` if:
1. The user *explicitly* asks to bypass sandbox, OR
2. A command just failed and you see evidence of sandbox restrictions causing the failure
- When analyzing failures:
- Commands can fail for many reasons unrelated to the sandbox (missing files, wrong arguments, network issues, etc.)
- Evidence of sandbox-caused failures includes:
- "Operation not permitted" errors for file/network operations
- Access denied to specific paths outside allowed directories
- Network connection failures to non-whitelisted hosts
- Unix socket connection errors
- Sandbox violation logs can be noisy, but clear permission errors are strong indicators
- When you see evidence of sandbox-caused failure:
- IMMEDIATELY retry with \`dangerouslyDisableSandbox: true\` (don't ask, just do it)
- Briefly explain what sandbox restriction likely caused the failure
- Mention: "Use \`/sandbox\` to manage restrictions"
- This will prompt the user for permission
- Example of normal usage: { "command": "ls", "description": "List files" }
- Example of override: { "command": "my-tool", "description": "Run my-tool", "dangerouslyDisableSandbox": true }
- DO NOT suggest adding sensitive paths like ~/.bashrc, ~/.zshrc, ~/.ssh/*, or credential files to the allowlist
+6 -6
View File
@@ -10,12 +10,12 @@ from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from src.shared.config import get_settings
from src.shared.logging import setup_logging, get_logger
from src.shared.exceptions import AppException
from src.shared.context import user_provider
from src.shared.auth import validate_api_key
from src.domains.router import root_router from src.domains.router import root_router
from src.shared.auth import validate_api_key
from src.shared.config import get_settings
from src.shared.context import user_provider
from src.shared.exceptions import AppException
from src.shared.logging import get_logger, setup_logging
settings = get_settings() settings = get_settings()
setup_logging(settings.log_level) setup_logging(settings.log_level)
@@ -44,7 +44,7 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title=settings.app_name, title=settings.app_name,
version=settings.app_version, version=settings.app_version,
description="Multi-Agent AI Development System", description=settings.app_description,
docs_url="/docs", docs_url="/docs",
redoc_url=None, redoc_url=None,
openapi_url="/openapi.json", openapi_url="/openapi.json",
+4
View File
@@ -0,0 +1,4 @@
"""Ollama integration for Webber."""
from src.ollama.provider import WebberOllamaProvider, get_ollama_provider
__all__ = ["WebberOllamaProvider", "get_ollama_provider"]

Some files were not shown because too many files have changed in this diff Show More