System Design

Hexagonal architecture with clean ports and adapters.

Overview

Irrlicht follows hexagonal (ports-and-adapters) architecture. Every external dependency is behind an interface. Every component is testable in isolation.

┌─────────────────────────────────────────┐ │ HTTP/WebSocket Interface │ │ GET /api/v1/sessions(/stream), /state │ └────────────────────┬────────────────────┘ │ ┌────────────────────┴────────────────────┐ │ Application Layer (Services) │ │ SessionDetector · PushService │ │ OrchestratorMonitor │ └────────────────────┬────────────────────┘ │ ┌────────────────────┴────────────────────┐ │ Domain Model │ │ SessionState · SessionMetrics │ │ agent.Event · orchestrator.State │ └────────────────────┬────────────────────┘ │ ┌────────────────────┴────────────────────┐ │ Inbound Adapters │ Outbound Adapters│ │ Claude Code │ Filesystem │ │ Codex · Pi · Aider │ WebSocket Hub │ │ OpenCode (SQLite) │ kqueue Process │ │ Process Scanner │ mDNS · Git · Log │ │ Gas Town │ │ └──────────────────────┴───────────────────┘

Project Structure

core/
├── cmd/
│   ├── irrlichd/          # Daemon entry point
│   └── irrlicht-ls/       # CLI listing tool
├── domain/                # Pure domain types
│   ├── session/           # SessionState, SessionMetrics
│   ├── agent/             # Agent event types
│   ├── orchestrator/      # Orchestrator state
│   └── config/            # Configuration
├── ports/
│   ├── inbound/           # Watcher, OrchestratorWatcher
│   └── outbound/          # Repository, Logger, ProcessWatcher, etc.
├── adapters/
│   ├── inbound/           # Event sources
│   └── outbound/          # System integrations
├── application/services/  # Orchestration logic
└── pkg/                   # Shared utilities (tailer, capacity)

Port Interfaces

Inbound Ports

type Watcher interface {
    Identity() agent.Identity
    Watch(ctx context.Context) error
    Subscribe() <-chan agent.Event
    Unsubscribe(ch <-chan agent.Event)
}

Each watcher carries the identity of the adapter that produced it (Identity().Name — e.g. "claude-code"). The SessionDetector's merge pipeline captures Identity() once per watcher and wraps every event with it, so adapter identity is a property of the watcher, not the event payload.

Outbound Ports

Key interfaces that define outbound boundaries:

  • SessionRepository — Persist and retrieve session state
  • ProcessWatcher — Monitor process lifecycle (kqueue)
  • MetricsCollector — Compute session metrics from transcripts
  • GitResolver — Resolve project names from git repositories
  • PushBroadcaster — Fan-out state changes to connected clients
  • Logger — Structured logging abstraction

Component Wiring

The main.go entry point wires all components together following dependency injection:

  1. Logger — structured JSON output
  2. Config — loaded from environment variables
  3. SessionRepository — filesystem adapter for state persistence
  4. GitResolver — resolves project names via git commands
  5. MetricsCollector — transcript tailer for session metrics
  6. ProcessWatcher — process exit detection behind a portable ports.ProcessObserver seam (build-tagged process_{darwin,linux,other}.go): kqueue on macOS, pidfd on Linux. Adds the Linux daemon without touching any adapter.
  7. Watcherscmd/irrlichd/wiring.go dispatches on each agent.Agent's Source variant: FilesUnderRoot adapters (Claude Code, Codex, Pi) get a shared fswatcher plus a process scanner; FilesUnderCWD (Aider) and ProcessOwnedStore (OpenCode) adapters get a process scanner only. Every watcher exposes Identity() so per-event adapter naming flows through the merge pipeline rather than being stamped at emit time.
  8. SessionDetector — thin coordinator that delegates to:
    • StateClassifier — pure functions for working/waiting/ready transitions
    • MetadataEnricher — git metadata, CWD, model, and metrics resolution
    • PIDManager — PID discovery with retry/backoff, process exit handling, liveness sweeps, subagent cleanup
  9. PushService — WebSocket fan-out to connected clients
  10. HTTP Server — API endpoints + embedded web UI

Data Flow

  1. Inbound adapter emits agent.Event
  2. SessionDetector receives event, creates or updates session
  3. MetricsCollector computes metrics from transcript
  4. State machine determines working / waiting / ready
  5. PushService broadcasts change via WebSocket
  6. SessionRepository persists to filesystem

Performance

Metric Value
Memory <5 MB typical footprint
Detection latency ~50–200 ms via FSEvents
Process exit ~1 ms via kqueue
Concurrency Tested up to 8 simultaneous sessions
Disk (state files) <100 KB
Disk (logs) <50 MB

Safety Guarantees

  • Zero configuration — works out of the box with no setup required
  • Idempotent operations — safe to restart at any time
  • Non-destructive — never corrupts existing configs or data
  • Atomic writes — temp file + rename pattern prevents partial writes