Statusline and observability
Castellan exposes complementary observability surfaces that share one schema:
statusline.json—ObservabilitySnapshot(file + MCP-readable)castellan run --json— typedcastellan-eventsNDJSON streamcastellan telemetry --json— SQLite history (sessions, vitals, cost rollups) for CI/metrics
TUI (castellan dashboard) and headless (castellan statusline refresh) write the same snapshot — no parallel JSON schemas.
statusline.json
Default path: ~/.config/castellan/statusline.json.
| Writer | When |
|---|---|
castellan dashboard | Every ~250ms while the TUI is open |
castellan statusline refresh | One-shot (or --watch) without ratatui |
{
"schema_version": 1,
"written_at": "2026-07-18T04:00:00+00:00",
"connected": true,
"socket": "/Users/you/.config/castellan/castellan.sock",
"bus": { "socket": "up", "events": "live", "panes": "socket" },
"agents": { "total": 3, "idle": 1, "working": 1, "blocked": 0, "done": 1, "unknown": 0 },
"selected": { "pane_id": "w1:p1", "agent": "claude", "status": "working" },
"vitals": { "circulation_score": 0.82, "deferred_ratio": 0.1, "cold_zone_count": 0, "pane_pulse": 0.4 },
"genome": { "genome_id": "gen-abc123", "generation": 4, "lineage_depth": 2 },
"budget": { "mcp_headroom": 40, "zone_headroom": 12, "deferred_ratio": 0.1 },
"session_cost": { "usd": 0.42, "tokens": 18300 },
"live_pressure": { "pane": 0.55 },
"plan_verify": { "passed": false, "tool": "shell", "reason_code": "missing_plan" },
"bio": { "kind": "quorum_fired", "summary": "phase=commit ρ=0.80" },
"topology_apply": { "tick": 3, "born": 1, "pruned": 0, "wake_threshold": 0.4 },
"potential_delta": null,
"telemetry": { "agent_transitions": 42 },
"last_events": []
}
potential_delta (Φ) is omitted or null unless the wire exposes a real value — never a fake number. Footer shows Φ=— when unknown.
Dual-bus honesty (bus)
| Field | Meaning |
|---|---|
socket | up / down — mux Unix socket ping |
events | live / stale / idle — EventBus activity |
panes | socket — Enter→attach uses the socket pane tree (in-process --mux run panes are a separate host; events may still fan out on the dual bus) |
Read paths
| Consumer | How |
|---|---|
| MCP | castellan_multiplexer_status — returns schema_version, written_at, age_secs, stale, plus status body |
| Headless / CI | castellan statusline refresh --json then jq .schema_version |
| History DB | castellan telemetry --limit 20 (or dashboard --history) |
castellan multiplexer ensure
castellan statusline refresh --once --json | jq '{schema_version, connected, bus, live_pressure}'
# MCP freshness
# castellan_multiplexer_status → { ok, stale, age_secs, status }
castellan-events schema (--json)
castellan run --json emits one serde-tagged event per line. Canonical variants include:
| Event | When |
|---|---|
episode_start / episode_end | Session boundaries |
scheduler_tick / wake_summary / task_dispatched | Dispatch |
deposit | Pheromone deposit (drives dashboard field heat) |
cost_rollup | Per-tick usd/tokens + session totals |
plan_verify_passed / plan_verify_denied | Guardians prove-before-execute |
quorum_fired / zone_quarantine / morphogenesis / genome_expression_applied | Bio chips |
topology_apply_drained | Mid-run topology apply |
verify_result / tool_decision / permission_denied / budget_exhausted | Governance |
mutation_proposed / mutation_resolved | Evolve proposals |
archive_write_back | Post-run archive merge |
Multiplexer events.subscribe bridges the same schema via EmittedEvent::from_castellan_event (fields live under data).
Metrics JSON for CI
castellan telemetry --limit 20 # vitals_count, session_cost, recent_* arrays
castellan statusline refresh --json | jq '{schema_version, session_cost, live_pressure, plan_verify, bio}'
Honest gap: Thin HTTP/SSE of the same snapshot is not shipped yet — not a browser SPA. Prefer
statusline.jsonand MCP for headless observers.
Failure paths
| Symptom | Cause | Fix |
|---|---|---|
statusline.json missing | Never refreshed | castellan statusline refresh or open castellan dashboard |
vitals null | No episode vitals yet | castellan run --goal "..." --plugin gridworld |
MCP stale: true | File older than ~30s | Re-run statusline refresh or keep dashboard open |
bus.panes=socket but attach empty | Looking at in-process run panes | Attach uses socket tree; ensure panes on mux socket |