13 KiB
Multi-Agent System for Claude Code
A lean, pragmatic multi-agent system for software and data engineering tasks. Designed for small teams (1-2 people) building data products.
Philosophy
Simple, Direct, Procedural Code
- Functions over classes
- Data-oriented design
- Explicit over clever
- Solve actual problems, not general cases
Inspired by Casey Muratori and Jonathan Blow's approach to software development.
System Structure
agent_system/
├── README.md # This file
├── coding_philosophy.md # Core principles (reference for all agents)
├── orchestrator.md # Lead Engineer Agent
├── code_analysis_agent.md # Code exploration agent
├── implementation_agent.md # Code writing agent
└── testing_agent.md # Testing agent
Agents
1. Orchestrator (Lead Engineer Agent)
File: orchestrator.md
Role: Coordinates all work, decides when to use workers
Responsibilities:
- Analyze task complexity
- Decide: handle directly or spawn workers
- Create worker specifications
- Synthesize worker outputs
- Make architectural decisions
Use: This is your main agent for Claude Code
2. Code Analysis Agent
File: code_analysis_agent.md
Role: Explore and understand code (read-only)
Responsibilities:
- Map code structure
- Trace data flow
- Identify patterns and issues
- Answer specific questions about codebase
Use: When you need to understand existing code before making changes
3. Implementation Agent
File: implementation_agent.md
Role: Write simple, direct code
Responsibilities:
- Implement features
- Modify existing code
- Write SQLMesh models
- Create Robyn routes
- Build evidence.dev dashboards
Use: For building and modifying code
4. Testing Agent
File: testing_agent.md
Role: Verify code works correctly
Responsibilities:
- Write pytest tests
- Create SQL test queries
- Test data transformations
- Validate edge cases
Use: For creating test suites
How It Works
Decision Tree
Task received
↓
Can I do this directly in <30 tool calls?
↓
YES → Handle directly (90% of tasks)
NO → ↓
↓
Is this truly parallelizable?
↓
YES → Spawn 2-3 workers (10% of tasks)
NO → Handle directly anyway
Golden Rule: Most tasks should be handled directly by the orchestrator. Only use multiple agents when parallelization provides clear benefit.
Example: Simple Task (Direct)
User: "Add an API endpoint to get user activity"
Orchestrator: This is straightforward, <20 tool calls
↓
Handles directly:
- Creates route in src/routes/activity.py
- Queries data lake
- Returns JSON
- Tests manually
- Done
No workers needed. Fast and simple.
Example: Complex Task (Multi-Agent)
User: "Migrate ETL pipeline to SQLMesh"
Orchestrator: This is complex, will benefit from parallel work
↓
Phase 1 - Analysis:
Spawns Code Analysis Agent
- Maps existing pipeline
- Identifies transformations
- Documents dependencies
→ Writes to .agent_work/analysis/
↓
Phase 2 - Implementation:
Spawns 2 Implementation Agents in parallel
- Agent A: Extract models
- Agent B: Transform models
→ Both write to .agent_work/implementation/
↓
Phase 3 - Testing:
Spawns Testing Agent
- Validates output correctness
→ Writes to .agent_work/testing/
↓
Orchestrator synthesizes:
- Reviews all outputs
- Resolves conflicts
- Creates migration plan
- Done
Parallelization saves time on truly complex work.
Tech Stack
Data Engineering
- SQLMesh - Data transformation framework (SQL models)
- DuckDB - Analytics database (OLAP queries)
- Iceberg - Data lake table format (on R2 storage)
- ELT - Extract → Load → Transform (in warehouse)
SaaS Application
- Robyn - Python web framework
- Hosts landing page, auth, payment
- Serves evidence.dev build at
/dashboard/
- evidence.dev - BI dashboards (SQL + Markdown → static site)
Architecture
User → Robyn
├── / (landing, auth, payment)
├── /api/* (API endpoints)
└── /dashboard/* (evidence.dev build)
Working Directory
All agent work goes into .agent_work/ with feature-specific subdirectories:
project_root/
├── README.md # Architecture, setup, tech stack
├── CLAUDE.md # Memory: decisions, patterns, conventions
├── .agent_work/ # Agent work (add to .gitignore)
│ ├── feature-user-dashboard/ # Feature-specific directory
│ │ ├── project_state.md # Track this feature's progress
│ │ ├── analysis/
│ │ │ └── findings.md
│ │ ├── implementation/
│ │ │ ├── feature.py
│ │ │ └── notes.md
│ │ └── testing/
│ │ ├── test_feature.py
│ │ └── results.md
│ └── feature-payment-integration/ # Another feature
│ ├── project_state.md
│ ├── analysis/
│ ├── implementation/
│ └── testing/
├── models/ # SQLMesh models
├── src/ # Application code
└── tests/ # Final test suite
Workflow:
- New feature → Create branch:
git checkout -b feature-name - Create
.agent_work/feature-name/directory - Track progress in
.agent_work/feature-name/project_state.md - Update global context in
README.mdandCLAUDE.mdas needed
Global vs Feature Context:
- README.md: Current architecture, tech stack, how to run
- CLAUDE.md: Memory file - decisions, patterns, conventions to follow
- project_state.md: Feature-specific progress (in
.agent_work/feature-name/)
Why .agent_work/ instead of /tmp/:
- Persists across sessions
- Easy to review agent work
- Can reference with normal paths
- Keep or discard as needed
- Feature-scoped organization
Add to .gitignore:
.agent_work/
Usage in Claude Code
Setting Up
-
Copy agent system files to your project:
mkdir -p .claude/agents/ cp agent_system/* .claude/agents/ -
Add to
.gitignore:.agent_work/ -
Create
.agent_work/directory:mkdir -p .agent_work/{analysis,implementation,testing}
Using the Orchestrator
In Claude Code, load the orchestrator:
@orchestrator.md
[Your request here]
The orchestrator will:
- Analyze the task
- Decide if workers are needed
- Spawn workers if beneficial
- Handle directly if simple
- Synthesize results
- Deliver solution
When Workers Are Spawned
The orchestrator automatically reads the appropriate agent file when spawning:
Orchestrator reads: code_analysis_agent.md
↓
Creates specific task spec
↓
Spawns Code Analysis Agent with:
- Agent instructions (from file)
- Task specification
- Output location
↓
Worker executes independently
↓
Writes to .agent_work/analysis/
Coding Philosophy
All agents follow these principles (from coding_philosophy.md):
Core Principles
- Functions over classes - Use functions unless you truly need classes
- Data is data - Simple structures (dicts, lists), not objects hiding behavior
- Explicit over implicit - No magic, no hiding
- Simple control flow - Straightforward if/else, early returns
- Build minimum that works - Solve actual problem, not general case
What to Avoid
❌ Classes wrapping single functions
❌ Inheritance hierarchies
❌ Framework magic
❌ Over-abstraction "for future flexibility"
❌ Configuration as code pyramids
What to Do
✅ Write simple, direct functions
✅ Make data transformations obvious
✅ Handle errors explicitly
✅ Keep business logic in SQL when possible
✅ Think about performance
Examples
Example 1: Build Dashboard
Request: "Create dashboard showing user activity trends"
Orchestrator Decision: Moderate complexity, 2 independent tasks
Execution:
-
Setup:
- Create branch:
git checkout -b feature-user-dashboard - Create
.agent_work/feature-user-dashboard/ - Read
README.mdandCLAUDE.mdfor context
- Create branch:
-
Spawns Implementation Agent A
- Creates SQLMesh model (user_activity_daily.sql)
- Writes to
.agent_work/feature-user-dashboard/implementation-data/
-
Spawns Implementation Agent B (parallel)
- Creates evidence.dev dashboard
- Writes to
.agent_work/feature-user-dashboard/implementation-viz/
-
Orchestrator synthesizes
- Reviews both outputs
- Tests evidence build
- Deploys together
- Updates
.agent_work/feature-user-dashboard/project_state.md
Result: Working dashboard with data model
Example 2: Fix Bug
Request: "This query is timing out, fix it"
Orchestrator Decision: Simple, direct handling
Execution:
-
Setup:
- Create branch:
git checkout -b fix-query-timeout - Create
.agent_work/fix-query-timeout/
- Create branch:
-
Orchestrator handles directly
- Runs EXPLAIN ANALYZE
- Identifies missing index
- Creates index
- Tests performance
- Documents in
.agent_work/fix-query-timeout/project_state.md
Result: Query now fast, documented
Example 3: Large Refactor
Request: "Migrate 50 Python files from sync to async"
Orchestrator Decision: Complex, phased approach
Execution:
-
Phase 1: Analysis
- Code Analysis Agent maps dependencies
- Identifies blocking calls
- Writes to
.agent_work/analysis/
-
Phase 2: Implementation (parallel)
- Implementation Agent A: Core modules (20 files)
- Implementation Agent B: API routes (15 files)
- Implementation Agent C: Utils (15 files)
- All write to
.agent_work/implementation/
-
Phase 3: Testing
- Testing Agent validates async behavior
- Writes to
.agent_work/testing/
-
Orchestrator synthesizes
- Resolves conflicts
- Integration testing
- Migration plan
Result: Migrated codebase with tests
Best Practices
For Orchestrator
- Default to handling directly
- Spawn workers only for truly parallel work
- Give workers focused, non-overlapping tasks
- Use extended thinking for planning
- Document decisions in
project_state.md
For Worker Specs
Good:
AGENT: implementation
OBJECTIVE: Create SQLMesh model for user_activity_daily
SCOPE: Create models/user_activity_daily.sql
CONSTRAINTS: DuckDB SQL, incremental by date, partition by event_date
OUTPUT: .agent_work/implementation/models/
BUDGET: 20 tool calls
Bad:
AGENT: implementation
OBJECTIVE: Help with the data stuff
For Long Tasks
- Maintain
.agent_work/project_state.md - Update after each major phase
- Use compaction if approaching context limits
- Load files just-in-time (not entire codebase)
Context Management
Just-in-Time Loading
Don't load entire codebases:
# Good: Survey, then target
find models/ -name "*.sql" | head -10
rg "SELECT.*FROM" models/
cat models/specific_model.sql
# Bad: Load everything
cat models/*.sql
Project State Tracking
For long tasks (>50 turns), maintain state:
## Project: [Name]
## Phase: [Current]
### Completed
- [x] Task 1 - Agent - Outcome
### Current
- [ ] Task 2 - Agent - Status
### Decisions
1. Decision - Rationale
### Next Steps
1. Step 1
2. Step 2
Troubleshooting
"Workers are duplicating work"
Cause: Vague task boundaries
Fix: Be more specific, assign non-overlapping files
"Coordination overhead too high"
Cause: Task not parallelizable
Fix: Handle directly, don't use workers
"Context window exceeded"
Cause: Loading too much data
Fix: Use JIT loading, summarize outputs
"Workers stepping on each other"
Cause: Overlapping responsibilities
Fix: Separate by file/module, clear boundaries
Summary
System:
- 4 agents: Orchestrator + 3 workers
- Orchestrator handles most tasks directly (90%)
- Workers used for truly complex, parallelizable work (10%)
Philosophy:
- Simple, direct, procedural code
- Data-oriented design
- Functions over classes
- Build minimum that works
Tech Stack:
- Data: SQLMesh, DuckDB, Iceberg, ELT
- SaaS: Robyn, evidence.dev
- Testing: pytest, DuckDB SQL tests
Working Directory:
.agent_work/for all agent outputs- Add to
.gitignore - Review, then move to final locations
Golden Rule: When in doubt, go simpler. Most tasks don't need multiple agents.
Getting Started
- Read
coding_philosophy.mdto understand principles - Use
orchestrator.mdas your main agent in Claude Code - Let orchestrator decide when to spawn workers
- Review outputs in
.agent_work/ - Iterate based on results
Start simple. Add complexity only when needed.