Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Spawning through Merged
  • 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

ConceptDescription
SessionA single agent working on a task in an isolated workspace
ProjectA repository configuration with plugin overrides and reaction rules
PluginA swappable implementation for one of 7 slots (agent, runtime, workspace, tracker, SCM, notifier, terminal)
ReactionA rule that fires when a session enters a specific state (e.g., CI failed → send logs to agent)
Lifecycle ManagerThe 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

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:

ToolRequired ByPurpose
gitAll workspacesClone repos, create worktrees, check status
tmuxTmuxRuntime, TmuxStrategyManage agent terminal sessions
sshSSH strategiesConnect to remote machines
tmateTmateStrategy onlyShared terminal sessions

Optional services:

ServicePurpose
NATSEvent messaging between components
SQLiteSession 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:

  1. Create an isolated workspace (git worktree by default)
  2. Run any post_create hooks (e.g., npm install)
  3. Launch the agent with the issue description or prompt
  4. 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:

EventDefault Reaction
CI failsSends failure logs to the agent (up to 2 retries)
Code review requests changesSends review comments to the agent
Merge conflicts detectedInstructs the agent to rebase
PR approved + CI greenNotifies you (or auto-merges if configured)
Agent exits unexpectedlySends 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

StateTerminalDescription
SpawningNoWorkspace being created, agent starting
WorkingNoAgent is actively writing code
PrDraftNoDraft pull request created
PrOpenNoPull request opened for review
CiPassingNoCI checks are green
CiFailedNoCI checks failed
CiFixSentNoAgent sent a fix for CI
CiFixFailedNoCI fix attempt also failed
ReviewPendingNoAwaiting code review
ChangesRequestedNoReviewer requested changes
ApprovedNoPR approved
MergeConflictsNoMerge conflicts detected
MergedYesPR merged successfully
DoneYesSession completed normally
ExitedNoAgent exited unexpectedly (can be restored)
KilledYesManually terminated

Activity States

Independent of the session status, each session has an activity state reflecting the agent process:

ActivityDescription
ActiveAgent is actively working
ReadyAgent is idle, waiting for input
IdleNo recent activity
WaitingInputAgent explicitly waiting for user input
BlockedBlocked on an external resource
ExitedAgent process has exited

Lifecycle Polling

The LifecycleManager runs a continuous polling loop that:

  1. Queries the tracker plugin for PR/CI status
  2. Queries the SCM plugin for review state
  3. Compares external state against the current session status
  4. Triggers status transitions
  5. Fires configured reactions
  6. Emits events to the event bus and database

Session Operations

OperationCLI CommandEffect
Spawnennio spawn <project>Creates workspace, starts agent
Sendennio send <session> <msg>Sends text to the running agent
Killennio session kill <id>Terminates the agent and marks session as Killed
Restoreennio session restore <id>Restarts an Exited session in its existing workspace
Infoennio session info <id>Shows session details, status history, recent events
Listennio 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

SlotTrait LocationPurpose
Agentennio-coreThe AI coding agent to run
Runtimeennio-coreHow the agent process is managed
Workspaceennio-coreHow the working directory is created
Trackerennio-coreIssue tracker integration (fetch issues, update status)
SCMennio-coreSource control (PR status, reviews, merge)
Notifierennio-coreWhere notifications are sent
Terminalennio-coreBrowser-based terminal access to sessions

Available Implementations

Agent Plugins

NameDescription
claude-codeAnthropic’s Claude Code CLI agent
aiderAider AI coding assistant
codexOpenAI Codex CLI
opencodeOpenCode agent

Runtime Plugins

NameDescription
tmuxRuns the agent inside a tmux session (default). Supports attach, send-keys, capture-pane.
processDirect child process. Simpler but no terminal attach.
sshRuns the agent on a remote machine via SSH + tmux.

Workspace Plugins

NameDescription
worktreeCreates a git worktree from the existing repo (default). Fast, shares git objects.
cloneFull 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

NameDescription
githubGitHub Issues — fetch issue details, create branches from issue titles
linearLinear — fetch issue details and metadata

SCM Plugins

NameDescription
githubGitHub PRs — check CI status, get reviews, merge PRs, auto-merge

Notifier Plugins

NameDescription
desktopDesktop notifications via notify-send (Linux) or osascript (macOS)
slackSlack incoming webhooks
webhookGeneric HTTP POST to any URL

Terminal Plugins

NameDescription
webWebSocket-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"
FieldDescription
modelModel to use (e.g., opus, sonnet)
permissionsPermission mode for the agent
passthroughAdditional 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

  1. The lifecycle manager detects a state change (e.g., CI failed)
  2. It looks up matching reaction rules for the session’s project
  3. It checks the retry count and escalation timeout
  4. It executes the action (send to agent, notify, auto-merge)
  5. If the action fails or the state persists beyond the escalation timeout, it escalates

Reaction Actions

ActionDescription
send_to_agentSends a message to the running agent with instructions
notifySends a notification through configured notifier plugins
auto_mergeMerges the PR via the SCM plugin

Default Reactions

Ennio ships with 9 built-in reactions:

KeyTriggerActionRetriesEscalation
ci-failedCI checks failsend_to_agent2120s
changes-requestedReviewer requests changessend_to_agent1800s
bugbot-commentsBot comments on PRsend_to_agent1800s
merge-conflictsMerge conflicts detectedsend_to_agent900s
approved-and-greenPR approved + CI greennotify
agent-stuckNo activity for thresholdnotify (urgent)600s
agent-needs-inputAgent waiting for inputnotify (urgent)
agent-exitedAgent process exitednotify (urgent)
all-completeAll project sessions donenotify (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

FieldTypeDefaultDescription
enabledbooltrueWhether this reaction is active
actionReactionAction"notify"What to do when triggered
messageString?Message sent to agent (for send_to_agent)
priorityEventPriority"info"Priority of emitted events
retriesu320Maximum retry attempts
escalate_afterDuration?Seconds before escalating
thresholdDuration?Idle time before triggering (e.g., agent-stuck)
include_summaryboolfalseInclude 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:

PriorityUse
InfoStatus updates, completions
ActionSomething needs attention soon
UrgentImmediate human attention required
CriticalSystem-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

  1. Ennio connects to the remote host via SSH (using russh)
  2. Creates a workspace on the remote machine (worktree or clone)
  3. Launches the agent inside a tmux/tmate session on the remote host
  4. Monitors the session by reading tmux pane output over SSH
  5. Optionally runs an ennio-node gRPC daemon for structured communication

SSH Strategies

StrategyDescription
tmuxCreates a tmux session on the remote host. Most reliable.
tmateCreates a tmate session for shared terminal access.
remote_controlUses claude remote-control for direct agent control.
nodeDeploys 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

PolicyBehavior
strictOnly connect if host key matches known_hosts
accept_newAccept and persist unknown keys, reject changed keys (default)
accept_allAccept 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?WorkspaceRuntimeCommunication
NoLocal worktree/cloneLocal tmux/processDirect
Yes, strategy: tmuxRemote via SSHRemote tmux via SSHSSH send-keys
Yes, strategy: nodeRemote via gRPCRemote via gRPCSSH-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:

FieldDescription
session_idWhich session incurred the cost
project_idWhich project the session belongs to
input_tokensNumber of input tokens consumed
output_tokensNumber of output tokens generated
cost_usdDollar cost of the API call
modelModel used (e.g., opus, sonnet)
timestampWhen 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:

ScopeDescription
GlobalTotal spend across all projects
ProjectSpend limit for a specific project
SessionSpend limit for a single session

Each budget has a period:

PeriodDescription
DailyResets every 24 hours
MonthlyResets every calendar month
TotalLifetime 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 fits
  • used / limit / remaining — current state
  • percent_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

PluginTransportConfiguration
desktopnotify-send (Linux) / osascript (macOS)No config needed
slackSlack incoming webhookwebhook_url
webhookHTTP POST to any URLurl

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:

PriorityMeaning
InfoStatus updates (e.g., all sessions complete)
ActionSomething needs attention soon
UrgentImmediate attention (e.g., agent exited, needs input)
CriticalSystem-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.

ParameterLocationRequiredDescription
project_idqueryNoFilter 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"
}
FieldTypeRequiredDescription
project_idStringYesProject to spawn in
issue_idStringNoIssue ID to work on (fetched from tracker)
promptStringNoDirect prompt for the agent
branchStringNoGit branch name to use
roleStringNoSession 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

StatusTrigger
200Success
400Invalid ID, configuration errors
402Budget exceeded
404Entity not found
409Entity already exists
504Operation timed out
500Internal 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]
ArgumentRequiredDescription
projectNoFilter sessions by project name

spawn

Spawn a new agent session.

ennio spawn <project> [OPTIONS]
OptionShortDescription
--issue-iIssue ID to work on (fetched from tracker)
--prompt-pDirect prompt for the agent
--branch-bGit branch name to use
--role-rSession 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>]
OptionDefaultDescription
--port3000Port 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]
OptionDefaultEnv VarDescription
--port9100gRPC listen port
--idle-timeout3600Seconds before auto-shutdown
--workspace-rootRoot directory for workspaces
--auth-tokenENNIO_NODE_AUTH_TOKENBearer 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

RPCPurposeRequestResponse
CreateWorkspaceCreate a git worktree or clone on the remote hostproject, repo, branch, typeworkspace path
DestroyWorkspaceRemove a workspace directoryworkspace path
CreateRuntimeLaunch an agent in a tmux sessioncommand, env, cwd, nameruntime handle
DestroyRuntimeKill a running agent sessionruntime handle
SendMessageSend text to a running agenthandle, message
GetOutputRead recent terminal outputhandle, line countoutput text
IsAliveCheck if agent process is runningruntime handlealive boolean
HeartbeatHealth checkhealthy, uptime
ShutdownRequest daemon shutdowngraceful flagaccepted 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

  1. CLI or API receives spawn request
  2. SessionManager resolves plugins for the project
  3. Workspace plugin creates isolated working directory
  4. Tracker plugin fetches issue details (if --issue provided)
  5. Runtime plugin launches the agent
  6. Session is persisted to SQLite and an event is emitted

Lifecycle Polling

  1. LifecycleManager iterates over active sessions
  2. For each session, queries Tracker and SCM plugins for external state
  3. Compares external state against current session status
  4. Triggers status transitions and fires matching reactions
  5. Events are emitted to the EventBus (in-memory broadcast) and persisted to SQLite
  6. 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

DecisionRationale
Trait-based pluginsSwap implementations without changing orchestration logic
SQLite over PostgreSQLZero-config, single-file database, embedded in the binary
Tokio broadcast for eventsLock-free, multi-consumer, backpressure-aware
SSH via russh (pure Rust)No dependency on system libssh, works cross-platform
SecretString for tokensPrevents accidental logging of secrets
Constant-time token comparisonPrevents timing attacks on API auth
Git worktrees as default workspaceFast 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 sqlx with 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-node over 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.

  • .proto files defining the node service
  • tonic/prost code generation via build.rs
  • Type conversions between proto types and ennio-core types

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.

  • Ledger async trait with InMemoryLedger implementation
  • Double-entry bookkeeping (accounts, transfers)
  • Budget scopes (global/project/session) and periods (daily/monthly/total)
  • rust_decimal::Decimal for monetary precision

ennio-ml

ML trait interfaces (no implementations).

  • SessionOutcomePredictor — predict success probability, duration, cost
  • AnomalyDetector — detect anomalous metrics
  • CostPredictor — 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

FieldTypeDefaultDescription
portu163000Web API listen port
terminal_portu163001Terminal WebSocket port
direct_terminal_portu16?Direct terminal access port
ready_thresholdDuration2sTime before a session is considered ready (milliseconds in YAML)
defaultsDefaultPluginssee belowGlobal plugin defaults
projects[ProjectConfig][]List of project configurations
notifiers[NotifierConfig][]Notification channel definitions
notification_routingMap<String, [String]>{}Route reaction types to specific notifiers
reactionsMap<String, ReactionConfig>built-in setGlobal reaction overrides
database_urlString?sqlite:ennio.dbSQLite database URL
nats_urlString?nats://127.0.0.1:4222NATS server URL
api_tokenSecretString?Bearer token for API authentication
cors_origins[String][]Allowed CORS origins

DefaultPlugins

FieldTypeDefaultDescription
runtimeString"tmux"Default runtime plugin
agentString"claude-code"Default agent plugin
workspaceString"worktree"Default workspace plugin
notifiers[String][]Default notifier names

ProjectConfig

FieldTypeDefaultDescription
nameStringrequiredUnique project identifier
project_idProjectId?Explicit project ID (auto-derived from name if omitted)
repoStringrequiredGit repository URL
pathPathBufrequiredAbsolute local path to the repository
default_branchString"main"Default git branch
session_prefixString?Prefix for generated session IDs
runtimeString?Override default runtime
agentString?Override default agent
workspaceString?Override default workspace
tracker_configTrackerConfig?Issue tracker configuration
scm_configScmConfig?Source control configuration
symlinks[SymlinkConfig][]Symlinks to create in workspace
post_create[String][]Shell commands run after workspace creation
agent_configAgentSpecificConfig?Agent-specific settings
reactionsMap<String, ReactionConfig>{}Project-specific reaction overrides
agent_rules[String][]Instructions passed to the agent
max_sessionsu32?Maximum concurrent sessions
ssh_configSshConnectionConfig?Remote execution config (enables SSH mode)

TrackerConfig

FieldTypeDescription
pluginStringPlugin name: "github" or "linear"
configMap<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

FieldTypeDescription
pluginStringPlugin name: "github"
configMap<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

FieldTypeDescription
pluginStringPlugin name: "desktop", "slack", or "webhook"
nameStringUnique notifier identifier (used in routing rules)
configMap<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

FieldTypeDefaultDescription
enabledbooltrueWhether this reaction is active
actionReactionAction"notify"send_to_agent, notify, or auto_merge
messageString?Message to send (for send_to_agent)
priorityEventPriority"info"Notification priority level
escalate_afterDuration?Seconds before escalating to notification
thresholdDuration?Time threshold before reaction triggers (e.g., idle detection)
retriesu320Maximum retry attempts
include_summaryboolfalseInclude 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

FieldTypeDescription
permissionsString?Permission mode for the agent (e.g., agent-specific flags)
modelString?Model to use (e.g., "opus", "sonnet")
passthroughMap<String, Value>Additional key-value pairs passed through to the agent
agent_config:
  model: opus
  permissions: "--dangerously-skip-permissions"
  passthrough:
    max_turns: 200

SymlinkConfig

FieldTypeDescription
sourcePathBufSource path (absolute or relative)
targetPathBufTarget path in workspace

SshConnectionConfig

FieldTypeDefaultDescription
hostStringrequiredRemote hostname
portu1622SSH port
usernameStringrequiredSSH username
authSshAuthConfigrequiredAuthentication method
strategySshStrategyConfigtmuxRemote execution strategy
connection_timeoutDuration30sSSH connection timeout (seconds in YAML)
keepalive_intervalDuration?SSH keepalive interval (seconds in YAML)
host_key_policyHostKeyPolicyConfigstrictHost key verification policy
known_hosts_pathPathBuf?Path to known_hosts file
node_configNodeConnectionConfig?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

ValueDescription
tmuxCreate a tmux session on the remote host (default)
tmateCreate a tmate session for shared terminal access
remote_controlUse agent’s remote control protocol
nodeDeploy and communicate via gRPC ennio-node daemon

HostKeyPolicyConfig

ValueDescription
strictReject unknown or changed host keys (default)
accept_newAccept unknown keys, reject changed keys
accept_allAccept any key (insecure, for testing only)

NodeConnectionConfig

FieldTypeDefaultDescription
portu169100gRPC listen port on remote host
idle_timeoutDuration3600sAuto-shutdown after idle (seconds in YAML)
workspace_rootPathBuf?Root directory for workspaces
ennio_binary_pathPathBuf?Path to ennio-node binary on remote
auth_tokenSecretString?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:

FieldTypeDescription
idEventIdUnique identifier
event_typeEventTypeWhat happened
priorityEventPrioritySeverity level
session_idSessionIdAffected session
project_idProjectIdOwning project
timestampDateTime<Utc>When it happened
messageStringHuman-readable description
dataJSONStructured payload

Event Types

Session Events

TypeDescription
SessionSpawnedNew session created and agent launched
SessionWorkingAgent began active work
SessionExitedAgent process exited unexpectedly
SessionKilledSession manually terminated
SessionRestoredExited session restarted
SessionCleanedSession workspace cleaned up

Status Events

TypeDescription
StatusChangedSession transitioned to a new status
ActivityChangedSession activity state changed

Pull Request Events

TypeDescription
PrCreatedPull request created
PrUpdatedPull request updated (new commits)
PrMergedPull request merged
PrClosedPull request closed without merging

CI Events

TypeDescription
CiPassingCI checks are green
CiFailingCI checks failed
CiFixSentAgent pushed a CI fix
CiFixFailedCI fix attempt also failed

Review Events

TypeDescription
ReviewPendingAwaiting code review
ReviewApprovedPR approved
ReviewChangesRequestedReviewer requested changes
ReviewCommentsSentReview comments forwarded to agent

Merge Events

TypeDescription
MergeReadyPR ready to merge (approved + CI green)
MergeConflictsMerge conflicts detected
MergeCompletedPR merged successfully

Reaction Events

TypeDescription
ReactionTriggeredA reaction rule fired
ReactionEscalatedReaction escalated after timeout
AllCompleteAll project sessions completed

Node Events

TypeDescription
NodeConnectedConnected to remote node
NodeDisconnectedDisconnected from remote node
NodeLaunchedRemote node daemon started
NodeHealthCheckNode health check performed

Event Priority

PriorityUse Case
InfoStatus updates, completions
ActionSomething needs attention
UrgentImmediate human attention needed
CriticalSystem-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:

CategoryTopic FormatEvent Types
Sessionsennio.sessions.{project_id}.{action}Spawned, Working, Exited, Killed, Restored, Cleaned, StatusChanged, ActivityChanged
Pull Requestsennio.pr.{project_id}.{action}PrCreated, PrUpdated, PrMerged, PrClosed
CIennio.ci.{project_id}.{action}CiPassing, CiFailing, CiFixSent, CiFixFailed
Reviewsennio.review.{project_id}.{action}ReviewPending, ReviewApproved, ReviewChangesRequested, ReviewCommentsSent
Mergeennio.merge.{project_id}.{action}MergeReady, MergeConflicts, MergeCompleted
Reactionsennio.reactions.{project_id}.{action}ReactionTriggered, ReactionEscalated
Lifecycleennio.lifecycle.{action}AllComplete
Nodesennio.node.{host}.{action}NodeConnected, NodeDisconnected, NodeLaunched, NodeHealthCheck
Commandsennio.commands.{command}Shutdown and other control commands
Metricsennio.metrics.{action}Metric collection events
Dashboardennio.dashboard.{action}Dashboard update events

External systems can subscribe to patterns:

  • ennio.sessions.my-project.* — all session events for a project
  • ennio.ci.my-project.* — all CI events for a project
  • ennio.node.build-server.* — all events from a remote node
  • ennio.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
  • SqlitePool from sqlx

Tables

sessions

Stores session metadata and state.

ColumnTypeDefaultDescription
idTEXT PRIMARY KEYSession ID (e.g., myapp-abc123)
project_idTEXT NOT NULLOwning project
statusTEXT NOT NULL'spawning'Current SessionStatus
activityTEXTCurrent ActivityState
branchTEXTGit branch name
issue_idTEXTLinked issue ID
workspace_pathTEXTAbsolute path to workspace directory
runtime_handleTEXTJSON-serialized runtime handle
agent_infoTEXTJSON-serialized agent session info
agent_nameTEXTAgent plugin name
pr_urlTEXTPull request URL
pr_numberINTEGERPull request number
tmux_nameTEXTTmux session name
config_hashTEXT NOT NULLHash of config at spawn time
roleTEXTSession role
metadataTEXT NOT NULL'{}'JSON metadata
created_atTEXT NOT NULLdatetime('now')ISO 8601 creation timestamp
last_activity_atTEXT NOT NULLdatetime('now')ISO 8601 last activity timestamp
restored_atTEXTISO 8601 restore timestamp
archived_atTEXTISO 8601 archive timestamp

events

Stores the event log.

ColumnTypeDefaultDescription
idTEXT PRIMARY KEYEvent ID
event_typeTEXT NOT NULLEventType variant name
priorityTEXT NOT NULLEventPriority variant name
session_idTEXT NOT NULLAssociated session (FK → sessions)
project_idTEXT NOT NULLOwning project
timestampTEXT NOT NULLdatetime('now')ISO 8601 event timestamp
messageTEXT NOT NULLHuman-readable description
dataTEXT NOT NULL'{}'JSON payload

projects

Stores project metadata.

ColumnTypeDefaultDescription
project_idTEXT PRIMARY KEYProject ID
nameTEXT NOT NULLProject name
repoTEXT NOT NULLGit repository URL
pathTEXT NOT NULLLocal filesystem path
default_branchTEXT NOT NULL'main'Default git branch
config_hashTEXT NOT NULLHash of project config
created_atTEXT NOT NULLdatetime('now')ISO 8601 creation timestamp
updated_atTEXT NOT NULLdatetime('now')ISO 8601 last update timestamp

session_metrics

Stores per-session performance metrics.

ColumnTypeDefaultDescription
session_idTEXT PRIMARY KEYAssociated session (FK → sessions)
total_tokens_inINTEGER NOT NULL0Total input tokens consumed
total_tokens_outINTEGER NOT NULL0Total output tokens generated
estimated_cost_usdREAL NOT NULL0.0Estimated total cost in USD
ci_runsINTEGER NOT NULL0Number of CI runs
ci_failuresINTEGER NOT NULL0Number of CI failures
review_roundsINTEGER NOT NULL0Number of review rounds
time_to_first_pr_secsINTEGERSeconds from spawn to first PR
time_to_merge_secsINTEGERSeconds from spawn to merge
updated_atTEXT NOT NULLdatetime('now')ISO 8601 last update timestamp

Indices

IndexTableColumnsPurpose
idx_events_session_ideventssession_idFast event lookup by session
idx_events_event_typeeventsevent_typeFast event lookup by type
idx_sessions_project_statussessionsproject_id, statusFast session filtering

Foreign Keys

  • events.session_idsessions.id (ON DELETE CASCADE)
  • session_metrics.session_idsessions.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:

  1. V1: sessions table
  2. V2: events table
  3. V3: projects table
  4. V4: session_metrics table
  5. 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.