Ennio
Ennio is an AI agent orchestrator, named after Ennio Morricone — the legendary Italian composer who conducted entire orchestras to create something greater than any single instrument could achieve.
Ennio does the same with AI coding agents. It spawns multiple agents across repositories, monitors their progress through the full pull request lifecycle, and reacts when things go wrong — automatically sending CI failure logs back to the agent, notifying you when reviews come in, and even auto-merging approved PRs.
What Ennio Does
- Spawns AI coding agents (Claude Code, Aider, Codex, OpenCode) in isolated git worktrees or cloned workspaces
- Monitors each session through 16 lifecycle states, from
SpawningthroughMerged - Reacts to external events — CI failures, code review feedback, merge conflicts — by sending instructions back to the agent or notifying you
- Runs locally or remotely over SSH, with a gRPC daemon (
ennio-node) for structured remote communication - Tracks costs with double-entry bookkeeping and configurable budgets
- Exposes a REST API, web dashboard, and terminal UI for monitoring
Key Concepts
| Concept | Description |
|---|---|
| Session | A single agent working on a task in an isolated workspace |
| Project | A repository configuration with plugin overrides and reaction rules |
| Plugin | A swappable implementation for one of 7 slots (agent, runtime, workspace, tracker, SCM, notifier, terminal) |
| Reaction | A rule that fires when a session enters a specific state (e.g., CI failed → send logs to agent) |
| Lifecycle Manager | The polling loop that checks external state and triggers reactions |
Architecture at a Glance
Ennio is a Rust workspace with 16 crates:
ennio-cli CLI binary (clap)
ennio-core Shared types, config, session model
ennio-services Orchestration (session manager, lifecycle, event bus)
ennio-plugins All plugin implementations
ennio-db SQLite persistence
ennio-ssh SSH client and remote strategies
ennio-node Remote gRPC daemon
ennio-proto Protobuf service definitions
ennio-web REST API (axum)
ennio-tui Terminal dashboard (ratatui)
ennio-dashboard Web dashboard (dioxus WASM)
ennio-nats NATS messaging
ennio-ledger Cost tracking and budgets
ennio-ml ML trait interfaces
ennio-observe OpenTelemetry integration
ennio-doc Documentation crate
Installation
Nix (Recommended)
Ennio provides a Nix flake with pre-built packages.
# Run directly without installing
nix run github:glottologist/ennio -- --help
# Build the CLI
nix build github:glottologist/ennio
# Build the remote node daemon
nix build github:glottologist/ennio#ennio-node
# Build the WASM dashboard
nix build github:glottologist/ennio#ennio-dashboard
From a local checkout:
nix build # ennio CLI (default package)
nix build .#ennio-node # remote daemon
nix run # run directly
nix flake check # clippy, fmt, tests, docs, audit
nix develop # dev shell with all tools
The dev shell includes: cargo-nextest, cargo-audit, cargo-watch, cargo-bloat, bacon, and rust-analyzer.
Cargo
Requires Rust 1.88+ and protoc (protobuf compiler).
# Install protoc (Debian/Ubuntu)
sudo apt install protobuf-compiler
# Install protoc (macOS)
brew install protobuf
# Build both binaries
cargo build --release -p ennio-cli -p ennio-node
Binaries are at target/release/ennio and target/release/ennio-node.
Docker
# Build the image
docker build -t ennio .
# Run the CLI
docker run --rm ennio --help
# Run the node daemon
docker run --rm --entrypoint ennio-node ennio --help
Pre-built images are published to glottologist/ennio on Docker Hub for tagged releases (v*).
The Docker image is based on debian:bookworm-slim and includes git, tmux, and openssh-client since Ennio shells out to these at runtime.
Runtime Dependencies
Ennio requires these tools to be available on $PATH:
| Tool | Required By | Purpose |
|---|---|---|
git | All workspaces | Clone repos, create worktrees, check status |
tmux | TmuxRuntime, TmuxStrategy | Manage agent terminal sessions |
ssh | SSH strategies | Connect to remote machines |
tmate | TmateStrategy only | Shared terminal sessions |
Optional services:
| Service | Purpose |
|---|---|
| NATS | Event messaging between components |
| SQLite | Session and event persistence (auto-created) |
Quick Start
This guide gets you from zero to a running agent in under 5 minutes.
1. Initialize Configuration
ennio init .
This creates ennio.yaml with sensible defaults. Edit it to add your project:
port: 3000
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
projects:
- name: my-app
repo: git@github.com:user/my-app.git
path: /home/user/repos/my-app
default_branch: main
tracker_config:
plugin: github
config:
owner: user
repo: my-app
token: ${GITHUB_TOKEN}
scm_config:
plugin: github
config:
owner: user
repo: my-app
token: ${GITHUB_TOKEN}
Note: Environment variables in
${VAR}syntax are expanded at load time.
2. Start the Orchestrator
ennio start
This boots the lifecycle polling loop, the web API, and connects to NATS and SQLite.
3. Spawn an Agent Session
# Work on a GitHub issue
ennio spawn my-app --issue 42
# Or give a direct prompt
ennio spawn my-app --prompt "Add input validation to the signup form"
# Optionally specify a branch
ennio spawn my-app --issue 42 --branch feat/signup-validation
Ennio will:
- Create an isolated workspace (git worktree by default)
- Run any
post_createhooks (e.g.,npm install) - Launch the agent with the issue description or prompt
- Begin monitoring the session lifecycle
4. Monitor Progress
# See all sessions
ennio status
# Filter by project
ennio status my-app
# Detailed session info
ennio session info <session-id>
# Open the web dashboard
ennio dashboard
5. Interact with a Session
# Send additional instructions to the agent
ennio send <session-id> "Also add unit tests"
# Open the agent's tmux terminal
ennio open <session-id>
# Kill a stuck session
ennio session kill <session-id>
# Restore an exited session
ennio session restore <session-id>
6. Stop the Orchestrator
ennio stop
What Happens Automatically
Once a session is spawned, the lifecycle manager polls external state and reacts:
| Event | Default Reaction |
|---|---|
| CI fails | Sends failure logs to the agent (up to 2 retries) |
| Code review requests changes | Sends review comments to the agent |
| Merge conflicts detected | Instructs the agent to rebase |
| PR approved + CI green | Notifies you (or auto-merges if configured) |
| Agent exits unexpectedly | Sends urgent notification |
All reactions are configurable per-project. See the Reactions guide.
Configuration
Ennio is configured via ennio.yaml, discovered by searching from the current directory upward.
Minimal Configuration
projects:
- name: my-project
repo: git@github.com:user/repo.git
path: /home/user/repos/repo
Everything else uses defaults: tmux runtime, claude-code agent, worktree workspace, port 3000.
Environment Variable Expansion
All string values support ${VAR} expansion:
api_token: ${ENNIO_API_TOKEN}
projects:
- name: app
tracker_config:
plugin: github
config:
token: ${GITHUB_TOKEN}
Config Discovery
Ennio searches for ennio.yaml starting from the current working directory, walking up to the filesystem root. The first match wins.
Full Reference
See the Configuration Reference for every field, its type, default value, and description.
Per-Project Overrides
Each project can override the global defaults for runtime, agent, and workspace:
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
projects:
- name: fast-project
runtime: process # override: use direct process instead of tmux
agent: aider # override: use aider instead of claude-code
workspace: clone # override: full clone instead of worktree
# ...
Database
Ennio uses SQLite for persistence. The database file is created automatically:
database_url: sqlite:ennio.db # relative to working directory
If omitted, Ennio defaults to sqlite:ennio.db (file-based, persists across restarts).
API Authentication
Protect the REST API with a bearer token:
api_token: ${ENNIO_API_TOKEN}
All authenticated endpoints require Authorization: Bearer <token>. The token is compared using constant-time SHA-256 hashing to prevent timing attacks.
Session Lifecycle
Every agent session in Ennio progresses through a state machine with 16 states.
State Machine
Spawning → Working → PrDraft → PrOpen ──→ CiPassing → ReviewPending → Approved → Merged → Done
│ │ │ │
│ CiFailed ChangesRequested MergeConflicts
│ │ │ │
│ CiFixSent (agent fixes) (agent rebases)
│ │
│ CiFixFailed
│
├── Exited (restorable)
└── Killed (terminal)
States
| State | Terminal | Description |
|---|---|---|
Spawning | No | Workspace being created, agent starting |
Working | No | Agent is actively writing code |
PrDraft | No | Draft pull request created |
PrOpen | No | Pull request opened for review |
CiPassing | No | CI checks are green |
CiFailed | No | CI checks failed |
CiFixSent | No | Agent sent a fix for CI |
CiFixFailed | No | CI fix attempt also failed |
ReviewPending | No | Awaiting code review |
ChangesRequested | No | Reviewer requested changes |
Approved | No | PR approved |
MergeConflicts | No | Merge conflicts detected |
Merged | Yes | PR merged successfully |
Done | Yes | Session completed normally |
Exited | No | Agent exited unexpectedly (can be restored) |
Killed | Yes | Manually terminated |
Activity States
Independent of the session status, each session has an activity state reflecting the agent process:
| Activity | Description |
|---|---|
Active | Agent is actively working |
Ready | Agent is idle, waiting for input |
Idle | No recent activity |
WaitingInput | Agent explicitly waiting for user input |
Blocked | Blocked on an external resource |
Exited | Agent process has exited |
Lifecycle Polling
The LifecycleManager runs a continuous polling loop that:
- Queries the tracker plugin for PR/CI status
- Queries the SCM plugin for review state
- Compares external state against the current session status
- Triggers status transitions
- Fires configured reactions
- Emits events to the event bus and database
Session Operations
| Operation | CLI Command | Effect |
|---|---|---|
| Spawn | ennio spawn <project> | Creates workspace, starts agent |
| Send | ennio send <session> <msg> | Sends text to the running agent |
| Kill | ennio session kill <id> | Terminates the agent and marks session as Killed |
| Restore | ennio session restore <id> | Restarts an Exited session in its existing workspace |
| Info | ennio session info <id> | Shows session details, status history, recent events |
| List | ennio session list [project] | Lists all sessions, optionally filtered by project |
Plugin System
Ennio is built around 7 pluggable slots. Each slot defines a trait, and Ennio ships with concrete implementations you can swap per-project.
Plugin Slots
| Slot | Trait Location | Purpose |
|---|---|---|
| Agent | ennio-core | The AI coding agent to run |
| Runtime | ennio-core | How the agent process is managed |
| Workspace | ennio-core | How the working directory is created |
| Tracker | ennio-core | Issue tracker integration (fetch issues, update status) |
| SCM | ennio-core | Source control (PR status, reviews, merge) |
| Notifier | ennio-core | Where notifications are sent |
| Terminal | ennio-core | Browser-based terminal access to sessions |
Available Implementations
Agent Plugins
| Name | Description |
|---|---|
claude-code | Anthropic’s Claude Code CLI agent |
aider | Aider AI coding assistant |
codex | OpenAI Codex CLI |
opencode | OpenCode agent |
Runtime Plugins
| Name | Description |
|---|---|
tmux | Runs the agent inside a tmux session (default). Supports attach, send-keys, capture-pane. |
process | Direct child process. Simpler but no terminal attach. |
ssh | Runs the agent on a remote machine via SSH + tmux. |
Workspace Plugins
| Name | Description |
|---|---|
worktree | Creates a git worktree from the existing repo (default). Fast, shares git objects. |
clone | Full shallow clone of the repository. Fully isolated but slower. |
Both workspace types support:
symlinks— symlink files into the workspace (e.g.,.env)post_create— shell commands run after workspace creation (e.g.,npm install)
Tracker Plugins
| Name | Description |
|---|---|
github | GitHub Issues — fetch issue details, create branches from issue titles |
linear | Linear — fetch issue details and metadata |
SCM Plugins
| Name | Description |
|---|---|
github | GitHub PRs — check CI status, get reviews, merge PRs, auto-merge |
Notifier Plugins
| Name | Description |
|---|---|
desktop | Desktop notifications via notify-send (Linux) or osascript (macOS) |
slack | Slack incoming webhooks |
webhook | Generic HTTP POST to any URL |
Terminal Plugins
| Name | Description |
|---|---|
web | WebSocket-based terminal access through the browser |
Configuration
Set global defaults and override per-project:
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
notifiers:
- desktop
projects:
- name: frontend
agent: aider # use aider for this project
workspace: clone # full clone instead of worktree
# runtime inherits tmux from defaults
Agent Configuration
Fine-tune agent behavior per-project:
projects:
- name: my-app
agent_config:
model: opus
permissions: "--dangerously-skip-permissions"
passthrough:
max_turns: 200
agent_rules:
- "Always write tests for new code"
- "Use conventional commits"
- "Never modify the database schema without a migration"
| Field | Description |
|---|---|
model | Model to use (e.g., opus, sonnet) |
permissions | Permission mode for the agent |
passthrough | Additional key-value pairs passed through to the agent |
agent_rules are passed to the agent as system instructions alongside the task prompt.
Reactions
Reactions are configurable rules that fire when a session enters a specific state. They are the core of Ennio’s autonomous behavior.
How Reactions Work
- The lifecycle manager detects a state change (e.g., CI failed)
- It looks up matching reaction rules for the session’s project
- It checks the retry count and escalation timeout
- It executes the action (send to agent, notify, auto-merge)
- If the action fails or the state persists beyond the escalation timeout, it escalates
Reaction Actions
| Action | Description |
|---|---|
send_to_agent | Sends a message to the running agent with instructions |
notify | Sends a notification through configured notifier plugins |
auto_merge | Merges the PR via the SCM plugin |
Default Reactions
Ennio ships with 9 built-in reactions:
| Key | Trigger | Action | Retries | Escalation |
|---|---|---|---|---|
ci-failed | CI checks fail | send_to_agent | 2 | 120s |
changes-requested | Reviewer requests changes | send_to_agent | — | 1800s |
bugbot-comments | Bot comments on PR | send_to_agent | — | 1800s |
merge-conflicts | Merge conflicts detected | send_to_agent | — | 900s |
approved-and-green | PR approved + CI green | notify | — | — |
agent-stuck | No activity for threshold | notify (urgent) | — | 600s |
agent-needs-input | Agent waiting for input | notify (urgent) | — | — |
agent-exited | Agent process exited | notify (urgent) | — | — |
all-complete | All project sessions done | notify (info) | — | — |
Configuration
Override reactions globally or per-project:
# Global reactions
reactions:
ci-failed:
enabled: true
action: send_to_agent
message: "CI failed. Check the logs and fix the issues."
priority: action
retries: 3
escalate_after: 180
approved-and-green:
enabled: true
action: auto_merge # auto-merge instead of just notifying
projects:
- name: critical-app
reactions:
ci-failed:
enabled: true
action: send_to_agent
message: "CI is red. This is a critical service — fix immediately."
priority: urgent
retries: 5
escalate_after: 60
Project-level reactions override global reactions for matching keys.
Reaction Fields
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this reaction is active |
action | ReactionAction | "notify" | What to do when triggered |
message | String? | — | Message sent to agent (for send_to_agent) |
priority | EventPriority | "info" | Priority of emitted events |
retries | u32 | 0 | Maximum retry attempts |
escalate_after | Duration? | — | Seconds before escalating |
threshold | Duration? | — | Idle time before triggering (e.g., agent-stuck) |
include_summary | bool | false | Include session summary in notification |
Escalation
When a reaction’s escalate_after timeout expires and the state hasn’t changed, Ennio escalates by sending a notification regardless of the original action. This ensures a human is alerted when automated recovery fails.
Event Priority
Reactions emit events with a priority level that affects notification routing:
| Priority | Use |
|---|---|
Info | Status updates, completions |
Action | Something needs attention soon |
Urgent | Immediate human attention required |
Critical | System-level failures |
Remote Execution
Ennio can run agent sessions on remote machines over SSH. Projects with ssh_config in their configuration run remotely; projects without it run locally.
How It Works
- Ennio connects to the remote host via SSH (using
russh) - Creates a workspace on the remote machine (worktree or clone)
- Launches the agent inside a tmux/tmate session on the remote host
- Monitors the session by reading tmux pane output over SSH
- Optionally runs an
ennio-nodegRPC daemon for structured communication
SSH Strategies
| Strategy | Description |
|---|---|
tmux | Creates a tmux session on the remote host. Most reliable. |
tmate | Creates a tmate session for shared terminal access. |
remote_control | Uses claude remote-control for direct agent control. |
node | Deploys and communicates with ennio-node via gRPC over SSH tunnel. |
Configuration
projects:
- name: remote-project
repo: git@github.com:user/repo.git
path: /home/deploy/repos/repo
ssh_config:
host: build-server.example.com
port: 22
username: deploy
auth:
type: key
path: ~/.ssh/id_ed25519
strategy: tmux
connection_timeout: 30s
keepalive_interval: 60s
host_key_policy: accept_new
known_hosts_path: ~/.ssh/known_hosts
Authentication Methods
SSH Key (default)
ssh_config:
auth:
type: key
path: ~/.ssh/id_ed25519
passphrase: ${SSH_PASSPHRASE} # optional
SSH Agent
ssh_config:
auth:
type: agent
Uses the running SSH agent (SSH_AUTH_SOCK).
Password
ssh_config:
auth:
type: password
password: ${SSH_PASSWORD}
Warning: Password authentication is less secure than key-based auth. Use it only when key-based auth is not available.
Host Key Verification
| Policy | Behavior |
|---|---|
strict | Only connect if host key matches known_hosts |
accept_new | Accept and persist unknown keys, reject changed keys (default) |
accept_all | Accept any host key (insecure, for testing only) |
Remote Node Daemon
For structured communication beyond tmux send-keys, deploy ennio-node on the remote host:
ssh_config:
strategy: node
node_config:
enabled: true
port: 9100
idle_timeout: 3600
workspace_root: /home/deploy/ennio-workspaces
ennio_binary_path: /usr/local/bin/ennio-node
auth_token: ${ENNIO_NODE_TOKEN}
The node daemon:
- Accepts gRPC calls over an SSH-tunneled port
- Manages workspaces and agent sessions locally on the remote host
- Reports health status back to the orchestrator
- Shuts down automatically after the idle timeout
Node Management
ennio node list # list all configured node projects
ennio node status build-server # check node health
ennio node connect remote-project # establish connection
ennio node disconnect remote-project
Local vs Remote Routing
The session manager routes automatically based on config:
ssh_config present? | Workspace | Runtime | Communication |
|---|---|---|---|
| No | Local worktree/clone | Local tmux/process | Direct |
| Yes, strategy: tmux | Remote via SSH | Remote tmux via SSH | SSH send-keys |
| Yes, strategy: node | Remote via gRPC | Remote via gRPC | SSH-tunneled gRPC |
Cost Tracking
Ennio includes a financial ledger for tracking AI agent costs using double-entry bookkeeping.
Overview
Every token consumed by an agent session is recorded as a CostEntry and posted to the ledger as a double-entry Transfer. This gives you accurate, auditable cost data per session, per project, and globally.
Cost Entries
Each cost entry records:
| Field | Description |
|---|---|
session_id | Which session incurred the cost |
project_id | Which project the session belongs to |
input_tokens | Number of input tokens consumed |
output_tokens | Number of output tokens generated |
cost_usd | Dollar cost of the API call |
model | Model used (e.g., opus, sonnet) |
timestamp | When the cost was incurred |
Querying Costs
The ledger trait provides:
get_session_cost(session_id) → Decimal
get_project_cost(project_id) → Decimal
get_total_cost() → Decimal
All monetary values use rust_decimal::Decimal for precision.
Budgets
Set spending limits at three scopes:
| Scope | Description |
|---|---|
Global | Total spend across all projects |
Project | Spend limit for a specific project |
Session | Spend limit for a single session |
Each budget has a period:
| Period | Description |
|---|---|
Daily | Resets every 24 hours |
Monthly | Resets every calendar month |
Total | Lifetime limit, never resets |
Budget Checking
Before spawning expensive operations, check the budget:
check_budget(project_id, amount) → BudgetStatus
BudgetStatus tells you:
within_budget— whether the amount fitsused/limit/remaining— current statepercent_used— utilization percentage
Double-Entry Bookkeeping
The ledger uses proper accounting:
- Asset accounts — track what you own
- Liability accounts — track what you owe
- Revenue accounts — track income
- Expense accounts — track spending
Every cost entry creates a Transfer that debits an expense account and credits an asset account, maintaining the accounting equation.
Implementation
Ennio ships with InMemoryLedger — a thread-safe in-memory implementation using RwLock. The Ledger trait is async and designed for future backing stores (database, TigerBeetle, etc.).
Notifications
Ennio sends notifications when reactions fire or sessions need attention.
Notifier Plugins
| Plugin | Transport | Configuration |
|---|---|---|
desktop | notify-send (Linux) / osascript (macOS) | No config needed |
slack | Slack incoming webhook | webhook_url |
webhook | HTTP POST to any URL | url |
Configuration
Defining Notifiers
Each notifier specifies a plugin name, a unique name for routing, and a config map with plugin-specific settings:
notifiers:
- plugin: slack
name: team-slack
config:
webhook_url: ${SLACK_WEBHOOK_URL}
- plugin: webhook
name: ops-alerts
config:
url: https://hooks.example.com/ennio
- plugin: desktop
name: local
config: {}
Default Notifiers
Set which notifiers are used by default:
defaults:
notifiers:
- local
- team-slack
Notification Routing
Route specific reaction types to specific notifiers:
notification_routing:
ci-failed:
- team-slack
agent-exited:
- team-slack
- ops-alerts
all-complete:
- local
If no routing rule matches, the default notifiers are used.
Event Priority
Notifications carry a priority from the triggering event:
| Priority | Meaning |
|---|---|
Info | Status updates (e.g., all sessions complete) |
Action | Something needs attention soon |
Urgent | Immediate attention (e.g., agent exited, needs input) |
Critical | System-level failure |
Notifier implementations can use priority to adjust formatting (e.g., Slack emoji, desktop urgency level).
REST API
Ennio exposes a REST API on the configured port (default: 3000).
Authentication
All endpoints under /api/v1 (except /health) require a bearer token when api_token is set in the configuration:
Authorization: Bearer <token>
The token is compared using constant-time SHA-256 hashing.
Endpoints
Health Check
GET /api/v1/health
Returns 200 OK with no authentication required. Use for load balancer health checks.
Response:
{
"data": "ok"
}
List Sessions
GET /api/v1/sessions[?project_id=<id>]
Returns all active sessions, optionally filtered by project.
| Parameter | Location | Required | Description |
|---|---|---|---|
project_id | query | No | Filter sessions by project ID |
Response:
{
"data": [
{
"id": "myapp-abc123",
"project_id": "my-app",
"status": "Working",
"activity": "Active",
"branch": "feat/auth",
"pr_url": null,
"agent_name": "claude-code"
}
]
}
Get Session
GET /api/v1/sessions/{id}
Returns details for a specific session.
Response: 200 with session object wrapped in data, or 404 if not found.
{
"data": {
"id": "myapp-abc123",
"project_id": "my-app",
"status": "CiPassing",
"activity": "Idle",
"branch": "feat/auth",
"pr_url": "https://github.com/org/repo/pull/42",
"agent_name": "claude-code"
}
}
Spawn Session
POST /api/v1/sessions
Request Body:
{
"project_id": "my-app",
"issue_id": "42",
"prompt": "Add input validation",
"branch": "feat/validation",
"role": "implementer"
}
| Field | Type | Required | Description |
|---|---|---|---|
project_id | String | Yes | Project to spawn in |
issue_id | String | No | Issue ID to work on (fetched from tracker) |
prompt | String | No | Direct prompt for the agent |
branch | String | No | Git branch name to use |
role | String | No | Session role |
Provide either issue_id or prompt (or both).
Response: 200 with the new session object wrapped in data.
Kill Session
DELETE /api/v1/sessions/{id}
Terminates the agent and marks the session as Killed.
Response:
{
"data": "killed"
}
Returns 404 if session not found.
Send Message to Session
POST /api/v1/sessions/{id}/send
Request Body:
{
"message": "Also add unit tests for the validation logic"
}
Sends text input to the running agent via the runtime plugin.
Response:
{
"data": "sent"
}
Returns 404 if session not found.
Error Responses
All errors return a JSON body:
{
"error": "Session not found",
"code": 404
}
HTTP Status Code Mapping
| Status | Trigger |
|---|---|
200 | Success |
400 | Invalid ID, configuration errors |
402 | Budget exceeded |
404 | Entity not found |
409 | Entity already exists |
504 | Operation timed out |
500 | Internal server error |
CORS
Configure allowed origins:
cors_origins:
- http://localhost:3000
- https://dashboard.example.com
Allowed methods: GET, POST, DELETE. If no origins are configured, CORS headers are not sent.
CLI Reference
Global Usage
ennio [COMMAND]
Commands
init
Initialize a new Ennio configuration file.
ennio init <path>
Creates ennio.yaml at the specified path with default values. Refuses to overwrite existing files.
start
Start the orchestrator lifecycle loop.
ennio start
Boots the full orchestrator: SQLite database, NATS connection, plugin registry, web API server, and lifecycle polling loop. Runs until interrupted (Ctrl+C) or stopped via ennio stop.
stop
Stop a running orchestrator.
ennio stop
Sends a shutdown signal via NATS.
status
Show status of all sessions.
ennio status [project]
| Argument | Required | Description |
|---|---|---|
project | No | Filter sessions by project name |
spawn
Spawn a new agent session.
ennio spawn <project> [OPTIONS]
| Option | Short | Description |
|---|---|---|
--issue | -i | Issue ID to work on (fetched from tracker) |
--prompt | -p | Direct prompt for the agent |
--branch | -b | Git branch name to use |
--role | -r | Session role |
Provide either --issue or --prompt (or both).
send
Send a message to a running session.
ennio send <session-id> <message>
The message is delivered to the agent via the runtime plugin (e.g., tmux send-keys).
session
Manage individual sessions.
session info
ennio session info <id>
Displays session details including status, activity, branch, PR URL, and recent events.
session kill
ennio session kill <id>
Terminates the agent and marks the session as Killed.
session restore
ennio session restore <id>
Restarts an Exited session in its existing workspace.
session list
ennio session list [project]
Lists all sessions, optionally filtered by project.
dashboard
Open the web dashboard.
ennio dashboard [--port <port>]
| Option | Default | Description |
|---|---|---|
--port | 3000 | Port for the dashboard web server |
open
Open a session’s terminal.
ennio open <session-id>
Prints the tmux attach command to connect to the agent’s terminal session.
node
Manage remote node daemons.
node status
ennio node status [host]
Check health of a remote ennio-node daemon.
node list
ennio node list
List all projects configured with remote node connections.
node connect
ennio node connect <project>
Establish an SSH tunnel and connect to the remote node for the specified project.
node disconnect
ennio node disconnect <project>
Disconnect and shut down the remote node.
gRPC Node Protocol
The ennio-node daemon runs on remote machines and communicates with the orchestrator via gRPC over an SSH tunnel.
Running the Daemon
ennio-node [OPTIONS]
| Option | Default | Env Var | Description |
|---|---|---|---|
--port | 9100 | — | gRPC listen port |
--idle-timeout | 3600 | — | Seconds before auto-shutdown |
--workspace-root | — | — | Root directory for workspaces |
--auth-token | — | ENNIO_NODE_AUTH_TOKEN | Bearer token for authentication |
Authentication
When --auth-token is set, all gRPC calls must include a authorization metadata key with value Bearer <token>. The token is compared using constant-time SHA-256 hashing.
If no token is set, the daemon relies on SSH tunnel isolation for security (only reachable via the tunnel).
Connection Flow
Orchestrator Remote Host
│ │
├── SSH connect ────────────────►│
├── Port forward (tunnel) ──────►│ :9100
├── gRPC Heartbeat ────────────►│
│◄──────────── healthy=true ────┤
├── gRPC CreateWorkspace ──────►│
│◄──────────── workspace_path ──┤
├── gRPC CreateRuntime ────────►│
│◄──────────── runtime handle ──┤
│ ...polling... │
├── gRPC Shutdown ─────────────►│
│◄──────────── accepted=true ───┤
│ │
Idle Timeout
The daemon automatically shuts down after --idle-timeout seconds of no gRPC activity. This prevents orphaned daemons from consuming resources on remote machines. The orchestrator re-deploys the daemon on next connection.
Service Definition
service EnnioNode {
rpc CreateWorkspace(CreateWorkspaceRequest) returns (CreateWorkspaceResponse);
rpc DestroyWorkspace(DestroyWorkspaceRequest) returns (DestroyWorkspaceResponse);
rpc CreateRuntime(CreateRuntimeRequest) returns (CreateRuntimeResponse);
rpc DestroyRuntime(DestroyRuntimeRequest) returns (DestroyRuntimeResponse);
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
rpc GetOutput(GetOutputRequest) returns (GetOutputResponse);
rpc IsAlive(IsAliveRequest) returns (IsAliveResponse);
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
}
Message Types
Workspace Management
message CreateWorkspaceRequest {
string project_id = 1;
string repo_url = 2;
string path = 3;
string session_id = 4;
string default_branch = 5;
optional string branch = 6;
string workspace_type = 7; // "worktree" or "clone"
}
message CreateWorkspaceResponse {
string workspace_path = 1;
}
message DestroyWorkspaceRequest {
string workspace_path = 1;
}
message DestroyWorkspaceResponse {}
Runtime Management
message CreateRuntimeRequest {
string session_id = 1;
string launch_command = 2;
map<string, string> env = 3;
string cwd = 4;
string session_name = 5;
}
message CreateRuntimeResponse {
ProtoRuntimeHandle handle = 1;
}
message DestroyRuntimeRequest {
ProtoRuntimeHandle handle = 1;
}
message DestroyRuntimeResponse {}
message ProtoRuntimeHandle {
string id = 1;
string runtime_name = 2;
map<string, string> data = 3;
}
Session Communication
message SendMessageRequest {
ProtoRuntimeHandle handle = 1;
string message = 2;
}
message SendMessageResponse {}
message GetOutputRequest {
ProtoRuntimeHandle handle = 1;
uint32 lines = 2;
}
message GetOutputResponse {
string output = 1;
}
message IsAliveRequest {
ProtoRuntimeHandle handle = 1;
}
message IsAliveResponse {
bool alive = 1;
}
Health and Lifecycle
message HeartbeatRequest {}
message HeartbeatResponse {
bool healthy = 1;
uint64 uptime_secs = 2;
}
message ShutdownRequest {
bool graceful = 1;
}
message ShutdownResponse {
bool accepted = 1;
}
RPC Reference
| RPC | Purpose | Request | Response |
|---|---|---|---|
CreateWorkspace | Create a git worktree or clone on the remote host | project, repo, branch, type | workspace path |
DestroyWorkspace | Remove a workspace directory | workspace path | — |
CreateRuntime | Launch an agent in a tmux session | command, env, cwd, name | runtime handle |
DestroyRuntime | Kill a running agent session | runtime handle | — |
SendMessage | Send text to a running agent | handle, message | — |
GetOutput | Read recent terminal output | handle, line count | output text |
IsAlive | Check if agent process is running | runtime handle | alive boolean |
Heartbeat | Health check | — | healthy, uptime |
Shutdown | Request daemon shutdown | graceful flag | accepted boolean |
Client Implementation
The orchestrator’s RemoteNode client in ennio-ssh handles:
- SSH tunnel establishment to the gRPC port
- Connection management and reconnection
- All RPC calls with proper error mapping to
EnnioError - Health checking before operations
Architecture Overview
System Design
Ennio follows a modular, plugin-driven architecture. The core orchestration loop is decoupled from specific agent implementations, runtimes, and external services through trait-based plugin slots.
┌─────────────────────────────────────────────────────┐
│ ennio-cli │
│ (clap CLI binary) │
├─────────────────────────────────────────────────────┤
│ ennio-services │
│ ┌──────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ SessionMgr │ │ LifecycleMgr│ │ EventBus │ │
│ │ (spawn/kill/ │ │ (poll/react/│ │ (broadcast│ │
│ │ restore) │ │ escalate) │ │ channel) │ │
│ └──────┬───────┘ └──────┬──────┘ └─────┬─────┘ │
├─────────┼─────────────────┼────────────────┼────────┤
│ │ ennio-plugins │ │
│ ┌──────┴──────────────────┴───────────────┴──────┐ │
│ │ Agent │ Runtime │ Workspace │ Tracker │ SCM │...│ │
│ └────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ ennio-core │ ennio-db │ ennio-ssh │
│ (types, │ (SQLite │ (SSH client, │
│ config, │ persist) │ strategies) │
│ traits) │ │ │
├────────────────┼─────────────┼───────────────────────┤
│ ennio-web │ ennio-nats │ ennio-ledger │
│ (REST API) │ (messaging)│ (cost tracking) │
└────────────────┴─────────────┴───────────────────────┘
Data Flow
Session Spawn
- CLI or API receives spawn request
- SessionManager resolves plugins for the project
- Workspace plugin creates isolated working directory
- Tracker plugin fetches issue details (if
--issueprovided) - Runtime plugin launches the agent
- Session is persisted to SQLite and an event is emitted
Lifecycle Polling
- LifecycleManager iterates over active sessions
- For each session, queries Tracker and SCM plugins for external state
- Compares external state against current session status
- Triggers status transitions and fires matching reactions
- Events are emitted to the EventBus (in-memory broadcast) and persisted to SQLite
- Events are optionally published to NATS for external consumers
Event System
Events flow through two channels:
- EventBus — tokio broadcast channel for in-process subscribers (web API, lifecycle manager)
- NATS — external messaging for distributed setups (multiple orchestrator instances, external monitoring)
- SQLite — persistent event log for history and debugging
Key Design Decisions
| Decision | Rationale |
|---|---|
| Trait-based plugins | Swap implementations without changing orchestration logic |
| SQLite over PostgreSQL | Zero-config, single-file database, embedded in the binary |
| Tokio broadcast for events | Lock-free, multi-consumer, backpressure-aware |
SSH via russh (pure Rust) | No dependency on system libssh, works cross-platform |
SecretString for tokens | Prevents accidental logging of secrets |
| Constant-time token comparison | Prevents timing attacks on API auth |
| Git worktrees as default workspace | Fast creation, shared git objects, minimal disk usage |
Crate Map
Ennio is a Rust workspace with 16 crates. This page describes each crate’s purpose, dependencies, and public API surface.
Dependency Graph
ennio-cli ──► ennio-services ──► ennio-core
│ │ ▲
│ ├──► ennio-db ─────┘
│ ├──► ennio-nats ───┘
│ └──► ennio-plugins ──► ennio-ssh ──► ennio-core
│ │
├──► ennio-web ─────────┘
├──► ennio-tui
└──► ennio-observe
ennio-node ──► ennio-proto ──► ennio-core
│ │
└──► ennio-ssh ┘
ennio-dashboard (standalone WASM, no workspace deps)
ennio-ledger ──► (standalone, rust_decimal)
ennio-ml ──► ennio-core
ennio-doc (documentation only)
Crate Details
ennio-core
Shared foundation crate. No heavy dependencies.
- Types:
SessionId,ProjectId,EventId(validated newtypes) - Config:
OrchestratorConfig,ProjectConfig,SshConnectionConfig - Session:
Session,SessionStatus(16 states),ActivityState - Events:
OrchestratorEvent,EventType(30 variants),EventPriority - Traits: All 7 plugin traits (
Agent,Runtime,Workspace,Tracker,Scm,Notifier,Terminal) - Paths: Workspace path construction utilities
ennio-services
Core orchestration logic.
- SessionManager — spawn, kill, restore, list, send
- LifecycleManager — poll loop, status transitions, reaction engine
- EventBus — tokio broadcast channel for in-process event distribution
- ConfigLoader — config file discovery and loading
- PluginRegistry — stores plugin instances for all 7 slots
ennio-plugins
Concrete plugin implementations.
- 4 agent plugins, 3 runtime plugins, 2 workspace plugins
- 2 tracker plugins, 1 SCM plugin, 3 notifier plugins, 1 terminal plugin
- See Plugin System for the full list
ennio-db
SQLite persistence layer.
- Connection pool with WAL journal mode
- SQL migrations (auto-run on startup)
- Repository functions: sessions, events, projects, metrics
- Uses
sqlxwith compile-time-unchecked queries
ennio-ssh
SSH client and remote execution.
- SshClient — connect, execute commands, upload/download files
- Strategies:
TmuxStrategy,TmateStrategy,RemoteControlStrategy - SshRuntime — runtime plugin backed by SSH + strategy
- RemoteNode — gRPC client for
ennio-nodeover SSH tunnel - Workspaces:
SshWorktreeWorkspace,SshCloneWorkspace
ennio-node
Remote gRPC daemon binary.
- Runs on remote machines, managed by orchestrator over SSH
- Accepts workspace creation, agent spawn, and health check RPCs
- Bearer token authentication (optional)
- Auto-shutdown after idle timeout
ennio-proto
Protobuf service definitions and generated code.
.protofiles defining the node servicetonic/prostcode generation viabuild.rs- Type conversions between proto types and
ennio-coretypes
ennio-web
REST API server.
- Built on
axum - 5 authenticated endpoints + 1 health check
- Bearer token middleware
- CORS support
ennio-tui
Terminal dashboard.
- Built on
ratatui+crossterm - Session table with color-coded status
- Detail panel and event log
- Keyboard-driven navigation
ennio-dashboard
Web dashboard (Dioxus WASM).
- Session cards with status, activity, branch, PR info
- Attention zones for sessions needing action
- Standalone WASM binary (no workspace crate deps)
ennio-nats
NATS messaging layer.
- Topic hierarchy builders and subscribe patterns
- Typed event publishing
- Client and subscription wrappers around
async-nats
ennio-ledger
Cost tracking and budgets.
Ledgerasync trait withInMemoryLedgerimplementation- Double-entry bookkeeping (accounts, transfers)
- Budget scopes (global/project/session) and periods (daily/monthly/total)
rust_decimal::Decimalfor monetary precision
ennio-ml
ML trait interfaces (no implementations).
SessionOutcomePredictor— predict success probability, duration, costAnomalyDetector— detect anomalous metricsCostPredictor— estimate remaining and total cost- Infrastructure-only: traits for future ML backing
ennio-observe
OpenTelemetry integration.
- Tracing subscriber setup
- OTLP exporter configuration
- Prometheus metrics endpoint
ennio-doc
Documentation crate.
- mdBook documentation source
- Build and generation tooling
Configuration Reference
Complete reference for ennio.yaml. All fields listed with types, defaults, and descriptions.
Top-Level Fields
| Field | Type | Default | Description |
|---|---|---|---|
port | u16 | 3000 | Web API listen port |
terminal_port | u16 | 3001 | Terminal WebSocket port |
direct_terminal_port | u16? | — | Direct terminal access port |
ready_threshold | Duration | 2s | Time before a session is considered ready (milliseconds in YAML) |
defaults | DefaultPlugins | see below | Global plugin defaults |
projects | [ProjectConfig] | [] | List of project configurations |
notifiers | [NotifierConfig] | [] | Notification channel definitions |
notification_routing | Map<String, [String]> | {} | Route reaction types to specific notifiers |
reactions | Map<String, ReactionConfig> | built-in set | Global reaction overrides |
database_url | String? | sqlite:ennio.db | SQLite database URL |
nats_url | String? | nats://127.0.0.1:4222 | NATS server URL |
api_token | SecretString? | — | Bearer token for API authentication |
cors_origins | [String] | [] | Allowed CORS origins |
DefaultPlugins
| Field | Type | Default | Description |
|---|---|---|---|
runtime | String | "tmux" | Default runtime plugin |
agent | String | "claude-code" | Default agent plugin |
workspace | String | "worktree" | Default workspace plugin |
notifiers | [String] | [] | Default notifier names |
ProjectConfig
| Field | Type | Default | Description |
|---|---|---|---|
name | String | required | Unique project identifier |
project_id | ProjectId? | — | Explicit project ID (auto-derived from name if omitted) |
repo | String | required | Git repository URL |
path | PathBuf | required | Absolute local path to the repository |
default_branch | String | "main" | Default git branch |
session_prefix | String? | — | Prefix for generated session IDs |
runtime | String? | — | Override default runtime |
agent | String? | — | Override default agent |
workspace | String? | — | Override default workspace |
tracker_config | TrackerConfig? | — | Issue tracker configuration |
scm_config | ScmConfig? | — | Source control configuration |
symlinks | [SymlinkConfig] | [] | Symlinks to create in workspace |
post_create | [String] | [] | Shell commands run after workspace creation |
agent_config | AgentSpecificConfig? | — | Agent-specific settings |
reactions | Map<String, ReactionConfig> | {} | Project-specific reaction overrides |
agent_rules | [String] | [] | Instructions passed to the agent |
max_sessions | u32? | — | Maximum concurrent sessions |
ssh_config | SshConnectionConfig? | — | Remote execution config (enables SSH mode) |
TrackerConfig
| Field | Type | Description |
|---|---|---|
plugin | String | Plugin name: "github" or "linear" |
config | Map<String, Value> | Plugin-specific configuration (e.g., owner, repo, token) |
tracker_config:
plugin: github
config:
owner: my-org
repo: my-repo
token: ${GITHUB_TOKEN}
ScmConfig
| Field | Type | Description |
|---|---|---|
plugin | String | Plugin name: "github" |
config | Map<String, Value> | Plugin-specific configuration (e.g., owner, repo, token) |
scm_config:
plugin: github
config:
owner: my-org
repo: my-repo
token: ${GITHUB_TOKEN}
NotifierConfig
| Field | Type | Description |
|---|---|---|
plugin | String | Plugin name: "desktop", "slack", or "webhook" |
name | String | Unique notifier identifier (used in routing rules) |
config | Map<String, Value> | Plugin-specific configuration |
notifiers:
- plugin: slack
name: team-slack
config:
webhook_url: ${SLACK_WEBHOOK_URL}
- plugin: webhook
name: ops-alerts
config:
url: https://hooks.example.com/ennio
- plugin: desktop
name: local
config: {}
ReactionConfig
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this reaction is active |
action | ReactionAction | "notify" | send_to_agent, notify, or auto_merge |
message | String? | — | Message to send (for send_to_agent) |
priority | EventPriority | "info" | Notification priority level |
escalate_after | Duration? | — | Seconds before escalating to notification |
threshold | Duration? | — | Time threshold before reaction triggers (e.g., idle detection) |
retries | u32 | 0 | Maximum retry attempts |
include_summary | bool | false | Include session summary in notification |
reactions:
ci-failed:
enabled: true
action: send_to_agent
message: "CI failed. Check the logs and fix the issues."
priority: action
retries: 3
escalate_after: 180
AgentSpecificConfig
| Field | Type | Description |
|---|---|---|
permissions | String? | Permission mode for the agent (e.g., agent-specific flags) |
model | String? | Model to use (e.g., "opus", "sonnet") |
passthrough | Map<String, Value> | Additional key-value pairs passed through to the agent |
agent_config:
model: opus
permissions: "--dangerously-skip-permissions"
passthrough:
max_turns: 200
SymlinkConfig
| Field | Type | Description |
|---|---|---|
source | PathBuf | Source path (absolute or relative) |
target | PathBuf | Target path in workspace |
SshConnectionConfig
| Field | Type | Default | Description |
|---|---|---|---|
host | String | required | Remote hostname |
port | u16 | 22 | SSH port |
username | String | required | SSH username |
auth | SshAuthConfig | required | Authentication method |
strategy | SshStrategyConfig | tmux | Remote execution strategy |
connection_timeout | Duration | 30s | SSH connection timeout (seconds in YAML) |
keepalive_interval | Duration? | — | SSH keepalive interval (seconds in YAML) |
host_key_policy | HostKeyPolicyConfig | strict | Host key verification policy |
known_hosts_path | PathBuf? | — | Path to known_hosts file |
node_config | NodeConnectionConfig? | — | Remote node daemon config |
SshAuthConfig Variants
Discriminated by the type field.
Key authentication:
auth:
type: key
path: ~/.ssh/id_ed25519
passphrase: ${SSH_PASSPHRASE} # optional
Agent authentication:
auth:
type: agent
Password authentication:
auth:
type: password
password: ${SSH_PASSWORD}
SshStrategyConfig
| Value | Description |
|---|---|
tmux | Create a tmux session on the remote host (default) |
tmate | Create a tmate session for shared terminal access |
remote_control | Use agent’s remote control protocol |
node | Deploy and communicate via gRPC ennio-node daemon |
HostKeyPolicyConfig
| Value | Description |
|---|---|
strict | Reject unknown or changed host keys (default) |
accept_new | Accept unknown keys, reject changed keys |
accept_all | Accept any key (insecure, for testing only) |
NodeConnectionConfig
| Field | Type | Default | Description |
|---|---|---|---|
port | u16 | 9100 | gRPC listen port on remote host |
idle_timeout | Duration | 3600s | Auto-shutdown after idle (seconds in YAML) |
workspace_root | PathBuf? | — | Root directory for workspaces |
ennio_binary_path | PathBuf? | — | Path to ennio-node binary on remote |
auth_token | SecretString? | — | Bearer token for gRPC auth |
Full Example
port: 3000
terminal_port: 3001
api_token: ${ENNIO_API_TOKEN}
database_url: sqlite:ennio.db
nats_url: nats://127.0.0.1:4222
cors_origins:
- http://localhost:3000
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
notifiers:
- local
notifiers:
- plugin: desktop
name: local
config: {}
- plugin: slack
name: team-slack
config:
webhook_url: ${SLACK_WEBHOOK}
notification_routing:
agent-exited:
- team-slack
- local
all-complete:
- local
reactions:
approved-and-green:
enabled: true
action: auto_merge
projects:
- name: backend
repo: git@github.com:org/backend.git
path: /home/user/repos/backend
default_branch: main
max_sessions: 3
session_prefix: be
tracker_config:
plugin: github
config:
owner: org
repo: backend
token: ${GITHUB_TOKEN}
scm_config:
plugin: github
config:
owner: org
repo: backend
token: ${GITHUB_TOKEN}
symlinks:
- source: ../.env
target: .env
post_create:
- cargo build
agent_config:
model: opus
passthrough:
max_turns: 200
agent_rules:
- "Write tests for all new code"
- "Use conventional commits"
- name: remote-ml
repo: git@github.com:org/ml-pipeline.git
path: /home/gpu-user/repos/ml-pipeline
agent: aider
ssh_config:
host: gpu-server.internal
username: gpu-user
auth:
type: key
path: ~/.ssh/id_ed25519
strategy: node
host_key_policy: accept_new
node_config:
port: 9100
workspace_root: /home/gpu-user/workspaces
auth_token: ${NODE_AUTH_TOKEN}
Event System
Ennio emits structured events for every significant state change. Events flow through multiple channels for real-time and historical access.
Event Structure
Every event contains:
| Field | Type | Description |
|---|---|---|
id | EventId | Unique identifier |
event_type | EventType | What happened |
priority | EventPriority | Severity level |
session_id | SessionId | Affected session |
project_id | ProjectId | Owning project |
timestamp | DateTime<Utc> | When it happened |
message | String | Human-readable description |
data | JSON | Structured payload |
Event Types
Session Events
| Type | Description |
|---|---|
SessionSpawned | New session created and agent launched |
SessionWorking | Agent began active work |
SessionExited | Agent process exited unexpectedly |
SessionKilled | Session manually terminated |
SessionRestored | Exited session restarted |
SessionCleaned | Session workspace cleaned up |
Status Events
| Type | Description |
|---|---|
StatusChanged | Session transitioned to a new status |
ActivityChanged | Session activity state changed |
Pull Request Events
| Type | Description |
|---|---|
PrCreated | Pull request created |
PrUpdated | Pull request updated (new commits) |
PrMerged | Pull request merged |
PrClosed | Pull request closed without merging |
CI Events
| Type | Description |
|---|---|
CiPassing | CI checks are green |
CiFailing | CI checks failed |
CiFixSent | Agent pushed a CI fix |
CiFixFailed | CI fix attempt also failed |
Review Events
| Type | Description |
|---|---|
ReviewPending | Awaiting code review |
ReviewApproved | PR approved |
ReviewChangesRequested | Reviewer requested changes |
ReviewCommentsSent | Review comments forwarded to agent |
Merge Events
| Type | Description |
|---|---|
MergeReady | PR ready to merge (approved + CI green) |
MergeConflicts | Merge conflicts detected |
MergeCompleted | PR merged successfully |
Reaction Events
| Type | Description |
|---|---|
ReactionTriggered | A reaction rule fired |
ReactionEscalated | Reaction escalated after timeout |
AllComplete | All project sessions completed |
Node Events
| Type | Description |
|---|---|
NodeConnected | Connected to remote node |
NodeDisconnected | Disconnected from remote node |
NodeLaunched | Remote node daemon started |
NodeHealthCheck | Node health check performed |
Event Priority
| Priority | Use Case |
|---|---|
Info | Status updates, completions |
Action | Something needs attention |
Urgent | Immediate human attention needed |
Critical | System-level failures |
Priorities are ordered: Info < Action < Urgent < Critical.
Event Channels
EventBus (In-Process)
Tokio broadcast channel with capacity 1024. Subscribers receive events in real-time. Used by the lifecycle manager, web API (SSE), and internal consumers.
#![allow(unused)]
fn main() {
let rx = event_bus.subscribe(EventType::CiFailing);
while let Ok(event) = rx.recv().await {
// handle event
}
}
NATS (Distributed)
Events are published to category-based NATS topics. Each event type maps to a topic category:
| Category | Topic Format | Event Types |
|---|---|---|
| Sessions | ennio.sessions.{project_id}.{action} | Spawned, Working, Exited, Killed, Restored, Cleaned, StatusChanged, ActivityChanged |
| Pull Requests | ennio.pr.{project_id}.{action} | PrCreated, PrUpdated, PrMerged, PrClosed |
| CI | ennio.ci.{project_id}.{action} | CiPassing, CiFailing, CiFixSent, CiFixFailed |
| Reviews | ennio.review.{project_id}.{action} | ReviewPending, ReviewApproved, ReviewChangesRequested, ReviewCommentsSent |
| Merge | ennio.merge.{project_id}.{action} | MergeReady, MergeConflicts, MergeCompleted |
| Reactions | ennio.reactions.{project_id}.{action} | ReactionTriggered, ReactionEscalated |
| Lifecycle | ennio.lifecycle.{action} | AllComplete |
| Nodes | ennio.node.{host}.{action} | NodeConnected, NodeDisconnected, NodeLaunched, NodeHealthCheck |
| Commands | ennio.commands.{command} | Shutdown and other control commands |
| Metrics | ennio.metrics.{action} | Metric collection events |
| Dashboard | ennio.dashboard.{action} | Dashboard update events |
External systems can subscribe to patterns:
ennio.sessions.my-project.*— all session events for a projectennio.ci.my-project.*— all CI events for a projectennio.node.build-server.*— all events from a remote nodeennio.lifecycle.*— all lifecycle events
Topic segments are validated: alphanumeric, underscore, and hyphen characters only. No spaces or dots within segments.
SQLite (Persistent)
All events are persisted to the events table for history, debugging, and replay. Query via the REST API or directly from the database.
Database Schema
Ennio uses SQLite with WAL journal mode for persistence. The database is created automatically on startup and migrations run on every boot.
Connection
database_url: sqlite:ennio.db # file-based (default)
database_url: sqlite::memory: # in-memory (data lost on restart)
The connection pool is configured with:
- WAL journal mode (concurrent reads)
- Foreign keys enabled
SqlitePoolfromsqlx
Tables
sessions
Stores session metadata and state.
| Column | Type | Default | Description |
|---|---|---|---|
id | TEXT PRIMARY KEY | — | Session ID (e.g., myapp-abc123) |
project_id | TEXT NOT NULL | — | Owning project |
status | TEXT NOT NULL | 'spawning' | Current SessionStatus |
activity | TEXT | — | Current ActivityState |
branch | TEXT | — | Git branch name |
issue_id | TEXT | — | Linked issue ID |
workspace_path | TEXT | — | Absolute path to workspace directory |
runtime_handle | TEXT | — | JSON-serialized runtime handle |
agent_info | TEXT | — | JSON-serialized agent session info |
agent_name | TEXT | — | Agent plugin name |
pr_url | TEXT | — | Pull request URL |
pr_number | INTEGER | — | Pull request number |
tmux_name | TEXT | — | Tmux session name |
config_hash | TEXT NOT NULL | — | Hash of config at spawn time |
role | TEXT | — | Session role |
metadata | TEXT NOT NULL | '{}' | JSON metadata |
created_at | TEXT NOT NULL | datetime('now') | ISO 8601 creation timestamp |
last_activity_at | TEXT NOT NULL | datetime('now') | ISO 8601 last activity timestamp |
restored_at | TEXT | — | ISO 8601 restore timestamp |
archived_at | TEXT | — | ISO 8601 archive timestamp |
events
Stores the event log.
| Column | Type | Default | Description |
|---|---|---|---|
id | TEXT PRIMARY KEY | — | Event ID |
event_type | TEXT NOT NULL | — | EventType variant name |
priority | TEXT NOT NULL | — | EventPriority variant name |
session_id | TEXT NOT NULL | — | Associated session (FK → sessions) |
project_id | TEXT NOT NULL | — | Owning project |
timestamp | TEXT NOT NULL | datetime('now') | ISO 8601 event timestamp |
message | TEXT NOT NULL | — | Human-readable description |
data | TEXT NOT NULL | '{}' | JSON payload |
projects
Stores project metadata.
| Column | Type | Default | Description |
|---|---|---|---|
project_id | TEXT PRIMARY KEY | — | Project ID |
name | TEXT NOT NULL | — | Project name |
repo | TEXT NOT NULL | — | Git repository URL |
path | TEXT NOT NULL | — | Local filesystem path |
default_branch | TEXT NOT NULL | 'main' | Default git branch |
config_hash | TEXT NOT NULL | — | Hash of project config |
created_at | TEXT NOT NULL | datetime('now') | ISO 8601 creation timestamp |
updated_at | TEXT NOT NULL | datetime('now') | ISO 8601 last update timestamp |
session_metrics
Stores per-session performance metrics.
| Column | Type | Default | Description |
|---|---|---|---|
session_id | TEXT PRIMARY KEY | — | Associated session (FK → sessions) |
total_tokens_in | INTEGER NOT NULL | 0 | Total input tokens consumed |
total_tokens_out | INTEGER NOT NULL | 0 | Total output tokens generated |
estimated_cost_usd | REAL NOT NULL | 0.0 | Estimated total cost in USD |
ci_runs | INTEGER NOT NULL | 0 | Number of CI runs |
ci_failures | INTEGER NOT NULL | 0 | Number of CI failures |
review_rounds | INTEGER NOT NULL | 0 | Number of review rounds |
time_to_first_pr_secs | INTEGER | — | Seconds from spawn to first PR |
time_to_merge_secs | INTEGER | — | Seconds from spawn to merge |
updated_at | TEXT NOT NULL | datetime('now') | ISO 8601 last update timestamp |
Indices
| Index | Table | Columns | Purpose |
|---|---|---|---|
idx_events_session_id | events | session_id | Fast event lookup by session |
idx_events_event_type | events | event_type | Fast event lookup by type |
idx_sessions_project_status | sessions | project_id, status | Fast session filtering |
Foreign Keys
events.session_id→sessions.id(ON DELETE CASCADE)session_metrics.session_id→sessions.id(ON DELETE CASCADE)
Migrations
Migrations are embedded in the binary and run automatically on startup. They are idempotent — using CREATE TABLE IF NOT EXISTS patterns. The migration order is:
- V1:
sessionstable - V2:
eventstable - V3:
projectstable - V4:
session_metricstable - V5: Performance indices
Querying
All database access goes through repository functions in ennio-db:
sessions::insert(pool, session)
sessions::get(pool, session_id) → Option<Session>
sessions::list(pool, project_filter) → Vec<Session>
sessions::update(pool, session)
sessions::delete(pool, session_id)
events::insert(pool, event)
events::get(pool, event_id) → Option<OrchestratorEvent>
events::list_by_session(pool, session_id) → Vec<OrchestratorEvent>
events::list_by_project(pool, project_id) → Vec<OrchestratorEvent>
projects::insert(pool, project)
projects::get(pool, project_id) → Option<ProjectRow>
projects::list(pool) → Vec<ProjectRow>
metrics::insert(pool, metrics)
metrics::get(pool, session_id) → Option<SessionMetricsRow>
metrics::update(pool, metrics)
All queries use parameterized bindings via sqlx::query().bind() — no SQL injection risk.