Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 0 additions & 50 deletions CLAUDE.md

This file was deleted.

113 changes: 113 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Contributing to Drasill

## Prerequisites

```bash
uv --version # Python package manager
hm --version # Heimdall session supervisor
claude --version # Claude Code CLI (optional, for agent testing)
just --version # Task runner (optional, for convenience)
```

Install Python dependencies:

```bash
uv sync
```

Download the heimdall binary for integration tests:

```bash
bash scripts/fetch-hm.sh
```

## Development

### Quick commands

```bash
just serve # start dev server on :8400 (hot reload)
just check # lint + format + types + tests
just check-all # above + Playwright E2E tests
just test # unit tests only
just test-e2e # Playwright browser tests only
just doctor # verify all dependencies
```

Or without just:

```bash
uv run drasill serve --reload
uv run nox -s all_checks
uv run pytest
uv run pytest e2e_tests/ --no-cov
uv run drasill check-deps
```

### Project layout

```
py_src/drasill/ # source code
py_tests/ # unit + integration tests
e2e_tests/ # Playwright browser tests
scripts/ # CI helpers (fetch-hm.sh)
docs/ # architecture and API docs
```

## Code Style

- **Python 3.13+** — modern type syntax (`list[]`, `dict[]`, `X | None`, `collections.abc`)
- **Ruff** — linter + formatter, `select = ["ALL"]` with targeted ignores in `pyproject.toml`
- **ty** — type checker, strict mode, zero errors
- **Google-style docstrings** for all public APIs
- **No raw ANSI escapes** — use library functions

## Testing

- **pytest + pytest-asyncio** — async tests are the default
- **95% coverage gate** — enforced in CI via `--cov-fail-under=95`
- **Playwright** — E2E browser tests in `e2e_tests/` (not counted in coverage)
- **Real hm integration tests** — `py_tests/test_hm_integration.py` runs against a real heimdall binary

### Running specific tests

```bash
uv run pytest py_tests/test_mesh.py -x -q # one file
uv run pytest -k "test_idle_wake" -x # by name
HM_BIN=./bin/hm uv run pytest py_tests/test_hm_integration.py --no-cov # hm integration
```

## Architecture

- **FastAPI** app factory in `app.py`
- **Agent mesh** — `/mesh/*` endpoints for registration, messaging, SSE events
- **Heimdall** — PTY session supervisor, communicated via Unix socket protocol
- **SQLite** via aiosqlite (WAL mode, CREATE IF NOT EXISTS, no migration framework)
- **HTMX + Alpine.js** — live-updating dashboard with SSE
- **No hooks or plugins** — agents get mesh awareness via system prompt injection at launch

### Key files

| File | Purpose |
|------|---------|
| `app.py` | App factory, lifespan, page routes |
| `mesh/router.py` | Mesh API endpoints |
| `mesh/pruning.py` | Agent monitoring + idle-wake message delivery |
| `terminal/router.py` | Session creation, WebSocket relay, mesh prompt |
| `supervisor_client.py` | Async client for heimdall protocol |
| `doctor.py` | Dependency health checks |
| `db.py` | SQLite schema + data access |
| `config.py` | TOML → Pydantic config |

## CI Pipeline

- **nox all_checks** — lint, format check, type check, tests (with coverage)
- **Codecov** — coverage uploaded on every push
- **hm binary** — downloaded from heimdall releases for integration tests
- **Playwright** — E2E tests run separately (not in nox)

## Commit Conventions

- Conventional commits: `feat:`, `fix:`, `refactor:`, `docs:`, `ci:`, `test:`
- Breaking changes: `feat!:` or `fix!:`
- Keep commits focused — one logical change per commit
37 changes: 11 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![FastAPI](https://img.shields.io/badge/framework-FastAPI-009688.svg)](https://fastapi.tiangolo.com/)
[![HTMX](https://img.shields.io/badge/frontend-HTMX%20%2B%20Alpine.js-3366cc.svg)](https://htmx.org/)

An agent mesh control plane and dashboard server for coordinating local [Claude Code](https://docs.anthropic.com/en/docs/claude-code) sessions. Agents register, discover each other, and exchange messages through Drasill — using Claude Code's native HTTP hooks for interrupt delivery.
An agent mesh control plane and dashboard server for coordinating local [Claude Code](https://docs.anthropic.com/en/docs/claude-code) sessions. Agents register, discover each other, and exchange messages through Drasill — using heimdall stdin injection for message delivery.

---

Expand All @@ -33,9 +33,9 @@ The mesh turns Drasill into a control plane for local Claude Code sessions:
│ (alpha) │ │ (beta) │ │ (gamma) │
└──────┬────────┘ └──────┬────────┘ └──────┬────────┘
│ │ │
│ register │ heartbeat │ inbox
heartbeat │ send msg │ consume
│ check inbox │ check inbox │ send msg
│ register │ send msg │ inbox
send msg │ check inbox │ consume
│ check inbox │ │ send msg
│ │ │
└────────────────────┼────────────────────┘
Expand All @@ -45,33 +45,26 @@ The mesh turns Drasill into a control plane for local Claude Code sessions:
│ │
│ /mesh/* │
│ SQLite │
Scheduler
SSE + async
└────────────────┘
```

- **Register** — `POST /mesh/agents` joins the mesh with a name and optional session ID
- **Discover** — `GET /mesh/agents` lists all active agents
- **Message** — `POST /mesh/agents/{name}/inbox` sends a message (with optional heimdall wake)
- **Consume** — `GET /mesh/agents/{name}/inbox` returns the oldest unread message
- **Heartbeat** — `POST /mesh/agents/{name}/heartbeat` keeps the agent alive
- **Pruning** — Stale agents auto-transition: active → idle → dead → removed

The inbox endpoint is designed for Claude Code's **Stop hook** — when an agent finishes work, the hook checks for messages. If one is waiting, the agent continues with it as its next instruction. No polling, no busy-waiting.

All `/mesh/*` endpoints are **localhost-only** (middleware rejects non-loopback requests), so exposure via Cloudflare Tunnel or similar is safe.

### Web UI

A live-updating control panel showing agent status, recent messages, and a send-message form. SSE events trigger HTMX partial reloads, Alpine.js handles client-side interactivity. Web terminal gives browser access to agent pty sessions.

### Claude Code Plugin
### Agent Integration

The `plugin/drasill-mesh/` directory is a Claude Code plugin providing:

- **Stop hook** — checks inbox on task completion, continues with new messages
- **Idle hook** — checks inbox during idle periods
- **Heartbeat hook** — keeps the agent registered while Claude Code is running
- **`/agent-mesh` skill** — join/leave/send commands for mesh interaction
No plugins or hooks needed. Drasill injects mesh awareness into each agent's system prompt at launch and delivers messages via heimdall stdin injection when agents go idle. Agents communicate with peers using `curl` to the mesh API.

---

Expand All @@ -83,7 +76,7 @@ The `plugin/drasill-mesh/` directory is a Claude Code plugin providing:
| [heimdall (hm)](https://github.com/nazq/heimdall) | Yes | Rust pty supervisor — agents run inside heimdall sessions |
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | Yes | The AI coding agent that Drasill coordinates |
| [git](https://git-scm.com/) | Yes | Used for session naming (repo + branch) and general dev |
| [curl](https://curl.se/) | Recommended | Plugin hooks use curl for HTTP calls to Drasill |
| [curl](https://curl.se/) | Recommended | Agents use curl to communicate with the mesh API |
| [Tailscale](https://tailscale.com/) | Optional | Access Drasill from other devices on your tailnet |

```bash
Expand Down Expand Up @@ -112,13 +105,7 @@ open http://localhost:8400

### Join the mesh from Claude Code

Install the plugin, then from a Claude Code session:

```
/agent-mesh join alpha
```

Send a message from any terminal:
Agents are mesh-aware at launch — no setup needed. Send a message from any terminal:

```bash
curl -s -X POST "http://localhost:8400/mesh/agents/alpha/inbox" \
Expand Down Expand Up @@ -187,6 +174,8 @@ py_src/drasill/
├── config.py # TOML → Pydantic config loader
├── db.py # SQLite schema + init
├── claude.py # Async claude CLI wrapper
├── doctor.py # Dependency health checks
├── supervisor_client.py # Async heimdall socket client
├── mesh/
│ ├── router.py # /mesh/* API endpoints
│ ├── models.py # Pydantic request/response models
Expand All @@ -198,10 +187,6 @@ py_src/drasill/
│ └── router.py # WebSocket heimdall proxy + session creation
├── templates/ # Jinja2 + HTMX templates
└── static/ # PWA assets + CSS

plugin/drasill-mesh/ # Claude Code plugin
├── hooks/ # Stop, idle hooks + watchdog heartbeat
└── skills/agent-mesh/ # /agent-mesh skill definition
```

---
Expand Down
47 changes: 21 additions & 26 deletions docs/ARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,22 @@ Agent mesh control plane and personal dashboard server for coordinating local Cl
│ SSE │ HTTP │ WebSocket
┌───────┴──────────────┴──────────────────┴───────────┐
│ FastAPI (uvicorn) │
│ ┌────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ Mesh │ │Dashboard │ │ Terminal Router │ │
│ │ Router │ │ Plugin │ │ (heimdall client) │ │
│ └───┬────┘ │ System │ └─────────┬──────────┘ │
│ │ └────┬─────┘ │ │
│ ┌───┴────────────┴──────────────────┴──────────┐ │
│ ┌────────┐ ┌──────────────────────────────────┐ │
│ │ Mesh │ │ Terminal Router (heimdall client) │ │
│ │ Router │ └──────────────────────┬───────────┘ │
│ └───┬────┘ │ │
│ ┌───┴─────────────────────────────────┴─────────┐ │
│ │ SQLite (WAL mode) │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ SSE EventBus │ │ Pruning Loop (60s) │ │
│ │ SSE EventBus │ │ Monitoring Loop (5s) │ │
│ └──────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────┘
┌───────┴─────────────────────────────────────────────┐
│ Claude Code Instances (in heimdall) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Agent A │ │ Agent B │ │ Agent C │ ... │
│ │ (hooks) │ │ (hooks) │ │ (hooks) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────┘
```
Expand All @@ -42,22 +40,20 @@ Agent mesh control plane and personal dashboard server for coordinating local Cl
| Module | Purpose |
|--------|---------|
| `app.py` | FastAPI app factory, lifespan, page/API routes |
| `config.py` | TOML config → Pydantic models |
| `config.py` | TOML config → Pydantic models (`ServerConfig`, `DbConfig`, `PruningConfig`) |
| `db.py` | SQLite schema, WAL mode, connection factory |
| `discovery.py` | Dashboard plugin auto-discovery |
| `claude.py` | Async wrapper for `claude -p` CLI |
| `mesh/` | Agent registry, messaging, SSE, pruning |
| `terminal/` | WebSocket heimdall proxy, session creation |
| `dashboards/` | Plugin system for live data panels |
| `mesh/` | Agent registry, messaging, SSE, pruning/monitoring |
| `terminal/` | WebSocket heimdall proxy, session creation, mesh prompt injection |
| `supervisor_client.py` | Unix socket client for heimdall IPC |

Detailed docs:

- [Database & Schema](./database.md)
- [Agent Mesh](./mesh.md)
- [Terminal Integration](./terminal.md)
- [Dashboard Plugin System](./dashboards.md)
- [Agent Integration](./plugin.md)
- [Frontend & Templates](./frontend.md)
- [Plugin & Hooks](./plugin.md)
- [Config & Deployment](./deployment.md)

## Data Flow
Expand All @@ -71,16 +67,16 @@ Claude Code instance → POST /mesh/agents → SQLite insert → SSE "refresh"
### Message Delivery

```
Sender agent
Sender agent (or external channel)
→ POST /mesh/agents/{target}/inbox
→ SQLite insert
→ SSE "refresh" (dashboard update)
→ heimdall INPUT frame (if target has session_id)

Receiver agent
Stop hook fires → GET /mesh/agents/{self}/inbox?format=hook
Returns {decision: "block", reason: "Message from X: ..."}
→ Agent reads message and acts on it
Drasill detects active→idle via STATUS polling
Writes message to stdin via heimdall INPUT frame
→ Agent sees message at prompt, processes it
```

### Terminal WebSocket
Expand All @@ -89,18 +85,17 @@ Receiver agent
Browser
→ WS /terminal/{agent}/ws
→ Lookup agent's session_id in DB
Spawn read loop (adaptive polling: 150ms active, 1s idle)
→ Input events → heimdall INPUT frame
STATUS query outputsend_json to browser
Subscribe to heimdall Unix socket (push stream)
→ Input events (JSON) → heimdall INPUT frame
heimdall OUTPUT framesraw binary to browser (xterm.js renders)
```

### Agent Lifecycle

```
active (heartbeat < idle_timeout)
→ idle (no heartbeat for idle_timeout seconds)
→ dead (no heartbeat for 3× idle_timeout)
→ removed (agent row + orphaned messages deleted)
active ──(no pty output)──→ idle ──(session gone)──→ deleted immediately
active ──(session gone)──────────────────────────→ deleted immediately
dead ──(status=dead > 1h)──────────────────────→ removed (row + messages deleted)
```

## Tech Stack
Expand Down
Loading
Loading