bots-and-co/PLAN.md
2025-11-27 10:40:00 +01:00

41 KiB

Agent Orchestra

A multi-agent system where Claude-powered agents collaborate via Slack, each running in isolated Docker containers with role-based permissions and tools.


Claude Agent SDK Integration Notes

Key SDK Concepts

  1. Two APIs:

    • query() - One-shot, creates new session each time
    • ClaudeSDKClient - Persistent session, maintains conversation history

    We use ClaudeSDKClient because agents need to remember context across messages.

  2. Built-in Tools: The SDK includes Claude Code's tools:

    • Read, Write, Edit - File operations
    • Bash - Shell commands
    • Glob, Grep - File search
    • WebSearch, WebFetch - Web access
    • Task - Subagent spawning
    • NotebookEdit - Jupyter notebooks
  3. Custom Tools via SDK MCP Servers:

    • Use @tool decorator and create_sdk_mcp_server()
    • Runs in-process (no subprocess overhead)
    • Tools are prefixed: mcp__<server>__<tool>
  4. Hooks for Permissions:

    • PreToolUse - Check before tool execution, can deny
    • PostToolUse - Log/audit after execution
    • Defined via HookMatcher in ClaudeAgentOptions
  5. Permission Modes:

    • "default" - Standard permission behavior
    • "acceptEdits" - Auto-accept file edits
    • "bypassPermissions" - Skip all checks (use with caution)

Architecture Decision: Orchestrator vs. Per-Container SDK

Option A: Orchestrator runs SDK (chosen)

┌─────────────────────────────────────────┐
│            Orchestrator                  │
│  - Runs ClaudeSDKClient per agent       │
│  - Routes Slack messages                 │
│  - Agents execute via SDK tools         │
│    (Bash, Write, etc. run in container) │
└─────────────────────────────────────────┘
         │ docker exec
         ▼
┌─────────────────┐
│ Agent Container │
│ - Workspace     │
│ - Git clone     │
│ - No SDK here   │
└─────────────────┘

Option B: SDK runs inside each container

┌─────────────────┐     ┌─────────────────┐
│ Agent Container │     │ Agent Container │
│ - SDK Client    │     │ - SDK Client    │
│ - Claude CLI    │     │ - Claude CLI    │
│ - Node.js       │     │ - Node.js       │
└─────────────────┘     └─────────────────┘

We choose Option A because:

  • Simpler container images (no Node.js/CLI required)
  • Centralized orchestration and routing
  • SDK's built-in tools (Bash, Write) can still operate on container filesystems via mounted volumes
  • Easier to manage API keys and rate limits

Setting Up SDK with Agent Options

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, tool, create_sdk_mcp_server

# Create custom tools
@tool("slack_send", "Send Slack message", {"channel": str, "text": str})
async def slack_send(args):
    await slack_client.chat_postMessage(
        channel=args["channel"], 
        text=args["text"]
    )
    return {"content": [{"type": "text", "text": "Sent"}]}

# Create MCP server
tools_server = create_sdk_mcp_server(
    name="agent-tools",
    tools=[slack_send]
)

# Configure agent
options = ClaudeAgentOptions(
    system_prompt=agent_config.system_prompt,
    model="sonnet",
    
    # Built-in tools
    allowed_tools=["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
    disallowed_tools=["Task"],  # Don't allow spawning subagents
    
    # Custom tools via MCP
    mcp_servers={"tools": tools_server},
    
    # Permission mode
    permission_mode="acceptEdits",
    
    # Working directory (mapped to container volume)
    cwd="/data/workspaces/developer",
    
    # Hooks for fine-grained control
    hooks={
        "PreToolUse": [HookMatcher(hooks=[permission_checker])]
    }
)

# Create persistent client
async with ClaudeSDKClient(options) as client:
    # Send message (client remembers context)
    await client.query("Create a hello.py file")
    async for msg in client.receive_response():
        handle_message(msg)
    
    # Follow-up (same session, remembers previous)
    await client.query("Add a main function")
    async for msg in client.receive_response():
        handle_message(msg)

Project Overview

Goal: Build a server that orchestrates multiple async Claude agents (using Claude Agent SDK) that communicate through Slack, manage projects together, and can delegate work to each other.

Key Concepts:

  • Agents defined via YAML (roles, tools, permissions, model)
  • Slack as the communication layer (threads, channels, mentions)
  • Each agent runs in its own persistent Docker container
  • Shared git repo with branch-based access control
  • Simple JSON-based project/task management
  • CEO agent as supervisor/orchestrator

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Host Machine                              │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    Orchestrator Service                      ││
│  │  - Slack event listener (Socket Mode)                       ││
│  │  - Agent lifecycle management                               ││
│  │  - Message routing                                          ││
│  │  - MCP server exposure                                      ││
│  └─────────────────────────────────────────────────────────────┘│
│                              │                                   │
│         ┌────────────────────┼────────────────────┐             │
│         ▼                    ▼                    ▼             │
│  ┌─────────────┐     ┌─────────────┐      ┌─────────────┐       │
│  │  CEO Agent  │     │  PM Agent   │      │  Dev Agent  │       │
│  │  Container  │     │  Container  │      │  Container  │       │
│  │             │     │             │      │             │       │
│  │ - memory/   │     │ - memory/   │      │ - memory/   │       │
│  │ - tools     │     │ - tools     │      │ - tools     │       │
│  │ - git clone │     │ - git clone │      │ - git clone │       │
│  └─────────────┘     └─────────────┘      └─────────────┘       │
│         │                    │                    │             │
│         └────────────────────┼────────────────────┘             │
│                              ▼                                   │
│                    ┌─────────────────┐                          │
│                    │  Shared Volume  │                          │
│                    │  - /repos/      │                          │
│                    │  - /projects/   │                          │
│                    │  - /memory/     │                          │
│                    └─────────────────┘                          │
└─────────────────────────────────────────────────────────────────┘

Directory Structure

agent-orchestra/
├── CLAUDE.md                    # Claude Code instructions
├── README.md
├── pyproject.toml
├── docker-compose.yml
├── Dockerfile.orchestrator      # Main service (runs SDK)
├── Dockerfile.workspace         # Lightweight workspace containers
│
├── config/
│   ├── orchestra.yml            # Global config (Slack, Gitea, Docker)
│   └── agents/
│       ├── ceo.yml
│       ├── product_manager.yml
│       ├── developer.yml
│       ├── tech_lead.yml
│       └── designer.yml
│
├── src/
│   └── orchestra/
│       ├── __init__.py
│       ├── main.py              # Entry point
│       │
│       ├── core/
│       │   ├── __init__.py
│       │   ├── orchestrator.py  # Main service, Slack listener
│       │   ├── agent.py         # ClaudeSDKClient wrapper
│       │   ├── config.py        # YAML config → Pydantic models
│       │   ├── container.py     # Docker workspace management
│       │   └── permissions.py   # PreToolUse hooks
│       │
│       ├── slack/
│       │   ├── __init__.py
│       │   ├── client.py        # Slack API wrapper
│       │   ├── events.py        # Event handlers, routing
│       │   └── formatter.py     # Message formatting
│       │
│       ├── tools/
│       │   ├── __init__.py
│       │   ├── server.py        # create_sdk_mcp_server setup
│       │   ├── slack_tools.py   # @tool decorated Slack operations
│       │   ├── task_tools.py    # @tool decorated task management
│       │   ├── gitea_tools.py   # @tool decorated Gitea/PR operations
│       │   └── gitea_client.py  # Gitea API client (httpx)
│       │
│       └── memory/
│           ├── __init__.py
│           └── manager.py       # Memory file read/write
│
├── data/                        # Mounted volumes
│   ├── workspaces/              # Per-agent working directories
│   │   ├── developer/
│   │   ├── product_manager/
│   │   └── tech_lead/
│   ├── repos/                   # Shared git repository clone
│   ├── projects/                # Project/task JSON files
│   │   └── boards.json
│   └── memory/                  # Agent memory folders
│       ├── ceo/
│       │   └── memory.md
│       ├── product_manager/
│       │   └── memory.md
│       └── developer/
│           └── memory.md
│
└── tests/
    ├── __init__.py
    ├── test_agent.py
    ├── test_tools.py
    ├── test_permissions.py
    └── test_slack.py

Configuration Schema

Global Config (config/orchestra.yml)

slack:
  app_token: "${SLACK_APP_TOKEN}"
  bot_token: "${SLACK_BOT_TOKEN}"
  default_channel: "general"

git:
  provider: "gitea"
  base_url: "${GITEA_URL}"          # e.g., https://git.example.com
  api_token: "${GITEA_API_TOKEN}"
  repo_owner: "${GITEA_REPO_OWNER}"
  repo_name: "${GITEA_REPO_NAME}"
  main_branch: "main"
  dev_branch: "dev"

docker:
  network: "orchestra-net"
  base_image: "agent-orchestra:latest"

mcp:
  expose_port: 8080
  external_servers:
    - name: "filesystem"
      url: "stdio://mcp-filesystem"

CEO Agent Config (config/agents/ceo.yml)

id: ceo
name: "CEO Chris"
slack_user_id: "${SLACK_CEO_USER_ID}"

model: "sonnet"
max_turns: 100

system_prompt: |
  You are CEO Chris, the supervisor of the agent team.
  You have broad oversight and can step in when needed.
  
  Your responsibilities:
  - Monitor team progress and intervene if blocked
  - Make high-level decisions when there's disagreement
  - Ensure quality standards are maintained
  - Approve major architectural decisions
  - Merge approved changes to main branch
  
  You generally let the team operate autonomously but
  provide guidance when asked or when you notice issues.
  
  You have access to all tools and can perform any operation.

# All tools allowed
allowed_tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
  - WebSearch
  - WebFetch
  - Task
  # All Slack tools
  - mcp__agent__slack_send_message
  - mcp__agent__slack_reply_thread
  - mcp__agent__slack_create_channel
  - mcp__agent__slack_delete_channel
  - mcp__agent__slack_invite_to_channel
  # All task tools
  - mcp__agent__tasks_create_project
  - mcp__agent__tasks_create_board
  - mcp__agent__tasks_create
  - mcp__agent__tasks_assign
  - mcp__agent__tasks_update_status
  - mcp__agent__tasks_view
  - mcp__agent__tasks_delete
  # All git tools
  - mcp__agent__gitea_create_pr
  - mcp__agent__gitea_list_prs
  - mcp__agent__gitea_get_pr
  - mcp__agent__gitea_merge_pr
  - mcp__agent__gitea_add_review
  - mcp__agent__gitea_merge_to_main

disallowed_tools: []  # CEO has no restrictions

permissions:
  filesystem:
    allowed_paths:
      - "/**"
  git:
    can_push_to:
      - "*"
    can_merge_to:
      - "main"
      - "dev"

memory:
  path: "/memory/ceo"
  files:
    - "memory.md"

can_mention:
  - "*"

can_delegate_to:
  - "*"

container:
  resources:
    memory: "2g"
    cpus: "1.0"

Tech Lead Config (config/agents/tech_lead.yml)

id: tech_lead
name: "Tech Lead Terry"
slack_user_id: "${SLACK_TECHLEAD_USER_ID}"

model: "sonnet"
max_turns: 50

system_prompt: |
  You are Tech Lead Terry, responsible for code quality and architecture.
  
  Your responsibilities:
  - Review PRs from developers
  - Make architectural decisions
  - Ensure code quality and test coverage
  - Mentor developers on best practices
  - Approve PRs for merge to dev
  
  When reviewing code:
  1. Check for correctness and edge cases
  2. Verify tests exist and are meaningful
  3. Look for security issues
  4. Ensure code follows project conventions
  5. Provide constructive feedback

allowed_tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
  - WebSearch
  - WebFetch
  # Slack tools
  - mcp__agent__slack_send_message
  - mcp__agent__slack_reply_thread
  # Task tools
  - mcp__agent__tasks_view
  - mcp__agent__tasks_update_status
  - mcp__agent__tasks_create  # Can create tech debt tasks
  - mcp__agent__tasks_add_comment
  # Git tools - can review but not merge
  - mcp__agent__gitea_list_prs
  - mcp__agent__gitea_get_pr
  - mcp__agent__gitea_add_review
  - mcp__agent__gitea_add_comment

disallowed_tools:
  - mcp__agent__gitea_merge_pr
  - mcp__agent__slack_create_channel

permissions:
  filesystem:
    allowed_paths:
      - "/repos/**"
      - "/workspace/**"
  git:
    can_push_to: []  # Tech lead reviews, doesn't push
    can_merge_to: []  # Only PM merges

memory:
  path: "/memory/tech_lead"
  files:
    - "memory.md"

can_mention:
  - developer
  - product_manager
  - ceo

Agent Config (config/agents/developer.yml)

id: developer
name: "Dev Dana"
slack_user_id: "${SLACK_DEV_USER_ID}"  # Bot user for this agent

# Claude Agent SDK settings
model: "sonnet"  # sonnet, opus, haiku
max_turns: 50

system_prompt: |
  You are Dev Dana, a skilled software developer working in a team.
  You write clean, well-tested code. You work on feature branches
  and create PRs for review. You communicate progress in Slack.
  
  When assigned a task, you:
  1. Understand requirements fully (ask PM if unclear)
  2. Create a feature branch from dev
  3. Implement the solution with tests
  4. Create a PR and notify tech_lead for review
  
  You have access to the shared repository at /repos/main.
  
  Use the Slack tools to communicate with your team.
  Use the task tools to update your task status.

# Built-in Claude Code tools (Read, Write, Edit, Bash, Glob, Grep, WebSearch, etc.)
allowed_tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
  - WebSearch
  - WebFetch
  # Custom MCP tools (prefixed with mcp__<server>__)
  - mcp__agent__slack_send_message
  - mcp__agent__slack_reply_thread
  - mcp__agent__tasks_view
  - mcp__agent__tasks_update_status
  - mcp__agent__gitea_create_pr
  - mcp__agent__gitea_list_prs

disallowed_tools:
  - mcp__agent__slack_create_channel
  - mcp__agent__slack_delete_channel
  - mcp__agent__tasks_create_project
  - mcp__agent__gitea_merge_pr

# Permission hooks configuration
permissions:
  filesystem:
    allowed_paths:
      - "/repos/**"
      - "/workspace/**"
    denied_paths:
      - "/repos/.git/config"
      - "**/.env"
      - "**/secrets/**"
  
  git:
    branch_pattern: "feature/*"
    can_push_to:
      - "feature/*"
    cannot_push_to:
      - "main"
      - "dev"

memory:
  path: "/memory/developer"
  files:
    - "memory.md"

can_mention:
  - tech_lead
  - product_manager
  - ceo

container:
  resources:
    memory: "2g"
    cpus: "1.0"
  environment:
    - "PYTHONPATH=/workspace"

Product Manager Config (config/agents/product_manager.yml)

id: product_manager
name: "PM Paula"
slack_user_id: "${SLACK_PM_USER_ID}"

model: "sonnet"
max_turns: 50

system_prompt: |
  You are PM Paula, a product manager coordinating the team.
  You translate requirements into actionable tasks, create project
  channels, and ensure smooth delivery.
  
  When given a new feature request:
  1. Break it down into tasks
  2. Create a project channel if needed
  3. Assign tasks to appropriate team members
  4. Track progress and remove blockers
  
  You can merge approved PRs to the dev branch.
  You cannot merge to main - that requires CEO approval.

allowed_tools:
  - Read
  - Write
  - Glob
  - Grep
  - Bash
  - WebSearch
  - WebFetch
  # Slack tools
  - mcp__agent__slack_send_message
  - mcp__agent__slack_reply_thread
  - mcp__agent__slack_create_channel
  - mcp__agent__slack_invite_to_channel
  # Task tools
  - mcp__agent__tasks_create_project
  - mcp__agent__tasks_create_board
  - mcp__agent__tasks_create
  - mcp__agent__tasks_assign
  - mcp__agent__tasks_update_status
  - mcp__agent__tasks_view
  # Git/Gitea tools
  - mcp__agent__gitea_list_prs
  - mcp__agent__gitea_get_pr
  - mcp__agent__gitea_merge_pr
  - mcp__agent__gitea_add_review

disallowed_tools:
  - mcp__agent__gitea_push_to_main

permissions:
  filesystem:
    allowed_paths:
      - "/repos/**"
      - "/workspace/**"
      - "/projects/**"
  
  git:
    can_merge_to:
      - "dev"
    cannot_merge_to:
      - "main"

memory:
  path: "/memory/product_manager"
  files:
    - "memory.md"

can_mention:
  - developer
  - tech_lead
  - designer
  - ceo

can_delegate_to:
  - developer
  - designer

Core Components

1. Orchestrator (src/orchestra/core/orchestrator.py)

"""
Main orchestrator that:
- Loads agent configs from YAML
- Manages agent container lifecycle
- Routes Slack messages to appropriate agents
- Handles agent-to-agent communication
"""

class Orchestrator:
    async def start(self) -> None: ...
    async def stop(self) -> None: ...
    async def route_message(self, event: SlackEvent) -> None: ...
    async def spawn_agent(self, agent_id: str) -> AgentContainer: ...
    async def get_agent(self, agent_id: str) -> Agent: ...

2. Agent (src/orchestra/core/agent.py)

Uses ClaudeSDKClient from the Claude Agent SDK for persistent conversation sessions. Each agent maintains its own client instance for continuous context.

"""
Wraps Claude Agent SDK with:
- Persistent conversation via ClaudeSDKClient
- Custom tools via SDK MCP servers (in-process)
- Memory management
- Slack integration
"""
from claude_agent_sdk import (
    ClaudeSDKClient, 
    ClaudeAgentOptions,
    tool,
    create_sdk_mcp_server,
    AssistantMessage,
    TextBlock,
    ToolUseBlock,
    HookMatcher
)

class Agent:
    id: str
    config: AgentConfig
    client: ClaudeSDKClient  # Persistent session
    memory: MemoryManager
    
    async def start(self) -> None:
        """Initialize the ClaudeSDKClient with agent-specific options."""
        # Create in-process MCP server for custom tools
        tools_server = create_sdk_mcp_server(
            name=f"{self.id}-tools",
            version="1.0.0",
            tools=self._build_tools()
        )
        
        options = ClaudeAgentOptions(
            system_prompt=self.config.system_prompt,
            model=self.config.model,  # e.g., "sonnet", "opus"
            allowed_tools=self.config.allowed_tools,
            disallowed_tools=self.config.disallowed_tools,
            mcp_servers={
                "agent": tools_server,
                # External MCP servers for git, etc.
                "gitea": {
                    "type": "stdio",
                    "command": "gitea-mcp-server",
                    "args": ["--url", self.config.gitea_url]
                }
            },
            permission_mode="acceptEdits",  # Auto-accept in containers
            cwd=f"/workspace/{self.id}",
            hooks=self._build_hooks()
        )
        
        self.client = ClaudeSDKClient(options)
        await self.client.connect()
    
    async def process_message(self, message: str, context: dict) -> str:
        """Send message and collect response. Client maintains conversation history."""
        await self.client.query(message)
        
        response_text = ""
        async for msg in self.client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        response_text += block.text
        
        return response_text
    
    async def stop(self) -> None:
        """Disconnect the client."""
        await self.client.disconnect()
    
    def _build_tools(self) -> list:
        """Build custom tools based on agent config permissions."""
        tools = []
        
        if "slack" in self.config.tools:
            tools.extend(self._create_slack_tools())
        if "tasks" in self.config.tools:
            tools.extend(self._create_task_tools())
        
        return tools
    
    def _build_hooks(self) -> dict:
        """Build permission hooks to enforce tool restrictions."""
        async def check_permissions(input_data, tool_use_id, context):
            tool_name = input_data.get("tool_name")
            tool_input = input_data.get("tool_input", {})
            
            # Check git branch restrictions
            if tool_name == "Bash" and "git push" in tool_input.get("command", ""):
                branch = self._extract_branch(tool_input["command"])
                if not self._can_push_to_branch(branch):
                    return {
                        "hookSpecificOutput": {
                            "hookEventName": "PreToolUse",
                            "permissionDecision": "deny",
                            "permissionDecisionReason": f"Cannot push to {branch}"
                        }
                    }
            return {}
        
        return {
            "PreToolUse": [HookMatcher(hooks=[check_permissions])]
        }

3. Custom Tools with SDK MCP Server

The Claude Agent SDK supports in-process MCP servers, eliminating subprocess overhead.

"""
Custom tools implemented as SDK MCP servers.
These run in-process for better performance.
"""
from claude_agent_sdk import tool, create_sdk_mcp_server
from typing import Any

# Slack Tools
@tool("slack_send_message", "Send a message to a Slack channel or thread", {
    "channel": str,
    "text": str,
    "thread_ts": str  # Optional, for threading
})
async def slack_send_message(args: dict[str, Any]) -> dict[str, Any]:
    # Implementation uses slack_sdk
    channel = args["channel"]
    text = args["text"]
    thread_ts = args.get("thread_ts")
    
    result = await slack_client.chat_postMessage(
        channel=channel,
        text=text,
        thread_ts=thread_ts
    )
    
    return {
        "content": [{
            "type": "text",
            "text": f"Message sent to {channel}"
        }]
    }

@tool("slack_create_channel", "Create a new Slack channel", {
    "name": str,
    "description": str
})
async def slack_create_channel(args: dict[str, Any]) -> dict[str, Any]:
    result = await slack_client.conversations_create(
        name=args["name"]
    )
    return {
        "content": [{
            "type": "text",
            "text": f"Created channel #{args['name']}"
        }]
    }

# Task Management Tools
@tool("tasks_create", "Create a new task", {
    "title": str,
    "description": str,
    "assignee": str,
    "board_id": str
})
async def tasks_create(args: dict[str, Any]) -> dict[str, Any]:
    task = await task_manager.create_task(
        title=args["title"],
        description=args["description"],
        assignee=args["assignee"],
        board_id=args["board_id"]
    )
    return {
        "content": [{
            "type": "text",
            "text": f"Created task {task['id']}: {task['title']}"
        }]
    }

@tool("tasks_list", "List tasks with optional filters", {
    "board_id": str,
    "status": str,  # Optional: pending, in_progress, done
    "assignee": str  # Optional
})
async def tasks_list(args: dict[str, Any]) -> dict[str, Any]:
    tasks = await task_manager.list_tasks(**args)
    return {
        "content": [{
            "type": "text",
            "text": json.dumps(tasks, indent=2)
        }]
    }

# Gitea Tools (PR management)
@tool("gitea_create_pr", "Create a pull request", {
    "title": str,
    "body": str,
    "head": str,  # Source branch
    "base": str   # Target branch (usually "dev")
})
async def gitea_create_pr(args: dict[str, Any]) -> dict[str, Any]:
    pr = await gitea_client.create_pull_request(**args)
    return {
        "content": [{
            "type": "text",
            "text": f"Created PR #{pr['number']}: {pr['title']}"
        }]
    }

@tool("gitea_merge_pr", "Merge a pull request", {
    "pr_number": int,
    "merge_style": str  # merge, rebase, squash
})
async def gitea_merge_pr(args: dict[str, Any]) -> dict[str, Any]:
    result = await gitea_client.merge_pull_request(
        args["pr_number"],
        args["merge_style"]
    )
    return {
        "content": [{
            "type": "text",
            "text": f"Merged PR #{args['pr_number']}"
        }]
    }

# Create server with all tools
def create_agent_tools_server(agent_config: AgentConfig):
    """Create MCP server with tools filtered by agent permissions."""
    all_tools = {
        "slack_send_message": slack_send_message,
        "slack_create_channel": slack_create_channel,
        "tasks_create": tasks_create,
        "tasks_list": tasks_list,
        "gitea_create_pr": gitea_create_pr,
        "gitea_merge_pr": gitea_merge_pr,
    }
    
    # Filter based on agent's allowed operations
    allowed = []
    for tool_name, tool_fn in all_tools.items():
        if agent_config.can_use_tool(tool_name):
            allowed.append(tool_fn)
    
    return create_sdk_mcp_server(
        name=f"{agent_config.id}-tools",
        version="1.0.0",
        tools=allowed
    )

4. Container Manager (src/orchestra/core/container.py)

"""
Docker container lifecycle management:
- Build agent images
- Start/stop containers
- Volume mounting
- Network configuration
"""

class ContainerManager:
    async def create_container(self, agent_config: AgentConfig) -> Container: ...
    async def start_container(self, container_id: str) -> None: ...
    async def stop_container(self, container_id: str) -> None: ...
    async def exec_in_container(self, container_id: str, cmd: str) -> str: ...

Tool Implementations

Tasks Tool (src/orchestra/tools/tasks.py)

"""
JSON-based project management.
File: /projects/boards.json
"""

# Schema
{
  "projects": {
    "project-123": {
      "id": "project-123",
      "name": "Company Website",
      "channel": "proj-website",
      "created_by": "product_manager",
      "created_at": "2025-01-15T10:00:00Z"
    }
  },
  "boards": {
    "board-456": {
      "id": "board-456",
      "project_id": "project-123",
      "name": "Sprint 1",
      "columns": ["todo", "in_progress", "review", "done"]
    }
  },
  "tasks": {
    "task-789": {
      "id": "task-789",
      "board_id": "board-456",
      "title": "Implement homepage",
      "description": "Create responsive homepage with hero section",
      "status": "in_progress",
      "assignee": "developer",
      "created_by": "product_manager",
      "branch": "feature/task-789",
      "pr_url": null,
      "comments": [
        {
          "author": "developer",
          "text": "Started working on this",
          "timestamp": "2025-01-15T11:00:00Z"
        }
      ]
    }
  }
}

Git Tool (src/orchestra/tools/git.py)

"""
Git operations with permission checking.
Enforces branch patterns and operation restrictions.
Uses Gitea API for PR operations.
"""

class GitTool:
    # Local git operations
    async def checkout_branch(self, branch: str) -> str: ...
    async def create_branch(self, branch: str, from_ref: str = "dev") -> str: ...
    async def commit(self, message: str, files: list[str]) -> str: ...
    async def push(self, branch: str) -> str: ...
    async def pull(self, branch: str) -> str: ...
    async def status(self) -> str: ...
    async def diff(self, ref: str = "HEAD") -> str: ...
    
    # Gitea API operations
    async def create_pr(self, title: str, body: str, head: str, base: str = "dev") -> dict: ...
    async def list_prs(self, state: str = "open") -> list[dict]: ...
    async def get_pr(self, pr_number: int) -> dict: ...
    async def add_pr_comment(self, pr_number: int, body: str) -> dict: ...
    async def add_pr_review(self, pr_number: int, body: str, approved: bool) -> dict: ...
    async def merge_pr(self, pr_number: int, merge_style: str = "merge") -> dict: ...  # Permission checked


class GiteaClient:
    """
    Async client for Gitea API.
    Docs: https://docs.gitea.com/development/api-usage
    """
    
    def __init__(self, base_url: str, token: str, owner: str, repo: str):
        self.base_url = base_url.rstrip("/")
        self.token = token
        self.owner = owner
        self.repo = repo
    
    async def create_pull_request(
        self, title: str, body: str, head: str, base: str
    ) -> dict:
        """POST /repos/{owner}/{repo}/pulls"""
        ...
    
    async def get_pull_request(self, number: int) -> dict:
        """GET /repos/{owner}/{repo}/pulls/{index}"""
        ...
    
    async def merge_pull_request(
        self, number: int, merge_style: str = "merge"  # merge, rebase, squash
    ) -> dict:
        """POST /repos/{owner}/{repo}/pulls/{index}/merge"""
        ...
    
    async def create_review(
        self, number: int, body: str, event: str  # APPROVE, REQUEST_CHANGES, COMMENT
    ) -> dict:
        """POST /repos/{owner}/{repo}/pulls/{index}/reviews"""
        ...

Slack Integration

Event Flow

Slack Event (message/mention)
         │
         ▼
┌─────────────────┐
│  Event Handler  │  ← Parses mentions, extracts thread context
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│    Router       │  ← Determines which agent(s) should respond
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  Agent Queue    │  ← Each agent has async message queue
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  Agent Process  │  ← Claude SDK processes, may invoke tools
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  Slack Response │  ← Posts reply in thread, may mention others
└─────────────────┘

Message Format

Agents post messages with a consistent format:

🤖 *Dev Dana*
> Working on task-789: Implement homepage
> 
> Created branch `feature/task-789` and started implementation.
> Will have a PR ready for review in ~2 hours.
> 
> @tech_lead heads up, will need your review later

Memory System

Structure

/memory/
├── ceo/
│   └── memory.md           # CEO's persistent memory
├── product_manager/
│   └── memory.md           # PM's persistent memory
├── developer/
│   ├── memory.md           # General memory
│   ├── project-website.md  # Project-specific (future)
│   └── learnings.md        # Technical learnings (future)
└── tech_lead/
    └── memory.md

Memory Format (memory.md)

# Agent Memory - Dev Dana

## Active Context
- Currently working on: Company Website project
- Current branch: feature/task-789
- Pending PR: None

## Project Knowledge
### Company Website
- Tech stack: React, Tailwind, Next.js
- Design system: Uses shadcn/ui components
- PM: Paula, Tech Lead: Terry

## Team Preferences
- Paula prefers detailed task breakdowns
- Terry wants tests for all new features
- Code style: Prefer functional components

## Learnings
- 2025-01-15: This repo uses pnpm, not npm
- 2025-01-14: API endpoints are in /api folder

## Recent Decisions
- Using Next.js App Router (agreed with Terry)

Implementation Phases

Phase 1: Foundation (MVP)

Goal: Single agent responding to Slack messages

  1. Set up project structure with pyproject.toml
  2. Implement config loading (YAML → Pydantic models)
  3. Basic Slack integration (listen to events, post messages)
  4. Wrap Claude Agent SDK in Agent class
  5. Simple filesystem tool
  6. Memory loading/saving
  7. Basic Docker container for one agent

Deliverable: Can @mention one agent in Slack and get responses

Phase 2: Multi-Agent

Goal: Multiple agents running and communicating

  1. Container manager for multiple agents
  2. Message routing based on mentions
  3. Agent-to-agent communication via Slack
  4. Thread context preservation
  5. All agents defined and running

Deliverable: Can have PM delegate to Developer via Slack

Phase 3: Tools & Permissions

Goal: Full toolset with permission enforcement

  1. Tool registry with permission checking
  2. Git tool implementation
  3. Tasks/project management tool
  4. Code execution tool
  5. Web search tool
  6. Slack tools (create_channel, etc.)

Deliverable: Agents can do real work with proper restrictions

Phase 4: Workflow & Polish

Goal: Smooth end-to-end workflows

  1. PR creation and review flow
  2. Task lifecycle (create → assign → progress → complete)
  3. CEO supervision capabilities
  4. Better memory management
  5. Error handling and recovery
  6. MCP server exposure

Deliverable: Full workflow from request → implementation → review → merge

Phase 5: Production Ready

Goal: Deployable and maintainable

  1. Docker Compose setup
  2. Health checks and monitoring
  3. Logging and observability
  4. Configuration validation
  5. Documentation
  6. Integration tests

Docker Compose Structure

version: '3.8'

services:
  orchestrator:
    build:
      context: .
      dockerfile: Dockerfile.orchestrator
    environment:
      - SLACK_APP_TOKEN=${SLACK_APP_TOKEN}
      - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - GITEA_URL=${GITEA_URL}
      - GITEA_API_TOKEN=${GITEA_API_TOKEN}
    volumes:
      - ./config:/app/config:ro
      - ./data:/app/data
      - /var/run/docker.sock:/var/run/docker.sock  # For container management
    networks:
      - orchestra-net
    depends_on:
      - workspace-ceo
      - workspace-pm
      - workspace-dev
      - workspace-techlead

  # Lightweight workspace containers (no SDK, just filesystem)
  workspace-ceo:
    build:
      context: .
      dockerfile: Dockerfile.workspace
    volumes:
      - ./data/workspaces/ceo:/workspace
      - ./data/repos:/repos
      - ./data/memory/ceo:/memory
      - ./data/projects:/projects
    networks:
      - orchestra-net

  workspace-pm:
    build:
      context: .
      dockerfile: Dockerfile.workspace
    volumes:
      - ./data/workspaces/product_manager:/workspace
      - ./data/repos:/repos
      - ./data/memory/product_manager:/memory
      - ./data/projects:/projects
    networks:
      - orchestra-net

  workspace-dev:
    build:
      context: .
      dockerfile: Dockerfile.workspace
    volumes:
      - ./data/workspaces/developer:/workspace
      - ./data/repos:/repos
      - ./data/memory/developer:/memory
    networks:
      - orchestra-net

  workspace-techlead:
    build:
      context: .
      dockerfile: Dockerfile.workspace
    volumes:
      - ./data/workspaces/tech_lead:/workspace
      - ./data/repos:/repos:ro  # Read-only for tech lead
      - ./data/memory/tech_lead:/memory
    networks:
      - orchestra-net

volumes:
  orchestra-data:

networks:
  orchestra-net:
    driver: bridge

Dockerfile.orchestrator

FROM python:3.12-slim

# Install Node.js (required for Claude Agent SDK)
RUN apt-get update && apt-get install -y \
    curl \
    git \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && rm -rf /var/lib/apt/lists/*

# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code

# Install Python dependencies
WORKDIR /app
COPY pyproject.toml .
RUN pip install uv && uv sync

COPY src/ ./src/
COPY config/ ./config/

CMD ["uv", "run", "python", "-m", "orchestra.main"]

Dockerfile.workspace

FROM python:3.12-slim

# Lightweight container with common dev tools
RUN apt-get update && apt-get install -y \
    git \
    curl \
    jq \
    && rm -rf /var/lib/apt/lists/*

# Create workspace structure
RUN mkdir -p /workspace /repos /memory

WORKDIR /workspace

# Keep container running (orchestrator executes commands via docker exec)
CMD ["tail", "-f", "/dev/null"]

CLAUDE.md (for Claude Code)

# Agent Orchestra

Multi-agent system with Claude agents communicating via Slack.
Uses the Claude Agent SDK (claude-agent-sdk) for agent execution.

## Quick Start
```bash
# Prerequisites: Node.js, Claude Code CLI
npm install -g @anthropic-ai/claude-code

# Install Python dependencies
uv sync
cp .env.example .env  # Fill in ANTHROPIC_API_KEY, Slack tokens, Gitea tokens
uv run python -m orchestra.main

Architecture

  • Orchestrator: Routes Slack messages to agents, manages lifecycle
  • Agents: Each agent uses ClaudeSDKClient for persistent conversations
  • Tools: Built-in (Read/Write/Bash) + custom MCP tools (Slack/Tasks/Gitea)
  • Permissions: PreToolUse hooks enforce agent-specific restrictions
  • Containers: Each agent has isolated Docker container for workspace

Claude Agent SDK Usage

We use ClaudeSDKClient (not query()) because agents need persistent conversation context. Key patterns:

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async with ClaudeSDKClient(options) as client:
    await client.query(message)
    async for msg in client.receive_response():
        process(msg)

Custom tools use @tool decorator and create_sdk_mcp_server().

Key Files

  • config/orchestra.yml - Global config (Slack, Gitea, Docker)
  • config/agents/*.yml - Agent definitions (tools, permissions, prompts)
  • src/orchestra/core/orchestrator.py - Message routing, lifecycle
  • src/orchestra/core/agent.py - ClaudeSDKClient wrapper
  • src/orchestra/tools/ - Custom MCP tool implementations
  • src/orchestra/tools/permissions.py - PreToolUse hook for restrictions

Agent Permissions

Each agent has:

  • allowed_tools / disallowed_tools - Tool access
  • permissions.filesystem - Path restrictions
  • permissions.git - Branch push/merge restrictions

Enforced via PreToolUse hooks that check before execution.

Testing

uv run pytest
uv run pytest tests/test_agent.py -v

Common Tasks

  • Add agent: Create YAML in config/agents/, add to docker-compose
  • Add tool: Use @tool decorator in src/orchestra/tools/, register in server
  • Debug agent: Logs in stdout, use --debug for SDK verbose output

---

## Dependencies

```toml
[project]
name = "agent-orchestra"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "claude-agent-sdk>=0.1.0",     # Official Claude Agent SDK
    "slack-sdk>=3.27.0",
    "slack-bolt>=1.18.0",
    "pydantic>=2.5.0",
    "pydantic-settings>=2.1.0",
    "pyyaml>=6.0.0",
    "docker>=7.0.0",
    "aiohttp>=3.9.0",
    "httpx>=0.26.0",
    "anyio>=4.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-asyncio>=0.23.0",
    "pytest-mock>=3.12.0",
    "ruff>=0.1.0",
]

Prerequisites:

  • Python 3.10+
  • Node.js (required by Claude Agent SDK)
  • Claude Code CLI 2.0.0+: npm install -g @anthropic-ai/claude-code