Adapters
Inbound and outbound adapters that connect Irrlicht to the outside world.
Inbound Adapters (Event Sources)
Inbound adapters drive events into the application. Each watches a specific agent's transcript files or process activity and emits session lifecycle events that the core state machine consumes.
Claude Code Adapter
- Package:
core/adapters/inbound/agents/claudecode/ - Watches:
~/.claude/projects/**/*.jsonl - Uses the shared fswatcher with recursive directory watching (two-level tree:
root/<project>/<id>.jsonl) - Adapter name in session state:
"claude-code"
Codex Adapter
- Package:
core/adapters/inbound/agents/codex/ - Watches:
~/.codex/sessions/(deep nesting:YYYY/MM/DD/*.jsonl) - Uses the shared fswatcher with recursive directory watching
- Adapter name:
"codex" - Model detection from
~/.codex/config.toml
Pi Adapter
- Package:
core/adapters/inbound/agents/pi/ - Watches:
~/.pi/agent/sessions/(nested:--<cwd-dashed>--/*.jsonl) - Uses the shared fswatcher with recursive directory watching
- Adapter name:
"pi" - Model detection from
~/.pi/agent/settings.json - JSONL v3 format with session header, tree-structured entries, and multi-provider support
Aider Adapter
- Package:
core/adapters/inbound/agents/aider/ - Watches: per-CWD transcript file
.aider.chat.history.md(markdown, not JSONL) - Process discovery via
CommandLineMatch(/aider($| )) since the OS process ispython - Adapter name:
"aider" - Waiting state is pinned to a trailing
?contract on the most recent assistant block - Stage: alpha — first agent shipped through the
/ir:onboard-agentdiscovery flow
OpenCode Adapter
- Package:
core/adapters/inbound/agents/opencode/ - Watches: SQLite WAL at
~/.local/share/opencode/storage/opencode.db-walvia fsnotify; pollssession/parttables on writes - First adapter on the SQLite-backed monitoring path: storage is a single WAL-mode database, not JSONL
- Bypasses the JSONL tailer via a
MetricsReaderon itsProcessOwnedStoresource variant for cost + token snapshots - Parent-child session linking via
parent_idcolumn —EventNewSessioncarriesParentSessionIDdirectly, no path-based heuristics - CWD-based PID discovery via
pgrep -x opencode EventRemovedemitted onsession.time_archivedso the daemon transitions toreadyimmediately rather than waiting for TTLstep-finishreasons (stop,interrupted,length,error) all map toturn_done- Adapter name:
"opencode" - Stage: alpha — closes #100
Process Scanner
- Package:
core/adapters/inbound/agents/processlifecycle/ - Polls once per second per adapter via
pgrep -x <name>(exact-name match) orpgrep -f <regex>(command-line match, used by aider); interval backs off when the PID set is stable - Creates pre-sessions (
proc-<pid>) before transcripts exist - CWD discovery via
lsof - Suppresses ghost pre-sessions when a real transcript exists
Shared File System Watcher
- Package:
core/adapters/inbound/agents/fswatcher/ - Built on fsnotify (kqueue on macOS)
- Recursive directory watching
- Event types:
EventNewSession,EventActivity,EventRemoved - Max age filtering
- Race condition handling for new directories
Gas Town Orchestrator
- Package:
core/adapters/inbound/orchestrators/gastown/ - Watches daemon state file + polls
gtCLI - Role detection from session CWD paths
- State model: global agents, codebases, worktrees, workers, work units
Adapter Interfaces
Every inbound agent adapter is wired up by exporting a single agent.Agent declaration. The daemon's bootstrap in core/cmd/irrlichd/main.go consumes one []agent.Agent slice; core/cmd/irrlichd/wiring.go dispatches each declaration on its Source variant to build the right combination of fswatcher and process scanner. The per-projection helpers in core/adapters/inbound/agents/maps.go derive the parser, PID-discovery, process-name, subagent-count, and metrics-reader maps the rest of the daemon consumes.
An adapter package therefore satisfies the contract by exporting a single top-level Agent() function that returns a fully-populated agent.Agent. Watching the filesystem, scanning processes, and broadcasting events are handled by the shared infrastructure (core/adapters/inbound/agents/fswatcher/, core/adapters/inbound/agents/processlifecycle/) plus the per-adapter watcher when the source needs one (e.g. OpenCode's SQLite WAL watcher).
Adapter declaration — agent.Agent
Defined as a sealed-sum across three orthogonal axes in core/domain/agent/: Identity (name + branding), Process (how to find the OS process), and Source (where the conversation lives).
// Top-level declaration — Identity × Process × Source.
type Agent struct {
Identity Identity
Process Process
Source Source
}
type Identity struct {
Name string // adapter label on events, e.g. "claude-code"
DisplayName string // human-readable label, e.g. "Claude Code"
IconSVGLight string // raw <svg>…</svg> markup, 14×14 (light theme)
IconSVGDark string // raw <svg>…</svg> markup, 14×14 (dark theme)
}
// Process bundles process-recognition with session-to-PID resolution.
// PIDForSession lives here rather than on Source so every variant gets
// it uniformly.
type Process struct {
Match ProcessMatcher
PIDForSession PIDDiscoverFunc
}
type PIDDiscoverFunc func(
cwd, transcriptPath string,
disambiguate func([]int) int,
) (int, error)
// ProcessMatcher — sealed sum, exactly one of:
type ProcessMatcher interface{ isProcessMatcher() }
type ExactName struct {
Name string // e.g. "claude" — runs as `pgrep -x claude`
}
type CommandPattern struct {
Regex *regexp.Regexp // matched against the full command line
// via `pgrep -f`, e.g. "/aider($| )"
}
// Source — sealed sum, exactly one of:
type Source interface{ isSource() }
// JSONL transcripts under a fixed $HOME-relative directory tree
// (Claude Code, Codex, Pi).
type FilesUnderRoot struct {
Dir string // path relative to $HOME, e.g. ".claude/projects"
Parser FileParser // JSONLineParser or RawLineParser
}
// One transcript per running process inside its CWD (aider).
type FilesUnderCWD struct {
Filename string // basename only, e.g. ".aider.chat.history.md"
Parser RawLineParser // FilesUnderCWD always pairs with raw
}
// Session state lives in a structured store, typically SQLite
// (OpenCode → SQLite WAL).
type ProcessOwnedStore struct {
PathForPID func(pid int) string // resolves the store path for a PID
Reader MetricsReader // bypasses the JSONL-tailer path
}
// File-format dispatch carried inside FilesUnderRoot — JSONL or raw text.
type FileParser interface{ isFileParser() }
type JSONLineParser struct { NewParser func() LineParser }
type RawLineParser struct { NewParser func() RawParser }
Which canonical scenarios apply to an adapter is no longer a static per-adapter declaration. It is judged per (scenario, adapter) cell by the assess verb of the /ir:onboarding-factory skill across three pillars (agent capability / daemon sensor capture / driver capability), recorded in each cell's replaydata/agents/<name>/scenarios/<cell>/metadata.json. None of it is read by Go daemon code.
Transcript parser — tailer.TranscriptParser
Defined in core/pkg/tailer/parser.go. The base contract is one method; raw-text formats (e.g. aider's markdown history) implement the optional RawLineParser in addition.
// Required for every adapter.
type TranscriptParser interface {
// ParseLine maps one decoded JSONL line into a normalized event.
// Return nil to silently skip the line.
ParseLine(raw map[string]interface{}) *ParsedEvent
}
// Optional. Implemented by parsers whose source is not JSONL (aider).
// When present, the tailer skips its JSON pre-parse and hands the
// trimmed line to ParseLineRaw. ParseLine should be a nil-returning no-op.
type RawLineParser interface {
ParseLineRaw(line string) *ParsedEvent
}
Three further optional hooks are detected by type assertion inside the tailer; implement only the ones the adapter needs.
// Synthesize turn_done when the source format has no in-band end-of-turn marker
// (aider's TUI prompt is not written to the file). The tailer calls IdleFlush
// after each pass with the wall-clock idle time since the last line.
type IdleFlusher interface {
IdleFlush(idleFor time.Duration) *ParsedEvent
}
// Expose the in-progress turn's cost contribution so the live cost display
// reflects a streaming turn before it commits (Claude Code).
type PendingContributor interface {
PendingContribution() *PerTurnContribution
}
// Checkpoint and restore stateful per-turn accumulation across daemon
// restarts (Claude Code requestId cursor, Codex cumulative-usage cursor).
type ParserStateProvider interface {
GetParserLedger() ParserLedger
SetParserLedger(ParserLedger)
}
The parser's output type, tailer.ParsedEvent, is the normalized contract every transcript format collapses into — event type, tool deltas, token snapshot / per-turn contribution, model, CWD, task deltas, subagent completions, and the user-interrupt / tool-denial flags consumed by the state classifier. See core/pkg/tailer/parser.go for the full struct.
PID discovery — agent.PIDDiscoverFunc
Defined in core/domain/agent/discovery.go. Each adapter maps a live session back to the OS process that owns it; strategies vary (CWD-based for Claude Code, transcript-writer for Codex/Pi). The disambiguate callback picks one PID when multiple candidates match.
type PIDDiscoverFunc func(
cwd, transcriptPath string,
disambiguate func([]int) int,
) (int, error)
Watcher output — agent.Event
Defined in core/domain/agent/event.go. Events carry no adapter field; identity flows through the merge pipeline via Watcher.Identity() (core/ports/inbound/watcher.go).
type EventType string
const (
EventNewSession EventType = "new_session"
EventActivity EventType = "activity"
EventRemoved EventType = "removed"
)
type Event struct {
Type EventType
SessionID string // UUID portion of the filename (no .jsonl)
ProjectDir string // leaf directory under the watched root
TranscriptPath string // absolute path; for DB-backed adapters: "<db>?session=<id>"
Size int64
CWD string // working directory of the agent process
ParentSessionID string // empty unless this is a subagent
}
// The port the daemon consumes. Each per-watcher drain goroutine in
// SessionDetector.Run() captures Identity() once and wraps every event
// with it, so per-adapter identity is a property of the watcher rather
// than the event payload.
type Watcher interface {
Identity() agent.Identity
Watch(ctx context.Context) error
Subscribe() <-chan agent.Event
Unsubscribe(ch <-chan agent.Event)
}
Outbound Adapters (System Integration)
Outbound adapters handle persistence, process monitoring, real-time communication, and other side effects that flow out of the core.
Filesystem Repository
- Package:
core/adapters/outbound/filesystem/ - Persists session state as JSON files
- Atomic writes via temp file + rename
- Location:
~/Library/Application Support/Irrlicht/instances/
Process Watcher
- Package:
core/adapters/outbound/process/ - kqueue
EVFILT_PROCNOTE_EXITmonitoring - ~1ms exit detection latency
- Periodic liveness sweep as fallback
WebSocket Hub
- Package:
core/adapters/outbound/websocket/ - Fan-out to all connected clients
- Non-blocking sends (drops slow subscribers)
- Message types:
session_created/session_updated/session_deleted,orchestrator_state
Git Resolver
- Package:
core/adapters/outbound/git/ - Branch detection (strips
worktree-prefix) - Project name from
git-common-dir(worktree-aware) - CWD extraction from transcript tail (last 32KB)
Metrics Collector
- Package:
core/adapters/outbound/metrics/ - Wraps
TranscriptTailer - Extracts model, tokens, tool state, cost
mDNS Advertiser
- Package:
core/adapters/outbound/mdns/ - Bonjour/Zeroconf service advertisement
- Service type:
_irrlicht._tcp - Includes hostname and IPv4 in TXT records
Logger
- Package:
core/adapters/outbound/logging/ - Structured JSON logging
- Auto-rotation at 10MB, 5 files retained
Maturity Stages
Adapters and platforms in the compatibility grid share the same four-tier ladder: planned, alpha, beta, stable. Each tier states a common ladder (time, bug count, real-world use) once and then a small set of component-specific artefact checks. Every box can be ticked from public artefacts (the repo, GitHub issues, tagged releases) -- stage assignment reduces to running through the checklist.
The shape borrows from Kubernetes feature stages (alpha experimental, beta opt-out-able, GA stable) and the CNCF graduation criteria (real adopters, time at the previous tier).
planned
- Listed in the compatibility grid (README for adapters, homepage for platforms)
alpha — scaffolded, smoke path green
Common ladder:
- Code lives in the canonical location for its component type
- A documented smoke or replay path runs green from a clean checkout
For adapters, additionally:
- Adapter package at
core/adapters/inbound/agents/<name>/ - Exports an
agent.AgentfromAgent()with the rightSourcevariant (FilesUnderRoot/FilesUnderCWD/ProcessOwnedStore), parser, and PID-discovery hook - Registered in the
allAgentsslice incore/cmd/irrlichd/main.go - Emits all three lifecycle states:
working,waiting,ready replaydata/agents/<name>/capabilities.jsonpopulated againstreplaydata/agents/features.jsonbaseline-helloandfull-lifecycle-toolcallfixtures committed and replay green viatools/replay-fixtures.sh
For platforms, additionally:
- Builds and launches without manual intervention from the standard build script
- Smoke path works (renders sessions / receives daemon updates)
beta — feature-complete + 30 days real use
Common ladder:
- All
alphaboxes ticked - At least 30 days since first shipped in a tagged Irrlicht release
- At least one public report of real-world use by someone other than the author (GitHub issue, discussion, or PR comment)
- Zero open
bug-labeled issues against this component on ingo-eichhorst/Irrlicht
For adapters, additionally:
- All scenarios in
.claude/skills/ir:onboard-agent/scenarios.jsonwhoserequiresis covered by the adapter's capabilities replay green - Model + cost extraction wired through
core/adapters/outbound/metrics/ - Subagent / parent-child linkage emits
ParentSessionIDwhere the agent supports subagents
For platforms, additionally:
- All currently shipped Irrlicht features render correctly on this platform
stable — 6 months in use, code is settled
Common ladder:
- All
betaboxes ticked - At least 180 days since first shipped in a tagged Irrlicht release
- Zero
bug-labeled issues opened against this component in the last 30 days - Component code is settled:
git log <component-path> --since=30.days.agoshows no refactors or new features — only reactive changes (upstream format updates, new platform versions)
For adapters, additionally:
- Fixtures regenerated against the agent CLI version currently shipping upstream (matches the relevant entry in
min_versionsinscenarios.json)
Writing a New Adapter
The end-to-end path for adding an agent adapter -- from process discovery through fixture recording to stage promotion -- is documented under Adding a new agent adapter on the Contributing page. That section drives the /ir:onboard-agent skill, which produces the canonical scenario fixtures used by the criteria above.