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
17 changes: 15 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,28 @@ jobs:
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'

- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7

- name: Install dependencies
run: uv sync

- name: Download hm binary
run: |
bash scripts/fetch-hm.sh latest ./bin
echo "HM_BIN=$PWD/bin/hm" >> "$GITHUB_ENV"

- name: Run all checks
run: uv run nox -s all_checks

- name: Upload to Codecov
if: always()
uses: codecov/codecov-action@v5
with:
files: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ dist/

# Coverage
.coverage
coverage.xml
htmlcov/

# Database
*.db

# Downloaded binaries
bin/

# Plans (internal)
plans/
11 changes: 5 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,12 @@ uv run nox -s all_checks
## Architecture

- FastAPI app factory in `app.py`
- Dashboard plugin system: subclass `BaseDashboard`, drop in `dashboards/`
- Auto-discovery via `discovery.py`
- APScheduler 3.x `AsyncIOScheduler` for scheduled fetches
- SQLite via aiosqlite (CREATE IF NOT EXISTS, no migration framework)
- HTMX partials for live card updates, Alpine.js for client-side interactivity
- `claude.py` wraps `claude -p` CLI for AI-powered dashboard logic
- Agent mesh: `/mesh/*` endpoints for inter-agent coordination
- SSE event bus (`MeshEventBus`) broadcasts mutations, HTMX `sse:mesh` trigger
- SQLite via aiosqlite (CREATE IF NOT EXISTS, no migration framework)
- HTMX partials for live updates, Alpine.js for client-side interactivity
- Web terminal: WebSocket proxy to heimdall (hm) sessions via `/terminal/*`
- `claude.py` wraps `claude -p` CLI for AI-powered logic

## Config

Expand Down
29 changes: 11 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Python](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Coverage](https://img.shields.io/badge/coverage-96%25-brightgreen.svg)](pyproject.toml)
[![Coverage](https://codecov.io/gh/nazq/Drasill/graph/badge.svg)](https://codecov.io/gh/nazq/Drasill)
[![Ruff](https://img.shields.io/badge/linting-ruff-orange.svg)](https://docs.astral.sh/ruff/)
[![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/)
Expand Down Expand Up @@ -49,9 +49,9 @@ The mesh turns Drasill into a control plane for local Claude Code sessions:
└────────────────┘
```

- **Register** — `POST /mesh/agents` joins the mesh with name and capabilities
- **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 tmux wake)
- **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
Expand All @@ -60,13 +60,9 @@ The inbox endpoint is designed for Claude Code's **Stop hook** — when an agent

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

### Dashboard Server
### Web UI

A pluggable panel system with live-updating cards. Subclass `BaseDashboard`, drop a `.py` file into `dashboards/`, and it's auto-discovered at startup.

Each dashboard defines a `fetch()` method, a schedule, and a Jinja2 partial template. SSE events trigger HTMX partial reloads, and Alpine.js handles client-side interactivity.

The built-in **Agent Mesh** dashboard shows live agent status, recent messages, and a send-message form — all updating every 5 seconds.
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

Expand All @@ -84,15 +80,15 @@ The `plugin/drasill-mesh/` directory is a Claude Code plugin providing:
| Tool | Required | Purpose |
|------|----------|---------|
| [uv](https://docs.astral.sh/uv/) | Yes | Python package management and virtualenv |
| [tmux](https://github.com/tmux/tmux) | Yes | Terminal multiplexer — agents run inside tmux sessions |
| [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 |
| [Tailscale](https://tailscale.com/) | Optional | Access Drasill from other devices on your tailnet |

```bash
# Verify prerequisites
uv --version && tmux -V && claude --version && git --version && curl --version
uv --version && hm --version && claude --version && git --version && curl --version
```

## Quick Start
Expand Down Expand Up @@ -190,24 +186,21 @@ py_src/drasill/
├── app.py # FastAPI app factory + lifespan
├── config.py # TOML → Pydantic config loader
├── db.py # SQLite schema + init
├── discovery.py # Auto-discover dashboard subclasses
├── claude.py # Async claude CLI wrapper
├── dashboards/
│ ├── base.py # BaseDashboard ABC + Alert/Result models
│ └── agent_mesh.py # Live mesh status panel
├── mesh/
│ ├── router.py # /mesh/* API endpoints
│ ├── models.py # Pydantic request/response models
│ ├── events.py # SSE event bus (asyncio.Queue fanout)
│ ├── middleware.py # Localhost-only security middleware
│ ├── uploads.py # File attachment storage
│ └── pruning.py # Agent lifecycle management
├── terminal/
│ └── router.py # WebSocket tmux proxy + session creation
│ └── router.py # WebSocket heimdall proxy + session creation
├── templates/ # Jinja2 + HTMX templates
└── static/ # CSS
└── static/ # PWA assets + CSS

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

Expand Down
5 changes: 0 additions & 5 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,3 @@ port = 8400

[db]
path = "~/.local/share/drasill/dash.db"

# Per-dashboard overrides
[dashboards.agent_mesh]
enabled = true
schedule = "5s"
16 changes: 8 additions & 8 deletions docs/ARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Agent mesh control plane and personal dashboard server for coordinating local Cl
│ FastAPI (uvicorn) │
│ ┌────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ Mesh │ │Dashboard │ │ Terminal Router │ │
│ │ Router │ │ Plugin │ │ (tmux subprocess) │ │
│ │ Router │ │ Plugin │ │ (heimdall client) │ │
│ └───┬────┘ │ System │ └─────────┬──────────┘ │
│ │ └────┬─────┘ │ │
│ ┌───┴────────────┴──────────────────┴──────────┐ │
Expand All @@ -29,7 +29,7 @@ Agent mesh control plane and personal dashboard server for coordinating local Cl
└─────────────────────────────────────────────────────┘
┌───────┴─────────────────────────────────────────────┐
Claude Code Instances (in tmux)
│ Claude Code Instances (in heimdall)
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Agent A │ │ Agent B │ │ Agent C │ ... │
│ │ (hooks) │ │ (hooks) │ │ (hooks) │ │
Expand All @@ -47,7 +47,7 @@ Agent mesh control plane and personal dashboard server for coordinating local Cl
| `discovery.py` | Dashboard plugin auto-discovery |
| `claude.py` | Async wrapper for `claude -p` CLI |
| `mesh/` | Agent registry, messaging, SSE, pruning |
| `terminal/` | WebSocket tmux proxy, session creation |
| `terminal/` | WebSocket heimdall proxy, session creation |
| `dashboards/` | Plugin system for live data panels |

Detailed docs:
Expand Down Expand Up @@ -75,7 +75,7 @@ Sender agent
→ POST /mesh/agents/{target}/inbox
→ SQLite insert
→ SSE "refresh" (dashboard update)
tmux send-keys (if target has tmux_target)
heimdall INPUT frame (if target has session_id)

Receiver agent
→ Stop hook fires → GET /mesh/agents/{self}/inbox?format=hook
Expand All @@ -88,10 +88,10 @@ Receiver agent
```
Browser
→ WS /terminal/{agent}/ws
→ Lookup agent's tmux_target in DB
→ Lookup agent's session_id in DB
→ Spawn read loop (adaptive polling: 150ms active, 1s idle)
→ Input events → tmux send-keys
Capture output → send_json to browser
→ Input events → heimdall INPUT frame
STATUS query output → send_json to browser
```

### Agent Lifecycle
Expand All @@ -112,7 +112,7 @@ active (heartbeat < idle_timeout)
| Database | SQLite via aiosqlite (WAL mode) |
| Frontend | HTMX + Alpine.js + Tailwind CSS |
| Terminal | ghostty-web (Zig→WASM) + WebSocket |
| Process control | tmux subprocess calls |
| Process control | heimdall (Rust pty supervisor) |
| Linting | ruff (ALL rules) |
| Type checking | ty (strict) |
| Testing | pytest + pytest-asyncio, 95% gate |
Expand Down
69 changes: 0 additions & 69 deletions docs/dashboards.md

This file was deleted.

31 changes: 1 addition & 30 deletions docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,6 @@ PRAGMA synchronous = NORMAL # Balanced durability/perf

## Tables

### `runs` — Dashboard fetch history

Stores every dashboard fetch result for history/debugging.

| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK | Auto-increment |
| dashboard | TEXT | Dashboard plugin ID |
| started_at | TEXT | ISO 8601 UTC |
| finished_at | TEXT | ISO 8601 UTC |
| status | TEXT | `ok` or `error` |
| data_json | TEXT | JSON result payload |
| error | TEXT | Error message if failed |

Index: `idx_runs_dash_time(dashboard, started_at DESC)`

### `kv` — Dashboard key-value store

Persistent state for dashboard plugins (e.g., last-seen cursor).

| Column | Type | Notes |
|--------|------|-------|
| dashboard | TEXT | Composite PK |
| key | TEXT | Composite PK |
| value | TEXT | Arbitrary string/JSON |
| updated_at | TEXT | ISO 8601 UTC |

### `mesh_agents` — Agent registry

Each Claude Code instance registers here on mesh join.
Expand All @@ -48,9 +21,7 @@ Each Claude Code instance registers here on mesh join.
|--------|------|-------|
| name | TEXT PK | Unique agent identifier |
| pid | INTEGER | OS process ID |
| session_id | TEXT | Claude Code session ID |
| tmux_target | TEXT | e.g., `sess:0.0` for tmux wake |
| capabilities | TEXT | JSON array of capabilities |
| session_id | TEXT | heimdall session ID for wake delivery |
| registered_at | TEXT | ISO 8601 UTC |
| last_heartbeat | TEXT | Updated by heartbeat endpoint |
| status | TEXT | `active`, `idle`, or `dead` |
Expand Down
2 changes: 1 addition & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ drasill install cloudflared --print-only

### Why Not Docker?

Drasill manages tmux sessions on the host for agent terminals. A container can't reach the host's tmux server without privileged access and socket mounts, which defeats the purpose. Use `uv tool install` + systemd instead.
Drasill manages hm supervisor sessions on the host for agent terminals. A container can't reach the host's Unix sockets without privileged access and socket mounts, which defeats the purpose. Use `uv tool install` + systemd instead.

### Security Note

Expand Down
8 changes: 4 additions & 4 deletions docs/mesh.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Agent Mesh

The mesh coordinates multiple Claude Code instances running in tmux panes. Agents register, exchange messages, and maintain liveness through heartbeats.
The mesh coordinates multiple Claude Code instances running in heimdall (pty supervisor) sessions. Agents register, exchange messages, and maintain liveness through heartbeats.

## API Endpoints

Expand All @@ -10,7 +10,7 @@ All endpoints are prefixed with `/mesh` and restricted to localhost by `Localhos

| Method | Path | Description |
|--------|------|-------------|
| POST | `/agents` | Register agent (name, pid, tmux_target) |
| POST | `/agents` | Register agent (name, pid, session_id) |
| DELETE | `/agents/{name}` | Deregister + cleanup messages |
| GET | `/agents` | List all agents |
| GET | `/agents/{name}` | Get single agent details |
Expand Down Expand Up @@ -54,11 +54,11 @@ When an agent is removed, its undelivered messages are logged and deleted.

Messages reach agents through two channels:

1. **tmux push (immediate):** If the target agent has a `tmux_target`, Drasill runs `tmux send-keys` with a preview of the message. This wakes idle Claude Code instances.
1. **heimdall wake (immediate):** If the target agent has a `session_id`, Drasill sends an INPUT frame via `supervisor_client.send_input()` with a preview of the message. This wakes idle Claude Code instances.

2. **Stop hook (between tasks):** When Claude Code finishes a task, the Stop hook calls the inbox endpoint. If a message is waiting, it returns `{decision: "block", reason: "..."}` which Claude Code displays before stopping.

The tmux push is fire-and-forget — failures are logged but never block the sender.
The heimdall wake is fire-and-forget — failures are logged but never block the sender.

## Shared Dependencies

Expand Down
Loading
Loading