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:onboarding-factorydiscovery flow
OpenCode Adapter
- Package:
core/adapters/inbound/agents/opencode/ - Watches: SQLite WAL at
~/.local/share/opencode/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,content-filter) all map toturn_done- Adapter name:
"opencode" - Stage: alpha — closes #100
Kiro CLI Adapter
- Package:
core/adapters/inbound/agents/kirocli/ - Watches: JSONL transcripts at
~/.kiro/sessions/cli/<uuid>.jsonl(one file per session, live-appended) - Versioned event envelope (
Prompt/AssistantMessage/ToolResults/Clear); no explicit end-of-turn marker — a text-onlyAssistantMessagemaps toturn_done, one carryingtoolUseblocks keeps the turn open - The transcript carries no cwd; the working directory is read from the
<uuid>.jsonmetadata sidecar next to it - CWD-based PID discovery via
pgrep -x kiro-cli(thekiro-cli chatparent owns the session cwd; the transcript is not kept open between writes) /clearcontinues in the same session file (aClearevent, no new UUID); headless--no-interactiveruns persist no session files and are invisible- Adapter name:
"kiro-cli" - Stage: alpha — closes #280
Gemini CLI Adapter
- Package:
core/adapters/inbound/agents/geminicli/ - Watches: JSONL transcripts under
~/.gemini/tmp/(session files namedsession-<timestamp>-<hash>; nested subagent chats atchats/<parent-uuid>/<child-uuid>.jsonl) - Process discovery via command-line match on
bin/gemini(the OS process isnode); the working directory is read from the transcript body, not a header line - No explicit end-of-turn marker — a text-only assistant message maps to
turn_done, while benign info notices (e.g. "Model set to gemini-2.5-flash") are ignored - Adapter name:
"gemini-cli" - Stage: alpha
Antigravity Adapter
- Package:
core/adapters/inbound/agents/antigravity/ - One adapter covers both the
agyCLI and the Antigravity IDE — they write byte-compatible transcripts to sibling brain stores - Watches:
transcript.jsonlunder~/.gemini/antigravity-cli/brain/<conversation>/.system_generated/logs/(CLI) and~/.gemini/antigravity/brain/<conversation>/.system_generated/logs/(IDE) - Discovery is transcript-first: the IDE hosts many conversations in one Electron process with no per-conversation PID, so PID-less sessions are first-class; the
agyCLI is a standalone native binary matched by exact name - Because the transcript filename is the constant
transcript.jsonl, the session id is derived from the conversation directory path - Tokens and the canonical model are read from the sibling conversation store
~/.gemini/antigravity{,-cli}/conversations/<conversation>.db(not the transcript) so the context bar renders - Adapter name:
"antigravity" - Stage: alpha
Mistral Vibe Adapter
- Package:
core/adapters/inbound/agents/vibe/ - Watches:
messages.jsonlunder~/.vibe/logs/session/<session-id>/ - Because the transcript filename is the constant
messages.jsonl, the session id is derived from the parent directory name - Process discovery via command-line match:
vibeis a Python console-script, so the OS process name is the interpreter and an exact-name match would never fire - Tool calls use the OpenAI nested
function.nameshape rather than a flatname - The transcript carries no timestamp, working directory, model, or usage — those are read from the sibling
meta.jsonsidecar (config.active_model,config.auto_compact_threshold,stats.context_tokens), memoized by (mtime, size), so the model chip and context bar render - Opts into queued-turn boundary detection: vibe drains a mid-turn follow-up synchronously the instant the prior turn finishes, a genuinely distinct turn with no observable ready gap
- Adapter name:
"mistral-vibe" - Stage: alpha
GitHub Copilot Adapter
- Package:
core/adapters/inbound/agents/copilot/ - Watches:
events.jsonlunder~/.copilot/session-state/<session-id>/, relocatable via$COPILOT_HOME(which supersedes the deprecated--config-dir) - Because the transcript filename is the constant
events.jsonl, the session id is derived from the parent directory name - Every record shares one envelope —
{id, parentId, timestamp, type, data}— so parsing is a dispatch ontyperather than a shape guess - Turn boundaries are explicit (
assistant.turn_start/assistant.turn_end), soworking/readyride on real records instead of a quiet-tail heuristic. Copilot closes the turn after every tool call and opens a continuation, which the parser treats as continued work rather than a settle - Permission prompts route to
waitingfrom the transcript itself (permission.requested/permission.completed, paired onrequestId) — no hook installation required /compactwritessession.compaction_start/session.compaction_completeas ordinary persisted records, so the session holdsworkingacross the summarization call without needing a hook-driven overlay- The
!shell escape runs locally with no model turn; its injected records are skipped so a settled session is not bounced back toworkingwith nothing left to close it - Tokens and cost are live from
assistant.message.outputTokensper message —session.usage_checkpointcarries no counts, and the cumulative block exists only atsession.shutdown - In-process subagents (
task/explore) are counted on the parent: Copilot creates no child session directory and appends every subagent record to the parent's ownevents.jsonl, distinguished by a top-levelagentId - The adapter is observe-only, gated behind a single
transcriptspermission - Adapter name:
"copilot" - Stage: alpha
Hermes Agent Adapter
- Package:
core/adapters/inbound/agents/hermes/ - Reads: the shared SQLite store
$HERMES_HOME/state.db(default~/.hermes/state.db) — Hermes writes no transcript files, so there is nothing per-session to watch. Opened read-only throughcore/pkg/sqlitero, neversql.Opendirectly - Every session — CLI, TUI, and the messaging-gateway platforms — lands in the same database: a
sessionstable (one row per session, carrying aggregate tokens, cost, cwd and parent linkage) and amessagestable (one row per message, OpenAI-shaped) - A session row is INSERTed while the session is still live — roughly 2s after launch, with
ended_atNULL — so the store is a live discovery source, not a post-hoc archive finish_reasonon assistant rows is an explicit turn boundary:stopends the turn,tool_callsmeans more work follows, so no idle-flush heuristic is neededsessions.cwdis populated only for TUI sessions — the interactivehermes chatREPL recordssource='cli'and carries an empty cwd, so the project binding is borrowed from the live process instead- Process discovery via command-line match:
hermesis a Python console-script, so the OS process name is the interpreter and an exact-name match would never fire - Second
agent.ProcessOwnedStoreadapter after opencode, and the first whose store is shared across every surface the agent has - Writes (behind the
hooksconsent): three shell-hook entries in a marked block of$HERMES_HOME/config.yaml, plus the matching approvals in$HERMES_HOME/shell-hooks-allowlist.json— Hermes does not run a configured hook until it is approved. No code is installed: each entry runsirrlichd hook-post hermes, which resolves the daemon’s address at fire time pre_approval_request/post_approval_responseare the reason the channel exists: Hermes holds a pending approval in process memory andstate.dbhas no approval table, sowaitingis not reachable from the store at any polling interval.on_session_endfires once per turn and carriescompleted/interrupted- Adapter name:
"hermes" - Stage: alpha
Muse Adapter
- Package:
core/adapters/inbound/agents/muse/ - Watches:
session.jsonlunder~/.local/share/muse/sessions/<YYYY>/<MM>/<DD>/<session-id>/; the filename is a constant, so the session ID comes from the parent directory rather than the file - Subagents are nested child copies at
<parent-session-dir>/subagent/<child-id>/session.jsonl, so the parent link falls out of directory nesting with no file read - Binds the PID from each session’s own
.session.lockstamp. Process discovery matches themuse-bin-prefix on the command line, because the OS process name embeds the version and build (muse-bin-1.2.1-R2847.1) and themuselauncherexecs that pinned binary before a session starts — the wrapper is never observed running - Records are
payload_type-discriminated envelopes. Some lines are instead aretained_framethat batches several independently re-encoded inner records; each child is routed through the same dispatcher a top-level line uses, then merged into the single verdict the one-event-per-line parser contract requires - The context-window limit comes from muse’s own locally cached
~/.local/share/muse/model-catalog/files, not from the transcript, which never carries it - The
session-index.dbsidecar is deliberately never read — it was measured out of sync with the live session set, so it is not a reliable source for anything - The adapter is observe-only, gated behind a single
transcriptspermission. Muse offers no hook or control path - Account quota (separate consent row, “Muse account quota”): the Meta quota chip comes from Meta’s account API, not the transcript. It is declared on its own
muse-account-apipseudo-adapter under theaccount-apipermission (core/adapters/outbound/museaccountapi/). Once granted, the daemon reads the Muse OAuth credential (~/.config/muse/auth.json, or theai.meta.dev.credentialsmacOS Keychain item) once per grant, and a sweep every 60 seconds refreshes each live top-level Muse session’s quota, callingPOST https://api.meta.ai/muse-code/keyonly when the cached reading is older than 5 minutes. Revoking it stops further credential reads and requests and discards the cached reading - Adapter name:
"muse" - Stage: alpha
DeepSeek Harness Adapter
- Package:
core/adapters/inbound/agents/dsh/ - Watches:
session.v<N>.jsonl.zstdunder~/.dsh/sessions/<workspace>/session-<uuid>/, relocatable via$DSH_HOME(then$DSH_HOME/sessions/); only the highest transcript generation in a session directory is read - Transcripts are concatenated, checksummed zstd frames; the shared tailer decodes them before the parser sees the JSONL records
- The session ID comes from the
session-<uuid>directory rather than the file. A native in-process subagent is linked to its parent from its transcript’s first record (origin: subagentplusparentSession); forks carry a parent too but stay top-level - Process discovery via command-line match on
dsh, because the OS process name isnode. The PID is bound by checking which process holds the session’s siblingsession.lockopen for writing, and concurrent sessions that share one PID stay separate - DeepSeek Harness’s own one-shot Codex and Claude Code provider processes are recognised by argv and parent PID, so they do not show up as duplicate sessions
- The adapter is observe-only, gated behind a single
transcriptspermission - Adapter name:
"dsh" - Stage: alpha
Junie Adapter
- Package:
core/adapters/inbound/agents/junie/ - Watches:
events.jsonlunder~/.junie/sessions/<session-id>/; skips the rootindex.jsonl, siblingstate.jsonandtranscript.mdfiles, and nestedtask-*directories - Derives the session ID from the parent directory because every transcript uses the same
events.jsonlfilename - Reads
~/.junie/processes/<pid>-<session-id>-<hash>.jsonsidecars to bind the PID, session, and project. The neweststartedAtsidecar owns a shared PID. The adapter checks process liveness and the Junie command line before it trusts a sidecar - Uses
TaskStateas the explicit turn boundary. Unknown event kinds are skipped because the Junie event schema is not versioned - Declares one read-only
transcriptspermission. Junie provides no hook or control path - Adapter name:
"junie" - Stage: planned — the adapter exists, but recordings have not earned its maturity claims yet
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, shared-PID-owner, released-PID, process-name, argv-exclusion, session-host, launcher-PID, known-host, 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 in core/domain/agent/ as three orthogonal axes plus a permission list: Identity (name + branding), Process (how to find the OS process), Source (a sealed sum: where the conversation lives), and Permissions (the consent-gated capabilities the daemon may exercise).
// Top-level declaration — Identity × Process × Source × Permissions.
type Agent struct {
Identity Identity
Process Process
Source Source
Permissions []Permission // consent-gated capabilities the daemon may exercise
}
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
SharedPIDOwner SharedPIDOwnerFunc // optional: keep a root that still proves it owns a PID another root shares
ReleasedPID ReleasedPIDFunc // optional: end a root whose long-lived host process released its transcript (#2080)
LauncherPID LauncherPIDFunc // optional: process whose env + tty feed a hosted session's launcher (#2083)
ExcludeArgv func(argv []string) bool // reject a matched PID by argv shape; nil = never exclude
SessionHostArgv func(argv []string) bool // recognize a shared session host (the codex app-server, #2077): no pre-session, roots may bind to it
RequireKnownHost bool // gate admission on OS-level process ancestry (issue #784)
}
type PIDDiscoverFunc func(
cwd, transcriptPath string,
disambiguate func([]int) int,
) (int, error)
type SharedPIDOwnerFunc func(cwd, transcriptPath string, pid int) bool // inconclusive = false
type ReleasedPIDFunc func(cwd, transcriptPath string, pid int) bool // true only for a definitive "no longer holds it"
type LauncherPIDFunc func(cwd, transcriptPath string, pid int) int // 0 = keep pid
// 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
DirFunc func() string // optional resolver that replaces Dir, run only once observe consent is granted
DirByOS map[string]string // per-GOOS override of Dir, e.g. Windows %APPDATA%
ExtraDirs []string // additional roots watched alongside Dir
SessionIDFromPath func(path string) string // override: derive session ID from path, not filename stem
ParentSessionIDFromPath func(path string) string // derive a session's parent from its transcript; "" = top-level
}
// 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 }
// Permission declares one consent-gated capability: what it touches, what
// feature it unlocks, and (for modify-kind entries) how to apply/undo it.
// Nothing is exercised until the user grants it (issue #570).
type Permission struct {
Key string // stable identifier, e.g. "hooks", "transcripts"
Kind permission.Kind // modify (writes a file) or observe (reads only)
Title string // short label for the wizard row
FeatureUnlocked string // what granting enables
Touches string // what the daemon reads/writes when granted
Detail string // expanded (i) text: exact paths/commands, how to undo
Apply func() error // performs the modification on grant (nil for observe-kind)
Remove func() error // undoes it on revoke (nil for observe-kind)
Writes *ManagedUserFile // non-nil when Apply writes a shared, user-owned file (follows $HOME, not IRRLICHT_HOME)
Advisory []AdvisoryWrite // paths Apply is known to cause to be written that hold no irrlicht content (issue #1748)
}
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
}
Further optional hooks are detected by type assertion inside the tailer; implement only the ones the adapter needs. Three are shown below; the full set (also queuedTurnSplitter, rotationResetter and TranscriptPathAware) is in core/pkg/tailer/parser.go.
// 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
Terminal bool // true when this activity event ends the agent's turn (fast-path debounce)
Synthetic bool // true when injected by a hook handler, not a real transcript write (issue #905)
}
// 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/inbound/agents/processlifecycle/ - 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,focus_requested,permissions_updated, and thehistory_*_v2frames (full list: API Reference →GET /api/v1/sessions/stream)
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.
Since #1369 each adapter tier above planned has a machine-checked floor alongside its human criteria (platform tiers remain editorial). The floor is a set of core scenarios drawn from the 50 in the matrix — twelve of them, chosen for discriminating power rather than coverage (the set and the reasoning per scenario live in tools/onboarding-factory/internal/matrix/vocabulary.go). A scenario settles when it is recorded and observed, or when replaydata/agents/adapters.json declares a capability the adapter structurally lacks. The other 38 scenarios are optional and never block a promotion. of validate fails when an adapter claims a tier its core standing has not earned; claiming less is always allowed, because the time-and-adoption criteria below are invisible to it.
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
All()slice incore/adapters/inbound/agents/all.go - Emits the ordinary lifecycle states:
working,waiting,ready(erroradditionally requires a failure signal the adapter can parse) - Scenario cells assessed via the
assessverb of/ir:onboarding-factory, recorded inreplaydata/agents/<name>/scenarios/<cell>/metadata.json - Machine-checked (#1369): the four alpha-floor core scenarios are settled —
session-start,basic-turn,auto-executed-tool-call,turn-end-terminal-text. All four are reachable from lifecycle hooks alone, which is what makesalphaa day-one tier for an adapter that has hooks and no transcript parser: it asserts state only, no metrics. - An entry in
replaydata/agents/adapters.jsondeclaring this maturity and any missing capabilities.of validatefails if the claim exceeds what the core scenarios earn. - The alpha-floor cells recorded, committed and replay green via
tools/replay-fixtures.sh(1-1_session-start,2-1_basic-turn,2-2_auto-executed-tool-call,2-13_turn-end-terminal-text)
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:
- Machine-checked (#1369): all nine state-core scenarios settled — the four alpha-floor ones plus
session-end,long-idle-live-session,user-blocking-question,turn-aborted-by-error,multiple-sessions-same-cwd. Still no metrics scenario: that is thebeta/stableline. - Remaining scenario cells assessed — verdict recorded per cell in
replaydata/agents/<name>/scenarios/<cell>/metadata.json. Since #1369 these 38 non-core scenarios are optional: they are still assessed, recorded and rendered, but they do not hold a promotion back. - 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) - Machine-checked (#1369), adapters: all twelve core scenarios settled — the nine state-core plus the three metrics-core ones,
model-identification,token-accounting,model-context-display. This is the only tier that asserts anything about tokens, cost or model identity.
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:onboarding-factory skill, which produces the canonical scenario fixtures used by the criteria above.